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:
parent
45a3b53558
commit
0729487ae5
@ -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
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"/>
|
||||
|
@ -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));
|
||||
|
@ -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:
|
||||
*
|
||||
|
@ -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]
|
||||
|
@ -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
388
tests/utils/updateinfo
Executable 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())
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user