rpm-ostree/src/daemon/rpmostreed-deployment-utils.c
Jonathan Lebon 87ab4fafb6 libpriv: Add simpler base layer getter
In the majority of cases, we just want to know if a deployment is
layered and what the base commit is. Make a simpler accessor for this.
We can further simplify the logic on the client side here by relying on
the fact that there is only a base layer iif we're layered, so we don't
need two output variables.

I find it also makes things much easier to grok in some places, e.g. in
`rpmostreed-deployment-utils.c`, we now have:

    const char *current_base_checksum = current_base_checksum_owned ?: current_checksum;

which better crystallizes what we want to get at.

Or e.g. in the sysroot upgrader, we can drop a comment which states
something that is now completely obvious.

Closes: #1560
Approved by: cgwalters
2018-09-14 16:49:41 +00:00

1112 lines
42 KiB
C

/*
* Copyright (C) 2015 Red Hat, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "config.h"
#include <systemd/sd-journal.h>
#include <libglnx.h>
#include "rpmostree-types.h"
#include "rpmostreed-deployment-utils.h"
#include "rpmostree-origin.h"
#include "rpmostree-core.h"
#include "rpmostree-util.h"
#include "rpmostree-rpm-util.h"
#include "rpmostree-sysroot-core.h"
#include "rpmostree-core.h"
#include "rpmostree-package-variants.h"
#include "rpmostreed-utils.h"
#include "rpmostreed-errors.h"
/* Get a currently unique (for this host) identifier for the
* deployment; TODO - adding the deployment timestamp would make it
* persistently unique, needs API in libostree.
*/
char *
rpmostreed_deployment_generate_id (OstreeDeployment *deployment)
{
g_return_val_if_fail (OSTREE_IS_DEPLOYMENT (deployment), NULL);
return g_strdup_printf ("%s-%s.%u",
ostree_deployment_get_osname (deployment),
ostree_deployment_get_csum (deployment),
ostree_deployment_get_deployserial (deployment));
}
OstreeDeployment *
rpmostreed_deployment_get_for_id (OstreeSysroot *sysroot,
const gchar *deploy_id)
{
g_autoptr(GPtrArray) deployments = ostree_sysroot_get_deployments (sysroot);
for (guint i = 0; i < deployments->len; i++)
{
g_autofree gchar *id = rpmostreed_deployment_generate_id (deployments->pdata[i]);
if (g_strcmp0 (deploy_id, id) == 0)
return g_object_ref (deployments->pdata[i]);
}
return NULL;
}
/* rpmostreed_deployment_get_for_index:
*
* sysroot: A #OstreeSysroot instance
* index: string index being parsed
* error: a #GError instance
*
* Get a deployment based on a string index,
* the string is parsed and checked. Then
* the deployment at the parsed index will be
* returned.
*
* returns: NULL if an error is being made
*/
OstreeDeployment *
rpmostreed_deployment_get_for_index (OstreeSysroot *sysroot,
const gchar *index,
GError **error)
{
g_autoptr(GError) local_error = NULL;
int deployment_index = -1;
for (int counter = 0; counter < strlen(index); counter++)
{
if (!g_ascii_isdigit (index[counter]))
{
local_error = g_error_new (RPM_OSTREED_ERROR,
RPM_OSTREED_ERROR_FAILED,
"Invalid deployment index %s, must be a number and >= 0",
index);
g_propagate_error (error, g_steal_pointer (&local_error));
return NULL;
}
}
deployment_index = atoi (index);
g_autoptr(GPtrArray) deployments = ostree_sysroot_get_deployments (sysroot);
if (deployment_index >= deployments->len)
{
local_error = g_error_new (RPM_OSTREED_ERROR,
RPM_OSTREED_ERROR_FAILED,
"Out of range deployment index %d, expected < %d",
deployment_index, deployments->len);
g_propagate_error (error, g_steal_pointer (&local_error));
return NULL;
}
return g_object_ref (deployments->pdata[deployment_index]);
}
static gboolean
variant_add_remote_status (OstreeRepo *repo,
const gchar *origin_refspec,
const gchar *checksum,
GVariantDict *dict,
GError **error)
{
GLNX_AUTO_PREFIX_ERROR ("Loading origin status", error);
g_autofree gchar *remote = NULL;
if (!ostree_parse_refspec (origin_refspec, &remote, NULL, error))
return FALSE;
g_autoptr(GError) local_error = NULL;
gboolean gpg_verify = FALSE;
if (remote)
{
if (!ostree_repo_remote_get_gpg_verify (repo, remote, &gpg_verify, &local_error))
{
/* If the remote doesn't exist, let's note that so that status can
* render it specially.
*/
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
{
g_variant_dict_insert (dict, "remote-error", "s", local_error->message);
return TRUE;
}
g_propagate_error (error, g_steal_pointer (&local_error));
return FALSE;
}
}
g_variant_dict_insert (dict, "gpg-enabled", "b", gpg_verify);
if (!gpg_verify)
return TRUE; /* Note early return; no need to verify signatures! */
g_autoptr(OstreeGpgVerifyResult) verify_result =
ostree_repo_verify_commit_for_remote (repo, checksum, remote, NULL, NULL);
if (!verify_result)
{
/* Somehow, we have a deployment which has gpg-verify=true, but *doesn't* have a valid
* signature. Let's not just bomb out here. We need to return this in the variant so
* that `status` can show the appropriate msg. */
return TRUE;
}
g_auto(GVariantBuilder) builder;
g_variant_builder_init (&builder, G_VARIANT_TYPE ("av"));
guint n_sigs = ostree_gpg_verify_result_count_all (verify_result);
for (guint i = 0; i < n_sigs; i++)
g_variant_builder_add (&builder, "v", ostree_gpg_verify_result_get_all (verify_result, i));
g_variant_dict_insert_value (dict, "signatures", g_variant_builder_end (&builder));
return TRUE;
}
GVariant *
rpmostreed_deployment_generate_blank_variant (void)
{
GVariantDict dict;
g_variant_dict_init (&dict, NULL);
return g_variant_dict_end (&dict);
}
static void
variant_add_metadata_attribute (GVariantDict *dict,
const gchar *attribute,
const gchar *new_attribute,
GVariant *commit)
{
g_autofree gchar *attribute_string_value = NULL;
g_autoptr(GVariant) metadata = g_variant_get_child_value (commit, 0);
if (metadata != NULL)
{
g_variant_lookup (metadata, attribute, "s", &attribute_string_value);
if (attribute_string_value != NULL)
g_variant_dict_insert (dict, new_attribute ?: attribute, "s", attribute_string_value);
}
}
static void
variant_add_commit_details (GVariantDict *dict,
const char *prefix,
GVariant *commit)
{
g_autoptr(GVariant) metadata = NULL;
g_autofree gchar *version_commit = NULL;
guint64 timestamp = 0;
timestamp = ostree_commit_get_timestamp (commit);
metadata = g_variant_get_child_value (commit, 0);
if (metadata != NULL)
g_variant_lookup (metadata, "version", "s", &version_commit);
if (version_commit != NULL)
g_variant_dict_insert (dict, glnx_strjoina (prefix ?: "", "version"),
"s", version_commit);
if (timestamp > 0)
g_variant_dict_insert (dict, glnx_strjoina (prefix ?: "", "timestamp"),
"t", timestamp);
}
static void
variant_add_from_hash_table (GVariantDict *dict,
const char *key,
GHashTable *table)
{
g_autofree char **values = (char**)g_hash_table_get_keys_as_array (table, NULL);
g_variant_dict_insert (dict, key, "^as", values);
}
GVariant *
rpmostreed_deployment_generate_variant (OstreeSysroot *sysroot,
OstreeDeployment *deployment,
const char *booted_id,
OstreeRepo *repo,
GError **error)
{
const gchar *osname = ostree_deployment_get_osname (deployment);
const gchar *csum = ostree_deployment_get_csum (deployment);
gint serial = ostree_deployment_get_deployserial (deployment);
g_autoptr(GVariant) commit = NULL;
if (!ostree_repo_load_variant (repo,
OSTREE_OBJECT_TYPE_COMMIT,
csum,
&commit,
error))
return NULL;
g_autofree gchar *id = rpmostreed_deployment_generate_id (deployment);
g_autoptr(RpmOstreeOrigin) origin = rpmostree_origin_parse_deployment (deployment, error);
if (!origin)
return NULL;
RpmOstreeRefspecType refspec_type;
g_autofree char *refspec = rpmostree_origin_get_full_refspec (origin, &refspec_type);
GVariantDict dict;
g_variant_dict_init (&dict, NULL);
g_variant_dict_insert (&dict, "id", "s", id);
if (osname != NULL)
g_variant_dict_insert (&dict, "osname", "s", osname);
g_variant_dict_insert (&dict, "serial", "i", serial);
g_variant_dict_insert (&dict, "checksum", "s", csum);
gboolean is_layered = FALSE;
g_autofree char *base_checksum = NULL;
g_auto(GStrv) layered_pkgs = NULL;
g_autoptr(GVariant) removed_base_pkgs = NULL;
g_autoptr(GVariant) replaced_base_pkgs = NULL;
if (!rpmostree_deployment_get_layered_info (repo, deployment, &is_layered, &base_checksum,
&layered_pkgs, &removed_base_pkgs,
&replaced_base_pkgs, error))
return NULL;
g_autoptr(GVariant) base_commit = NULL;
if (is_layered)
{
if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT,
base_checksum, &base_commit, error))
return NULL;
g_variant_dict_insert (&dict, "base-checksum", "s", base_checksum);
variant_add_commit_details (&dict, "base-", base_commit);
/* for layered commits, check if their base commit has end of life attribute */
variant_add_metadata_attribute (&dict, OSTREE_COMMIT_META_KEY_ENDOFLIFE, "endoflife", base_commit);
/* See below for base commit metadata */
g_autoptr(GVariant) layered_metadata = g_variant_get_child_value (commit, 0);
g_variant_dict_insert (&dict, "layered-commit-meta", "@a{sv}", layered_metadata);
}
else
{
base_commit = g_variant_ref (commit);
base_checksum = g_strdup (csum);
variant_add_metadata_attribute (&dict, OSTREE_COMMIT_META_KEY_ENDOFLIFE, "endoflife", commit);
}
/* We used to bridge individual keys, but that was annoying; just pass through all
* of the commit metadata.
*/
{ g_autoptr(GVariant) base_meta = g_variant_get_child_value (base_commit, 0);
g_variant_dict_insert (&dict, "base-commit-meta", "@a{sv}", base_meta);
}
variant_add_commit_details (&dict, NULL, commit);
switch (refspec_type)
{
case RPMOSTREE_REFSPEC_TYPE_CHECKSUM:
{
g_autofree char *custom_origin_url = NULL;
g_autofree char *custom_origin_description = NULL;
rpmostree_origin_get_custom_description (origin, &custom_origin_url,
&custom_origin_description);
if (custom_origin_url)
g_variant_dict_insert (&dict, "custom-origin", "(ss)",
custom_origin_url,
custom_origin_description);
}
break;
case RPMOSTREE_REFSPEC_TYPE_OSTREE:
{
if (!variant_add_remote_status (repo, refspec, base_checksum, &dict, error))
return NULL;
g_autofree char *pending_base_commitrev = NULL;
if (!ostree_repo_resolve_rev (repo, refspec, TRUE,
&pending_base_commitrev, error))
return NULL;
if (pending_base_commitrev && !g_str_equal (pending_base_commitrev, base_checksum))
{
g_autoptr(GVariant) pending_base_commit = NULL;
if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT,
pending_base_commitrev, &pending_base_commit,
error))
return NULL;
g_variant_dict_insert (&dict, "pending-base-checksum", "s", pending_base_commitrev);
variant_add_commit_details (&dict, "pending-base-", pending_base_commit);
}
}
break;
case RPMOSTREE_REFSPEC_TYPE_ROJIG:
{
g_variant_dict_insert (&dict, "rojig-description", "@a{sv}",
rpmostree_origin_get_rojig_description (origin));
}
break;
}
g_autofree char *live_inprogress = NULL;
g_autofree char *live_replaced = NULL;
if (!rpmostree_syscore_deployment_get_live (sysroot, deployment, &live_inprogress,
&live_replaced, error))
return NULL;
if (live_inprogress)
g_variant_dict_insert (&dict, "live-inprogress", "s", live_inprogress);
if (live_replaced)
g_variant_dict_insert (&dict, "live-replaced", "s", live_replaced);
if (ostree_deployment_is_staged (deployment))
g_variant_dict_insert (&dict, "staged", "b", TRUE);
if (refspec)
g_variant_dict_insert (&dict, "origin", "s", refspec);
variant_add_from_hash_table (&dict, "requested-packages",
rpmostree_origin_get_packages (origin));
variant_add_from_hash_table (&dict, "requested-local-packages",
rpmostree_origin_get_local_packages (origin));
variant_add_from_hash_table (&dict, "requested-base-removals",
rpmostree_origin_get_overrides_remove (origin));
variant_add_from_hash_table (&dict, "requested-base-local-replacements",
rpmostree_origin_get_overrides_local_replace (origin));
g_variant_dict_insert (&dict, "packages", "^as", layered_pkgs);
g_variant_dict_insert_value (&dict, "base-removals", removed_base_pkgs);
g_variant_dict_insert_value (&dict, "base-local-replacements", replaced_base_pkgs);
g_variant_dict_insert (&dict, "pinned", "b",
ostree_deployment_is_pinned (deployment));
g_variant_dict_insert (&dict, "unlocked", "s",
ostree_deployment_unlocked_state_to_string (ostree_deployment_get_unlocked (deployment)));
g_variant_dict_insert (&dict, "regenerate-initramfs", "b",
rpmostree_origin_get_regenerate_initramfs (origin));
{ const char *const* args = rpmostree_origin_get_initramfs_args (origin);
if (args && *args)
g_variant_dict_insert (&dict, "initramfs-args", "^as", args);
}
if (booted_id != NULL)
g_variant_dict_insert (&dict, "booted", "b", g_strcmp0 (booted_id, id) == 0);
return g_variant_dict_end (&dict);
}
/* Adds the following keys to the vardict:
* - osname
* - checksum
* - version
* - timestamp
* - origin
* - signatures
* - gpg-enabled
*
* If not %NULL, @refspec and @checksum override defaults from @deployment. They can also be
* used to avoid lookups if they're already available.
* */
static gboolean
add_all_commit_details_to_vardict (OstreeDeployment *deployment,
OstreeRepo *repo,
const char *refspec, /* allow-none */
const char *checksum, /* allow-none */
GVariant *commit, /* allow-none */
GVariantDict *dict, /* allow-none */
GError **error)
{
const gchar *osname = ostree_deployment_get_osname (deployment);
g_autofree gchar *refspec_owned = NULL;
gboolean refspec_is_ostree = FALSE;
RpmOstreeRefspecType refspec_type;
if (!refspec)
{
g_autoptr(RpmOstreeOrigin) origin =
rpmostree_origin_parse_deployment (deployment, error);
if (!origin)
return FALSE;
refspec = refspec_owned = rpmostree_origin_get_full_refspec (origin, &refspec_type);
}
else
{
const char *refspec_remainder = NULL;
if (!rpmostree_refspec_classify (refspec, &refspec_type, &refspec_remainder, error))
return FALSE;
refspec = refspec_remainder;
}
refspec_is_ostree = refspec_type == RPMOSTREE_REFSPEC_TYPE_OSTREE;
if (refspec_type == RPMOSTREE_REFSPEC_TYPE_CHECKSUM && !commit)
checksum = refspec;
g_assert (refspec);
/* allow_noent=TRUE since the ref may have been deleted for a rebase */
g_autofree gchar *checksum_owned = NULL;
if (!checksum)
{
if (refspec_is_ostree)
{
if (!ostree_repo_resolve_rev (repo, refspec, TRUE, &checksum_owned, error))
return FALSE;
}
/* in that case, use deployment csum */
checksum = checksum_owned ?: ostree_deployment_get_csum (deployment);
}
g_autoptr(GVariant) commit_owned = NULL;
if (!commit)
{
if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, checksum,
&commit_owned, error))
return FALSE;
commit = commit_owned;
}
if (refspec_is_ostree)
{
if (!variant_add_remote_status (repo, refspec, checksum, dict, error))
return FALSE;
}
if (osname != NULL)
g_variant_dict_insert (dict, "osname", "s", osname);
g_variant_dict_insert (dict, "checksum", "s", checksum);
variant_add_commit_details (dict, NULL, commit);
g_variant_dict_insert (dict, "origin", "s", refspec);
return TRUE;
}
GVariant *
rpmostreed_commit_generate_cached_details_variant (OstreeDeployment *deployment,
OstreeRepo *repo,
const char *refspec,
const char *checksum,
GError **error)
{
GVariantDict dict;
g_variant_dict_init (&dict, NULL);
if (!add_all_commit_details_to_vardict (deployment, repo, refspec,
checksum, NULL, &dict, error))
return NULL;
return g_variant_ref_sink (g_variant_dict_end (&dict));
}
typedef struct {
gboolean initialized;
GPtrArray *upgraded;
GPtrArray *downgraded;
GPtrArray *removed;
GPtrArray *added;
} RpmDiff;
static void
rpm_diff_init (RpmDiff *diff)
{
g_assert (!diff->initialized);
diff->upgraded = g_ptr_array_new_with_free_func ((GDestroyNotify)g_variant_unref);
diff->downgraded = g_ptr_array_new_with_free_func ((GDestroyNotify)g_variant_unref);
diff->removed = g_ptr_array_new_with_free_func ((GDestroyNotify)g_variant_unref);
diff->added = g_ptr_array_new_with_free_func ((GDestroyNotify)g_variant_unref);
diff->initialized = TRUE;
}
static void
rpm_diff_clear (RpmDiff *diff)
{
if (!diff->initialized)
return;
g_clear_pointer (&diff->upgraded, (GDestroyNotify)g_ptr_array_unref);
g_clear_pointer (&diff->downgraded, (GDestroyNotify)g_ptr_array_unref);
g_clear_pointer (&diff->removed, (GDestroyNotify)g_ptr_array_unref);
g_clear_pointer (&diff->added, (GDestroyNotify)g_ptr_array_unref);
diff->initialized = FALSE;
}
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (RpmDiff, rpm_diff_clear);
static GVariant*
single_pkg_variant_new (RpmOstreePkgTypes type,
RpmOstreePackage *pkg)
{
return g_variant_ref_sink (
g_variant_new ("(usss)", type,
rpm_ostree_package_get_name (pkg),
rpm_ostree_package_get_evr (pkg),
rpm_ostree_package_get_arch (pkg)));
}
static GVariant*
modified_pkg_variant_new (RpmOstreePkgTypes type,
RpmOstreePackage *pkg_old,
RpmOstreePackage *pkg_new)
{
const char *name_old = rpm_ostree_package_get_name (pkg_old);
const char *name_new = rpm_ostree_package_get_name (pkg_new);
g_assert_cmpstr (name_old, ==, name_new);
return g_variant_ref_sink (
g_variant_new ("(us(ss)(ss))", type, name_old,
rpm_ostree_package_get_evr (pkg_old),
rpm_ostree_package_get_arch (pkg_old),
rpm_ostree_package_get_evr (pkg_new),
rpm_ostree_package_get_arch (pkg_new)));
}
static GVariant*
modified_dnfpkg_variant_new (RpmOstreePkgTypes type,
RpmOstreePackage *pkg_old,
DnfPackage *pkg_new)
{
const char *name_old = rpm_ostree_package_get_name (pkg_old);
const char *name_new = dnf_package_get_name (pkg_new);
g_assert_cmpstr (name_old, ==, name_new);
return g_variant_ref_sink (
g_variant_new ("(us(ss)(ss))", type, name_old,
rpm_ostree_package_get_evr (pkg_old),
rpm_ostree_package_get_arch (pkg_old),
dnf_package_get_evr (pkg_new),
dnf_package_get_arch (pkg_new)));
}
static gboolean
rpm_diff_add_db_diff (RpmDiff *diff,
OstreeRepo *repo,
RpmOstreePkgTypes type,
const char *old_checksum,
const char *new_checksum,
GPtrArray **out_modified_new,
GCancellable *cancellable,
GError **error)
{
g_autoptr(GPtrArray) removed = NULL;
g_autoptr(GPtrArray) added = NULL;
g_autoptr(GPtrArray) modified_old = NULL;
g_autoptr(GPtrArray) modified_new = NULL;
/* Use allow_noent; we'll just skip over the rpm diff if there's no data */
RpmOstreeDbDiffExtFlags flags = RPM_OSTREE_DB_DIFF_EXT_ALLOW_NOENT;
if (!rpm_ostree_db_diff_ext (repo, old_checksum, new_checksum, flags,
&removed, &added, &modified_old, &modified_new,
cancellable, error))
return FALSE;
/* check if allow_noent kicked in */
if (!removed)
return TRUE; /* NB: early return */
g_assert_cmpuint (modified_old->len, ==, modified_new->len);
for (guint i = 0; i < removed->len; i++)
g_ptr_array_add (diff->removed, single_pkg_variant_new (type, removed->pdata[i]));
for (guint i = 0; i < added->len; i++)
g_ptr_array_add (diff->added, single_pkg_variant_new (type, added->pdata[i]));
for (guint i = 0; i < modified_old->len; i++)
{
RpmOstreePackage *old_pkg = modified_old->pdata[i];
RpmOstreePackage *new_pkg = modified_new->pdata[i];
if (rpm_ostree_package_cmp (old_pkg, new_pkg) < 0)
g_ptr_array_add (diff->upgraded,
modified_pkg_variant_new (type, old_pkg, new_pkg));
else
g_ptr_array_add (diff->downgraded,
modified_pkg_variant_new (type, old_pkg, new_pkg));
}
if (out_modified_new)
*out_modified_new = g_steal_pointer (&modified_new);
return TRUE;
}
static void
rpm_diff_add_layered_diff (RpmDiff *diff,
RpmOstreePackage *old_pkg,
DnfPackage *new_pkg)
{
/* add to upgraded; layered pkgs only go up */
RpmOstreePkgTypes type = RPM_OSTREE_PKG_TYPE_LAYER;
g_ptr_array_add (diff->upgraded, modified_dnfpkg_variant_new (type, old_pkg, new_pkg));
}
static int
sort_pkgvariant_by_name (gconstpointer pkga_pp,
gconstpointer pkgb_pp)
{
GVariant *pkg_a = *((GVariant**)pkga_pp);
GVariant *pkg_b = *((GVariant**)pkgb_pp);
const char *pkgname_a;
g_variant_get_child (pkg_a, 1, "&s", &pkgname_a);
const char *pkgname_b;
g_variant_get_child (pkg_b, 1, "&s", &pkgname_b);
return strcmp (pkgname_a, pkgname_b);
}
static GVariant*
array_to_variant_new (const char *format, GPtrArray *array)
{
if (array->len == 0)
return g_variant_new (format, NULL);
/* make doubly sure it's sorted */
g_ptr_array_sort (array, sort_pkgvariant_by_name);
g_auto(GVariantBuilder) builder;
g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
for (guint i = 0; i < array->len; i++)
g_variant_builder_add_value (&builder, array->pdata[i]);
return g_variant_builder_end (&builder);
}
static gboolean
rpm_diff_is_empty (RpmDiff *diff)
{
g_assert (diff->initialized);
return !diff->upgraded->len &&
!diff->downgraded->len &&
!diff->removed->len &&
!diff->added->len;
}
static GVariant*
rpm_diff_variant_new (RpmDiff *diff)
{
g_assert (diff->initialized);
g_auto(GVariantDict) dict;
g_variant_dict_init (&dict, NULL);
g_variant_dict_insert_value (&dict, "upgraded", array_to_variant_new (
RPMOSTREE_DIFF_MODIFIED_GVARIANT_STRING, diff->upgraded));
g_variant_dict_insert_value (&dict, "downgraded", array_to_variant_new (
RPMOSTREE_DIFF_MODIFIED_GVARIANT_STRING, diff->downgraded));
g_variant_dict_insert_value (&dict, "removed", array_to_variant_new (
RPMOSTREE_DIFF_SINGLE_GVARIANT_STRING, diff->removed));
g_variant_dict_insert_value (&dict, "added", array_to_variant_new (
RPMOSTREE_DIFF_SINGLE_GVARIANT_STRING, diff->added));
return g_variant_dict_end (&dict);
}
static DnfPackage*
find_package (DnfSack *sack,
gboolean newer,
RpmOstreePackage *pkg)
{
hy_autoquery HyQuery query = hy_query_create (sack);
hy_query_filter (query, HY_PKG_NAME, HY_EQ, rpm_ostree_package_get_name (pkg));
if (newer)
{
hy_query_filter (query, HY_PKG_EVR, HY_GT, rpm_ostree_package_get_evr (pkg));
hy_query_filter (query, HY_PKG_ARCH, HY_NEQ, "src");
hy_query_filter_latest (query, TRUE);
}
else
{
/* we want an exact match */
hy_query_filter (query, HY_PKG_NEVRA, HY_EQ, rpm_ostree_package_get_nevra (pkg));
}
g_autoptr(GPtrArray) pkgs = hy_query_run (query);
if (pkgs->len == 0)
return NULL; /* canonicalize to NULL */
g_ptr_array_sort (pkgs, (GCompareFunc)rpmostree_pkg_array_compare);
return g_object_ref (pkgs->pdata[pkgs->len-1]);
}
/* For all layered pkgs, check if there are newer versions in the rpmmd. Add diff to
* @rpm_diff, and all new pkgs in @out_newer_packages (these are used later for advisories).
* */
static gboolean
rpmmd_diff_guess (OstreeRepo *repo,
const char *base_checksum,
const char *layered_checksum,
DnfSack *sack,
RpmDiff *rpm_diff,
GPtrArray **out_newer_packages,
GError **error)
{
/* Note here that we *don't* actually use layered_pkgs; we want to look at all the RPMs
* installed, whereas the layered pkgs (actually patterns) just represent top-level
* entries. IOW, we want to run through all layered RPMs, which include deps of
* layered_pkgs. */
g_autoptr(GPtrArray) all_layered_pkgs = NULL;
RpmOstreeDbDiffExtFlags flags = RPM_OSTREE_DB_DIFF_EXT_ALLOW_NOENT;
if (!rpm_ostree_db_diff_ext (repo, base_checksum, layered_checksum, flags, NULL,
&all_layered_pkgs, NULL, NULL, NULL, error))
return FALSE;
/* XXX: need to filter out local pkgs; though we still want to check for advisories --
* maybe we should do this in status.c instead? */
if (all_layered_pkgs == NULL || /* -> older layer before we injected pkglist metadata */
all_layered_pkgs->len == 0) /* -> no layered pkgs, e.g. override remove only */
{
*out_newer_packages = NULL;
return TRUE; /* note early return */
}
/* for each layered pkg, check if there's a newer version available (in reality, there may
* be other new pkgs that need to be layered or some pkgs that no longer need to, but we
* won't find out until we have the full commit available -- XXX: we could go the extra
* effort and use the rpmdb of new_checksum if we already have it somehow, though that's
* probably not the common case */
g_autoptr(GPtrArray) newer_packages =
g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
for (guint i = 0; i < all_layered_pkgs->len; i++)
{
RpmOstreePackage *pkg = all_layered_pkgs->pdata[i];
g_autoptr(DnfPackage) newer_pkg = find_package (sack, TRUE, pkg);
if (!newer_pkg)
continue;
g_ptr_array_add (newer_packages, g_object_ref (newer_pkg));
rpm_diff_add_layered_diff (rpm_diff, pkg, newer_pkg);
}
/* canonicalize to NULL if there's nothing new */
if (newer_packages->len == 0)
g_clear_pointer (&newer_packages, (GDestroyNotify)g_ptr_array_unref);
*out_newer_packages = g_steal_pointer (&newer_packages);
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);
}
/* The variant returned by this function is backwards compatible with the one returned by
* rpmostreed_commit_generate_cached_details_variant(). However, it also includes a base
* tree db diff, layered pkgs diff, state, advisories, etc... Also, it will happily return
* %NULL if no updates are available.
*
* If @staged_deployment is %NULL, update details are based on latest downloaded ostree
* rpmmd metadata. If @staged_deployment is not %NULL, then the update describes the diff
* between @booted_deployment and @staged_deployment. */
gboolean
rpmostreed_update_generate_variant (OstreeDeployment *booted_deployment,
OstreeDeployment *staged_deployment,
OstreeRepo *repo,
DnfSack *sack, /* allow-none */
GVariant **out_update,
GCancellable *cancellable,
GError **error)
{
GLNX_AUTO_PREFIX_ERROR ("Generating update variant", error);
g_autoptr(RpmOstreeOrigin) origin =
rpmostree_origin_parse_deployment (booted_deployment, error);
if (!origin)
return FALSE;
const char *refspec = rpmostree_origin_get_refspec (origin);
{ RpmOstreeRefspecType refspectype = RPMOSTREE_REFSPEC_TYPE_OSTREE;
const char *refspec_data;
if (!rpmostree_refspec_classify (refspec, &refspectype, &refspec_data, error))
return FALSE;
/* we don't support rojig-based origins yet */
switch (refspectype)
{
case RPMOSTREE_REFSPEC_TYPE_ROJIG:
*out_update = NULL;
return TRUE; /* NB: early return */
case RPMOSTREE_REFSPEC_TYPE_OSTREE:
case RPMOSTREE_REFSPEC_TYPE_CHECKSUM:
break;
}
/* just skip over "ostree://" so we can talk with libostree without thinking about it */
refspec = refspec_data;
}
/* let's start with the ostree side of things */
const char *current_checksum = ostree_deployment_get_csum (booted_deployment);
g_autofree char *current_base_checksum_owned = NULL;
if (!rpmostree_deployment_get_base_layer (repo, booted_deployment,
&current_base_checksum_owned, error))
return FALSE;
const char *current_base_checksum = current_base_checksum_owned ?: current_checksum;
gboolean is_new_layered = FALSE;
const char *new_checksum = NULL;
const char *new_base_checksum = NULL;
g_autofree char *new_base_checksum_owned = NULL;
if (staged_deployment)
{
new_checksum = ostree_deployment_get_csum (staged_deployment);
if (!rpmostree_deployment_get_base_layer (repo, staged_deployment,
&new_base_checksum_owned, error))
return FALSE;
new_base_checksum = new_base_checksum_owned ?: new_checksum;
}
else
{
if (!ostree_repo_resolve_rev_ext (repo, refspec, TRUE, 0,
&new_base_checksum_owned, error))
return FALSE;
new_base_checksum = new_base_checksum_owned;
/* just assume that the hypothetical new deployment would also be layered if we are */
is_new_layered = (current_base_checksum_owned != NULL);
}
/* Graciously handle rev no longer in repo; e.g. mucking around with rebase/rollback; we
* still want to do the rpm-md phase. In that case, just use the current csum. */
gboolean is_new_checksum = FALSE;
if (!new_base_checksum)
new_base_checksum = current_base_checksum;
else
is_new_checksum = !g_str_equal (new_base_checksum, current_base_checksum);
g_autoptr(GVariant) commit = NULL;
if (!ostree_repo_load_commit (repo, new_base_checksum, &commit, NULL, error))
return FALSE;
g_auto(GVariantDict) dict;
g_variant_dict_init (&dict, NULL);
/* first get all the traditional/backcompat stuff */
if (!add_all_commit_details_to_vardict (booted_deployment, repo, refspec,
new_base_checksum, commit, &dict, error))
return FALSE;
/* This may seem trivial, but it's important to keep the final variant as self-contained
* and "diff-based" as possible, since it'll be available as a D-Bus property. This makes
* it easier to consume for UIs like GNOME Software and Cockpit. */
g_variant_dict_insert (&dict, "ref-has-new-commit", "b", is_new_checksum);
g_auto(RpmDiff) rpm_diff = {0, };
rpm_diff_init (&rpm_diff);
/* we'll need these later for advisories, so just keep them around */
g_autoptr(GPtrArray) ostree_modified_new = NULL;
g_autoptr(GPtrArray) rpmmd_modified_new = NULL;
if (staged_deployment)
{
/* ok we have a staged deployment; we just need to do a simple diff and BOOM done! */
/* XXX: we're marking all pkgs as BASE right now even though there could be layered
* pkgs too -- we can tease those out in the future if needed */
if (!rpm_diff_add_db_diff (&rpm_diff, repo, RPM_OSTREE_PKG_TYPE_BASE,
current_checksum, new_checksum, &ostree_modified_new,
cancellable, error))
return FALSE;
}
else
{
/* no staged deployment; we do our best to come up with a diff:
* - if a new base checksum was pulled, do a db diff of the old and new bases
* - if there are currently any layered pkgs, lookup in sack for newer versions
*/
if (is_new_checksum)
{
if (!rpm_diff_add_db_diff (&rpm_diff, repo, RPM_OSTREE_PKG_TYPE_BASE,
current_base_checksum, new_base_checksum,
&ostree_modified_new, cancellable, error))
return FALSE;
}
/* now we look at the rpm-md/layering side */
GHashTable *layered_pkgs = rpmostree_origin_get_packages (origin);
/* check that it's actually layered (i.e. the requests are not all just dormant) */
if (sack && is_new_layered && g_hash_table_size (layered_pkgs) > 0)
{
if (!rpmmd_diff_guess (repo, current_base_checksum, current_checksum, sack,
&rpm_diff, &rpmmd_modified_new, error))
return FALSE;
}
}
/* don't bother inserting if there's nothing new */
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);
}
if (staged_deployment)
{
g_autofree char *id = rpmostreed_deployment_generate_id (staged_deployment);
g_variant_dict_insert (&dict, "deployment", "s", id);
}
/* but if there are no updates, then just ditch the whole thing and return NULL */
if (is_new_checksum || rpmmd_modified_new)
{
/* include a "state" checksum for cache invalidation; for now this is just the
* checksum of the deployment against which we ran, though we could base it off more
* things later if needed */
g_variant_dict_insert (&dict, "update-sha256", "s", current_checksum);
*out_update = g_variant_ref_sink (g_variant_dict_end (&dict));
}
else
*out_update = NULL;
return TRUE;
}