From 51fb64130583a99d0b2d23c3130e3c4b369addd5 Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Fri, 15 Dec 2017 17:01:46 +0000 Subject: [PATCH] Initial support for automatic updates This patch introduces a new `AutomaticUpdatePolicy` configuration. This was a long time coming for rpm-ostree, given that its update model makes it extremely apt for such a feature. The config supports a `check` mode, which should be very useful to Atomic Workstation users, as well as a `reboot` mode, which could be used in its present form in simple single node Atomic Host situations. There is still a lot of work to be done, including integrating advisories, and supporting a `deploy` mode. This feature hopefully will be leveraged as well by higher-level projects like GNOME Software and Cockpit. Closes: #1147 Approved by: cgwalters --- Makefile-daemon.am | 22 +- Makefile-libpriv.am | 2 + Makefile-tests.am | 14 +- man/rpm-ostreed.conf.xml | 17 +- src/app/rpmostree-builtin-status.c | 123 +++++- src/app/rpmostree-builtin-upgrade.c | 51 ++- src/app/rpmostree-dbus-helpers.c | 130 +++++- src/app/rpmostree-dbus-helpers.h | 6 + src/daemon/org.projectatomic.rpmostree1.xml | 27 +- src/daemon/rpm-ostreed-automatic.service.in | 8 + src/daemon/rpm-ostreed-automatic.timer | 11 + src/daemon/rpm-ostreed.conf | 1 + src/daemon/rpmostreed-daemon.c | 51 ++- src/daemon/rpmostreed-daemon.h | 4 + src/daemon/rpmostreed-deployment-utils.c | 461 +++++++++++++++++++- src/daemon/rpmostreed-deployment-utils.h | 6 + src/daemon/rpmostreed-os.c | 164 +++++-- src/daemon/rpmostreed-sysroot.c | 23 +- src/daemon/rpmostreed-transaction-types.c | 63 ++- src/daemon/rpmostreed-transaction-types.h | 2 + src/libpriv/libsd-time-util.c | 128 ++++++ src/libpriv/libsd-time-util.h | 67 +++ src/libpriv/rpmostree-core.c | 4 - src/libpriv/rpmostree-core.h | 4 + src/libpriv/rpmostree-rpm-util.c | 12 +- src/libpriv/rpmostree-rpm-util.h | 4 + src/libpriv/rpmostree-util.c | 78 +++- src/libpriv/rpmostree-util.h | 29 ++ tests/common/libtest.sh | 9 +- tests/common/libvm.sh | 2 +- tests/utils/inject-pkglist.c | 99 +++++ tests/vmcheck/overlay.sh | 42 ++ tests/vmcheck/test-autoupdate.sh | 186 ++++++++ tests/vmcheck/test.sh | 3 + 34 files changed, 1763 insertions(+), 90 deletions(-) create mode 100644 src/daemon/rpm-ostreed-automatic.service.in create mode 100644 src/daemon/rpm-ostreed-automatic.timer create mode 100644 src/libpriv/libsd-time-util.c create mode 100644 src/libpriv/libsd-time-util.h create mode 100644 tests/utils/inject-pkglist.c create mode 100755 tests/vmcheck/test-autoupdate.sh diff --git a/Makefile-daemon.am b/Makefile-daemon.am index bfe0e40b..f4bbf137 100644 --- a/Makefile-daemon.am +++ b/Makefile-daemon.am @@ -74,15 +74,26 @@ librpmostreed_la_LIBADD = \ dbusconf_DATA = $(srcdir)/src/daemon/org.projectatomic.rpmostree1.conf dbusconfdir = ${sysconfdir}/dbus-1/system.d -systemdunit_in_files = $(srcdir)/src/daemon/rpm-ostreed.service.in -systemdunit_DATA = $(systemdunit_in_files:.service.in=.service) +systemdunit_service_in_files = \ + $(srcdir)/src/daemon/rpm-ostreed.service.in \ + $(srcdir)/src/daemon/rpm-ostreed-automatic.service.in \ + $(NULL) + +systemdunit_service_files = $(systemdunit_service_in_files:.service.in=.service) +systemdunit_timer_files = $(srcdir)/src/daemon/rpm-ostreed-automatic.timer + +systemdunit_DATA = \ + $(systemdunit_service_files) \ + $(systemdunit_timer_files) \ + $(NULL) + systemdunitdir = $(prefix)/lib/systemd/system/ if BUILDOPT_ASAN daemon_asan_options = -e s,@SYSTEMD_ENVIRON\@,Environment=ASAN_OPTIONS=detect_leaks=false, else daemon_asan_options = -e /@SYSTEMD_ENVIRON\@/d endif -$(systemdunit_DATA): Makefile +$(systemdunit_service_files): Makefile $(SED_SUBST) $(daemon_asan_options) $@.in > $@ # We keep this stub script around to have SELinux labeling work, @@ -119,10 +130,11 @@ EXTRA_DIST += \ $(polkit_policy_DATA) \ $(sysconf_DATA) \ $(service_in_files) \ - $(systemdunit_in_files) \ + $(systemdunit_service_in_files) \ + $(systemdunit_timer_files) \ $(NULL) CLEANFILES += \ $(service_DATA) \ - $(systemdunit_DATA) \ + $(systemdunit_service_files) \ $(NULL) diff --git a/Makefile-libpriv.am b/Makefile-libpriv.am index 320e57ec..c50b642c 100644 --- a/Makefile-libpriv.am +++ b/Makefile-libpriv.am @@ -62,6 +62,8 @@ librpmostreepriv_la_SOURCES = \ src/libpriv/rpmostree-editor.h \ src/libpriv/libsd-locale-util.c \ src/libpriv/libsd-locale-util.h \ + src/libpriv/libsd-time-util.c \ + src/libpriv/libsd-time-util.h \ src/libpriv/rpmostree-libarchive-input-stream.c \ src/libpriv/rpmostree-libarchive-input-stream.h \ $(NULL) diff --git a/Makefile-tests.am b/Makefile-tests.am index d7d3d4b0..cd37ff61 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -18,7 +18,7 @@ if BUILDOPT_ASAN AM_TESTS_ENVIRONMENT += BUILDOPT_ASAN=yes ASAN_OPTIONS=detect_leaks=false endif -testbin_cppflags = $(AM_CPPFLAGS) -I $(srcdir)/src/libpriv -I $(srcdir)/libglnx -I $(srcdir)/tests/common +testbin_cppflags = $(AM_CPPFLAGS) -I $(srcdir)/src/lib -I $(srcdir)/src/libpriv -I $(srcdir)/libglnx -I $(srcdir)/tests/common testbin_cflags = $(AM_CFLAGS) $(PKGDEP_RPMOSTREE_CFLAGS) testbin_ldadd = $(PKGDEP_RPMOSTREE_LIBS) librpmostree-1.la librpmostreepriv.la @@ -56,10 +56,18 @@ uninstalled_test_scripts = \ tests/check/test-ucontainer.sh \ $(NULL) -uninstalled_test_extra_programs = dbus-run-session +uninstalled_test_extra_programs = \ + inject-pkglist \ + dbus-run-session \ + $(NULL) dbus_run_session_SOURCES = tests/utils/dbus-run-session.c +inject_pkglist_CPPFLAGS = $(testbin_cppflags) +inject_pkglist_CFLAGS = $(testbin_cflags) +inject_pkglist_LDADD = $(testbin_ldadd) libtest.la +inject_pkglist_SOURCES = tests/utils/inject-pkglist.c + check-local: @echo " *** NOTE ***" @echo " *** NOTE ***" @@ -78,7 +86,7 @@ vmsync: fi; \ env $(BASE_TESTS_ENVIRONMENT) ./tests/vmcheck/sync.sh -vmoverlay: +vmoverlay: inject-pkglist @set -e; if [ -z "$(SKIP_VMOVERLAY)" ]; then \ if [ -z "$(SKIP_INSTALL)" ]; then \ env $(BASE_TESTS_ENVIRONMENT) ./tests/vmcheck/install.sh; \ diff --git a/man/rpm-ostreed.conf.xml b/man/rpm-ostreed.conf.xml index 42deabcb..fd3e6e1a 100644 --- a/man/rpm-ostreed.conf.xml +++ b/man/rpm-ostreed.conf.xml @@ -67,12 +67,27 @@ Boston, MA 02111-1307, USA. + + AutomaticUpdatePolicy= + + + Controls the automatic update policy. Currently "none" or "check". + "none" disables automatic updates. "check" downloads just enough metadata to check + for updates and display them in rpm-ostree status. Defaults to + "none". + + Automatic updates enablement and frequency are controlled by the + rpm-ostreed-automatic.timer unit. + See systemd.timer5 + for more information on how to control systemd timers. + + IdleExitTimeout= Controls the time in seconds of inactivity before the daemon exits. Use 0 to - disable auto-exit. + disable auto-exit. Defaults to 60. verbose (i.e. we want the diff) */ + if (!rpmostree_print_cached_update (cached_update, opt_preview, + cancellable, error)) + return FALSE; + } } else if (!opt_reboot) { diff --git a/src/app/rpmostree-dbus-helpers.c b/src/app/rpmostree-dbus-helpers.c index 877460af..4ff14f50 100644 --- a/src/app/rpmostree-dbus-helpers.c +++ b/src/app/rpmostree-dbus-helpers.c @@ -20,14 +20,18 @@ #include "config.h" +#include +#include +#include + +#include +#include + #include "rpmostree-dbus-helpers.h" #include "rpmostree-builtins.h" #include "rpmostree-libbuiltin.h" -#include "libglnx.h" -#include -#include "glib-unix.h" -#include -#include +#include "rpmostree-util.h" +#include "rpmostree-rpm-util.h" void rpmostree_cleanup_peer (GPid *peer_pid) @@ -1154,3 +1158,119 @@ rpmostree_update_deployment (RPMOSTreeOS *os_proxy, cancellable, error); } + +static void +append_to_summary (GString *summary, + const char *type, + guint n) +{ + if (n == 0) + return; + if (summary->len > 0) + g_string_append (summary, ", "); + g_string_append_printf (summary, "%u %s", n, type); +} + +/* this is used by both `status` and `upgrade --check/--preview` */ +gboolean +rpmostree_print_cached_update (GVariant *cached_update, + gboolean verbose, + GCancellable *cancellable, + GError **error) +{ + GLNX_AUTO_PREFIX_ERROR ("Retrieving cached update", error); + + g_auto(GVariantDict) dict; + g_variant_dict_init (&dict, cached_update); + + /* let's just extract 📤 all the info ahead of time */ + + const char *checksum; + if (!g_variant_dict_lookup (&dict, "checksum", "&s", &checksum)) + return glnx_throw (error, "Missing \"checksum\" key"); + + const char *version; + if (!g_variant_dict_lookup (&dict, "version", "&s", &version)) + version= NULL; + + g_autofree char *timestamp = NULL; + { guint64 t; + if (!g_variant_dict_lookup (&dict, "timestamp", "t", &t)) + t = 0; + timestamp = rpmostree_timestamp_str_from_unix_utc (t); + } + + gboolean gpg_enabled; + if (!g_variant_dict_lookup (&dict, "gpg-enabled", "b", &gpg_enabled)) + gpg_enabled = FALSE; + + g_autoptr(GVariant) signatures = + g_variant_dict_lookup_value (&dict, "signatures", G_VARIANT_TYPE ("av")); + + gboolean is_new_checksum; + g_assert (g_variant_dict_lookup (&dict, "ref-has-new-commit", "b", &is_new_checksum)); + + g_autoptr(GVariant) rpm_diff = + g_variant_dict_lookup_value (&dict, "rpm-diff", G_VARIANT_TYPE ("a{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"), + strlen ("GPGSignature")); + + if (is_new_checksum) + { + rpmostree_print_timestamp_version (version, timestamp, max_key_len); + rpmostree_print_kv ("Commit", max_key_len, checksum); + if (gpg_enabled) + rpmostree_print_gpg_info (signatures, verbose, max_key_len); + } + + if (rpm_diff) + { + g_auto(GVariantDict) rpm_diff_dict; + g_variant_dict_init (&rpm_diff_dict, rpm_diff); + + g_autoptr(GVariant) upgraded = + _rpmostree_vardict_lookup_value_required (&rpm_diff_dict, "upgraded", + G_VARIANT_TYPE ("a(us(ss)(ss))"), error); + if (!upgraded) + return FALSE; + + g_autoptr(GVariant) downgraded = + _rpmostree_vardict_lookup_value_required (&rpm_diff_dict, "downgraded", + G_VARIANT_TYPE ("a(us(ss)(ss))"), error); + if (!downgraded) + return FALSE; + + g_autoptr(GVariant) removed = + _rpmostree_vardict_lookup_value_required (&rpm_diff_dict, "removed", + G_VARIANT_TYPE ("a(usss)"), error); + if (!removed) + return FALSE; + + g_autoptr(GVariant) added = + _rpmostree_vardict_lookup_value_required (&rpm_diff_dict, "added", + G_VARIANT_TYPE ("a(usss)"), error); + if (!added) + return FALSE; + + if (verbose) + rpmostree_variant_diff_print_formatted (max_key_len, + upgraded, downgraded, removed, added); + else + { + g_autoptr(GString) diff_summary = g_string_new (NULL); + append_to_summary (diff_summary, "upgraded", g_variant_n_children (upgraded)); + append_to_summary (diff_summary, "downgraded", g_variant_n_children (downgraded)); + append_to_summary (diff_summary, "removed", g_variant_n_children (removed)); + append_to_summary (diff_summary, "added", g_variant_n_children (added)); + rpmostree_print_kv ("Diff", max_key_len, diff_summary->str); + } + } + + return TRUE; +} diff --git a/src/app/rpmostree-dbus-helpers.h b/src/app/rpmostree-dbus-helpers.h index 339190b2..b4df6eb1 100644 --- a/src/app/rpmostree-dbus-helpers.h +++ b/src/app/rpmostree-dbus-helpers.h @@ -122,3 +122,9 @@ rpmostree_update_deployment (RPMOSTreeOS *os_proxy, char **out_transaction_address, GCancellable *cancellable, GError **error); + +gboolean +rpmostree_print_cached_update (GVariant *cached_update, + gboolean verbose, + GCancellable *cancellable, + GError **error); diff --git a/src/daemon/org.projectatomic.rpmostree1.xml b/src/daemon/org.projectatomic.rpmostree1.xml index 95fe7ead..853ed5dd 100644 --- a/src/daemon/org.projectatomic.rpmostree1.xml +++ b/src/daemon/org.projectatomic.rpmostree1.xml @@ -50,6 +50,9 @@ + + + @@ -77,12 +80,32 @@ 'origin' (type 's') 'signatures' (type 'av') 'gpg-enabled' (type 'b') + 'ref-has-new-commit' (type 'b') + TRUE if 'checksum' refers to a new commit we're not booted in. + 'rpm-diff' (type 'a{sv}') + 'upgraded' (type 'a(us(ss)(ss))') + 'downgraded' (type 'a(us(ss)(ss))') + 'removed' (type 'a(usss)') + 'added' (type 'a(usss)') --> - - + + + + + + + diff --git a/src/daemon/rpm-ostreed-automatic.service.in b/src/daemon/rpm-ostreed-automatic.service.in new file mode 100644 index 00000000..b8a274f1 --- /dev/null +++ b/src/daemon/rpm-ostreed-automatic.service.in @@ -0,0 +1,8 @@ +[Unit] +Description=RPM-OSTree Automatic Update +Documentation=man:rpm-ostree(1) man:rpm-ostreed.conf(5) +ConditionPathExists=/run/ostree-booted + +[Service] +Type=oneshot +ExecStart=@bindir@/rpm-ostree upgrade --automatic diff --git a/src/daemon/rpm-ostreed-automatic.timer b/src/daemon/rpm-ostreed-automatic.timer new file mode 100644 index 00000000..a15939fd --- /dev/null +++ b/src/daemon/rpm-ostreed-automatic.timer @@ -0,0 +1,11 @@ +[Unit] +Description=RPM-OSTree Automatic Update Trigger +Documentation=man:rpm-ostree(1) man:rpm-ostreed.conf(5) +ConditionPathExists=/run/ostree-booted + +[Timer] +OnBootSec=1h +OnUnitInactiveSec=1d + +[Install] +WantedBy=timers.target diff --git a/src/daemon/rpm-ostreed.conf b/src/daemon/rpm-ostreed.conf index 6fd39f59..0b00e4b4 100644 --- a/src/daemon/rpm-ostreed.conf +++ b/src/daemon/rpm-ostreed.conf @@ -3,4 +3,5 @@ # For option meanings, see rpm-ostreed.conf(5). [Daemon] +#AutomaticUpdatePolicy=none #IdleExitTimeout=60 diff --git a/src/daemon/rpmostreed-daemon.c b/src/daemon/rpmostreed-daemon.c index f3012ba0..f1ef5685 100644 --- a/src/daemon/rpmostreed-daemon.c +++ b/src/daemon/rpmostreed-daemon.c @@ -22,6 +22,7 @@ #include "rpmostreed-sysroot.h" #include "rpmostreed-types.h" #include "rpmostreed-utils.h" +#include "rpmostree-util.h" #include #include @@ -61,8 +62,9 @@ struct _RpmostreedDaemon { RpmostreedSysroot *sysroot; gchar *sysroot_path; - /* we only have one setting for now, so let's just keep it in the main struct */ + /* we only have two settings for now, so let's just keep it in the main struct */ guint idle_exit_timeout; + RpmostreedAutomaticUpdatePolicy auto_update_policy; GDBusConnection *connection; GDBusObjectManagerServer *object_manager; @@ -312,6 +314,17 @@ maybe_load_config_keyfile (GKeyFile **out_keyfile, return TRUE; } +static char* +get_config_str (GKeyFile *keyfile, + const char *key, + const char *default_val) +{ + g_autofree char *val = NULL; + if (keyfile) + val = g_key_file_get_string (keyfile, DAEMON_CONFIG_GROUP, key, NULL); + return g_steal_pointer (&val) ?: g_strdup (default_val); +} + static guint64 get_config_uint64 (GKeyFile *keyfile, const char *key, @@ -330,6 +343,20 @@ get_config_uint64 (GKeyFile *keyfile, return default_val; } +RpmostreedAutomaticUpdatePolicy +rpmostreed_get_automatic_update_policy (RpmostreedDaemon *self) +{ + return self->auto_update_policy; +} + +/* in-place version of g_ascii_strdown */ +static inline void +ascii_strdown_inplace (char *str) +{ + for (char *c = str; *c; c++) + *c = g_ascii_tolower (*c); +} + gboolean rpmostreed_daemon_reload_config (RpmostreedDaemon *self, gboolean *out_changed, @@ -343,12 +370,32 @@ rpmostreed_daemon_reload_config (RpmostreedDaemon *self, * follow-up requests are more responsive */ guint64 idle_exit_timeout = get_config_uint64 (config, "IdleExitTimeout", 60); + /* default to off for now; we will change it to "check" in a later release */ + RpmostreedAutomaticUpdatePolicy auto_update_policy = + RPMOSTREED_AUTOMATIC_UPDATE_POLICY_NONE; + + g_autofree char *auto_update_policy_str = + get_config_str (config, "AutomaticUpdatePolicy", NULL); + if (auto_update_policy_str) + { + ascii_strdown_inplace (auto_update_policy_str); + if (!rpmostree_str_to_auto_update_policy (auto_update_policy_str, + &auto_update_policy, error)) + return FALSE; + } + /* don't update changed for this; it's contained to RpmostreedDaemon so no other objects * need to be reloaded if it changes */ self->idle_exit_timeout = idle_exit_timeout; + gboolean changed = FALSE; + + changed = changed || (self->auto_update_policy != auto_update_policy); + + self->auto_update_policy = auto_update_policy; + if (out_changed) - *out_changed = FALSE; + *out_changed = changed; return TRUE; } diff --git a/src/daemon/rpmostreed-daemon.h b/src/daemon/rpmostreed-daemon.h index e93b74a7..55d6448f 100644 --- a/src/daemon/rpmostreed-daemon.h +++ b/src/daemon/rpmostreed-daemon.h @@ -19,6 +19,7 @@ #pragma once #include "rpmostreed-types.h" +#include "rpmostree-util.h" #define RPMOSTREED_TYPE_DAEMON (rpmostreed_daemon_get_type ()) #define RPMOSTREED_DAEMON(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), RPMOSTREED_TYPE_DAEMON, RpmostreedDaemon)) @@ -50,3 +51,6 @@ void rpmostreed_daemon_unpublish (RpmostreedDaemon *self, gboolean rpmostreed_daemon_reload_config (RpmostreedDaemon *self, gboolean *out_changed, GError **error); + +RpmostreedAutomaticUpdatePolicy +rpmostreed_get_automatic_update_policy (RpmostreedDaemon *self); diff --git a/src/daemon/rpmostreed-deployment-utils.c b/src/daemon/rpmostreed-deployment-utils.c index 2ce04b94..c3042012 100644 --- a/src/daemon/rpmostreed-deployment-utils.c +++ b/src/daemon/rpmostreed-deployment-utils.c @@ -18,15 +18,19 @@ #include "config.h" +#include +#include + #include "rpmostreed-deployment-utils.h" #include "rpmostree-origin.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" -#include - /* 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. @@ -187,7 +191,7 @@ variant_add_metadata_attribute (GVariantDict *dict, static void variant_add_commit_details (GVariantDict *dict, - const char *prefix, + const char *prefix, GVariant *commit) { g_autoptr(GVariant) metadata = NULL; @@ -437,3 +441,454 @@ rpmostreed_commit_generate_cached_details_variant (OstreeDeployment *deployment, 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 void +rpm_diff_add_base_db_diff (RpmDiff *diff, + /* element-type RpmOstreePackage */ + GPtrArray *removed, + GPtrArray *added, + GPtrArray *modified_old, + GPtrArray *modified_new) +{ + g_assert_cmpuint (modified_old->len, ==, modified_new->len); + + RpmOstreePkgTypes type = RPM_OSTREE_PKG_TYPE_BASE; + 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)); + } +} + +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 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 ("a(us(ss)(ss))", diff->upgraded)); + g_variant_dict_insert_value (&dict, "downgraded", + array_to_variant_new ("a(us(ss)(ss))", diff->downgraded)); + g_variant_dict_insert_value (&dict, "removed", + array_to_variant_new ("a(usss)", diff->removed)); + g_variant_dict_insert_value (&dict, "added", + array_to_variant_new ("a(usss)", diff->added)); + return g_variant_dict_end (&dict); +} + +static DnfPackage* +find_newer_package (DnfSack *sack, + 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)); + 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); + g_autoptr(GPtrArray) new_pkgs = hy_query_run (query); + if (new_pkgs->len == 0) + return NULL; /* canonicalize to NULL */ + g_ptr_array_sort (new_pkgs, (GCompareFunc)rpmostree_pkg_array_compare); + return g_object_ref (new_pkgs->pdata[new_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 (OstreeSysroot *sysroot, + /* these are just to avoid refetching them */ + OstreeRepo *repo, + OstreeDeployment *deployment, + const char *base_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; + const char *layered_checksum = ostree_deployment_get_csum (deployment); + 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_newer_package (sack, 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; +} + +static gboolean +get_cached_rpmmd_sack (OstreeSysroot *sysroot, + OstreeRepo *repo, + OstreeDeployment *deployment, + DnfSack **out_sack, + GError **error) +{ + /* we don't need the full force of the core ctx here; we just want a DnfContext so that it + * can load the repos and deal with releasever for us */ + g_autoptr(DnfContext) ctx = dnf_context_new (); + + /* We have to point to the same source root for releasever to hit the right cache: an + * interesting point here is that if there's a newer $releasever pending (i.e. 'deploy' + * auto update policy), we'll still be using the previous releasever -- this is OK though, + * we should be special-casing these rebases later re. how to display them; at least + * status already shows endoflife. See also deploy_transaction_execute(). */ + g_autofree char *deployment_root = rpmostree_get_deployment_root (sysroot, deployment); + dnf_context_set_source_root (ctx, deployment_root); + g_autofree char *reposdir = g_build_filename (deployment_root, "etc/yum.repos.d", NULL); + dnf_context_set_repo_dir (ctx, reposdir); + dnf_context_set_cache_dir (ctx, RPMOSTREE_CORE_CACHEDIR RPMOSTREE_DIR_CACHE_REPOMD); + dnf_context_set_solv_dir (ctx, RPMOSTREE_CORE_CACHEDIR RPMOSTREE_DIR_CACHE_SOLV); + + if (!dnf_context_setup (ctx, NULL, error)) + return FALSE; + + /* add the repos but strictly from cache; we should have already *just* checked & + * refreshed metadata as part of the DeployTransaction; but we gracefully handle bad cache + * too (e.g. if we start using the new dnf_context_clean_cache() on rebases?) */ + GPtrArray *repos = dnf_context_get_repos (ctx); + + /* need a new DnfSackAddFlags flag for dnf_sack_add_repos to say "don't fallback to + * updating if cache invalid/absent"; for now, just do it ourselves */ + GPtrArray *cached_enabled_repos = g_ptr_array_new (); + for (guint i = 0; i < repos->len; i++) + { + DnfRepo *repo = repos->pdata[i]; + if ((dnf_repo_get_enabled (repo) & DNF_REPO_ENABLED_PACKAGES) == 0) + continue; + + /* TODO: We need to expand libdnf here to somehow do a dnf_repo_check() without it + * triggering a download if there's no cache at all. Here, we just physically check + * for the location. */ + const char *location = dnf_repo_get_location (repo); + if (!glnx_fstatat_allow_noent (AT_FDCWD, location, NULL, 0, error)) + return FALSE; + if (errno == ENOENT) + continue; + + g_autoptr(GError) local_error = NULL; + g_autoptr(DnfState) state = dnf_state_new (); + if (!dnf_repo_check (repo, G_MAXUINT, state, &local_error)) + sd_journal_print (LOG_WARNING, "Couldn't load cache for repo %s: %s", + dnf_repo_get_id (repo), local_error->message); + else + g_ptr_array_add (cached_enabled_repos, repo); + } + + g_autoptr(DnfSack) sack = NULL; + if (cached_enabled_repos->len > 0) + { + /* Set up our own sack and point it to the solv cache. TODO: We could've used the sack + * from dnf_context_setup_sack(), but we need to extend libdnf to specify flags like + * UPDATEINFO beforehand. Otherwise we have to add_repos() twice which almost double + * startup time. */ + sack = dnf_sack_new (); + dnf_sack_set_cachedir (sack, RPMOSTREE_CORE_CACHEDIR RPMOSTREE_DIR_CACHE_SOLV); + if (!dnf_sack_setup (sack, DNF_SACK_SETUP_FLAG_MAKE_CACHE_DIR, error)) + return FALSE; + + /* we still use add_repos rather than add_repo separately above because it does nice + * things like process excludes */ + g_autoptr(DnfState) state = dnf_state_new (); + if (!dnf_sack_add_repos (sack, cached_enabled_repos, G_MAXUINT, + DNF_SACK_ADD_FLAG_UPDATEINFO, state, error)) + return FALSE; + } + + *out_sack = g_steal_pointer (&sack); + return TRUE; +} + +/* 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. */ +gboolean +rpmostreed_update_generate_variant (OstreeSysroot *sysroot, + OstreeDeployment *deployment, + OstreeRepo *repo, + GVariant **out_update, + GError **error) +{ + /* We try to minimize I/O in this function. We're in the daemon startup path, and thus + * directly contribute to lag from a cold `rpm-ostree status`. Anyway, as a principle we + * shouldn't do long-running operations outside of transactions. */ + + g_autoptr(RpmOstreeOrigin) origin = rpmostree_origin_parse_deployment (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 jigdo-based origins yet */ + if (refspectype != RPMOSTREE_REFSPEC_TYPE_OSTREE) + { + *out_update = NULL; + return TRUE; /* NB: early return */ + } + + /* 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 */ + + g_autofree char *new_checksum = NULL; + if (!ostree_repo_resolve_rev_ext (repo, refspec, TRUE, 0, &new_checksum, error)) + return FALSE; + + const char *current_checksum = ostree_deployment_get_csum (deployment); + gboolean is_layered; + g_autofree char *current_checksum_owned = NULL; + if (!rpmostree_deployment_get_layered_info (repo, deployment, &is_layered, + ¤t_checksum_owned, NULL, NULL, NULL, + error)) + return FALSE; + if (is_layered) + current_checksum = current_checksum_owned; + + /* 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_checksum) + new_checksum = g_strdup (current_checksum); + else + is_new_checksum = !g_str_equal (new_checksum, current_checksum); + + g_autoptr(GVariant) commit = NULL; + if (!ostree_repo_load_commit (repo, new_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 (deployment, repo, refspec, + new_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 this later for advisories, so just keep it around */ + g_autoptr(GPtrArray) ostree_modified_new = NULL; + if (is_new_checksum) + { + g_autoptr(GPtrArray) removed = NULL; + g_autoptr(GPtrArray) added = NULL; + g_autoptr(GPtrArray) modified_old = NULL; + + /* Note we allow_noent here; 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, current_checksum, new_checksum, flags, &removed, + &added, &modified_old, &ostree_modified_new, NULL, error)) + return FALSE; + + /* check if allow_noent kicked in */ + if (removed) + rpm_diff_add_base_db_diff (&rpm_diff, removed, added, + modified_old, ostree_modified_new); + } + + /* now we look at the rpm-md side */ + + /* first we try to set up a sack (NULL --> no cache available) */ + g_autoptr(DnfSack) sack = NULL; + if (!get_cached_rpmmd_sack (sysroot, repo, deployment, &sack, error)) + return FALSE; + + g_autoptr(GPtrArray) rpmmd_modified_new = NULL; + + 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_layered && g_hash_table_size (layered_pkgs) > 0) + { + if (!rpmmd_diff (sysroot, repo, deployment, current_checksum, sack, &rpm_diff, + &rpmmd_modified_new, error)) + return FALSE; + } + + g_variant_dict_insert (&dict, "rpm-diff", "@a{sv}", rpm_diff_variant_new (&rpm_diff)); + + /* 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)); + else + *out_update = NULL; + + return TRUE; +} diff --git a/src/daemon/rpmostreed-deployment-utils.h b/src/daemon/rpmostreed-deployment-utils.h index d50e8f2a..389ece88 100644 --- a/src/daemon/rpmostreed-deployment-utils.h +++ b/src/daemon/rpmostreed-deployment-utils.h @@ -44,3 +44,9 @@ GVariant * rpmostreed_commit_generate_cached_details_variant (OstreeDeploym OstreeRepo *repo, const gchar *refspec, GError **error); +gboolean +rpmostreed_update_generate_variant (OstreeSysroot *sysroot, + OstreeDeployment *deployment, + OstreeRepo *repo, + GVariant **out_update, + GError **error); diff --git a/src/daemon/rpmostreed-os.c b/src/daemon/rpmostreed-os.c index 86360890..f32d3df4 100644 --- a/src/daemon/rpmostreed-os.c +++ b/src/daemon/rpmostreed-os.c @@ -20,6 +20,7 @@ #include "ostree.h" #include +#include #include "rpmostreed-sysroot.h" #include "rpmostreed-daemon.h" @@ -119,7 +120,9 @@ os_authorize_method (GDBusInterfaceSkeleton *interface, { g_ptr_array_add (actions, "org.projectatomic.rpmostree1.deploy"); } - else if (g_strcmp0 (method_name, "Upgrade") == 0) + /* unite these for now; it could make sense at least to make "check" its own action */ + else if (g_strcmp0 (method_name, "Upgrade") == 0 || + g_strcmp0 (method_name, "AutomaticUpdateTrigger") == 0) { g_ptr_array_add (actions, "org.projectatomic.rpmostree1.upgrade"); } @@ -713,6 +716,20 @@ start_deployment_txn (GDBusMethodInvocation *invocation, cancellable, error); } +static gboolean +refresh_cached_update (RpmostreedOS*, GError **error); + +static void +on_auto_update_done (RpmostreedTransaction *transaction, RpmostreedOS *self) +{ + g_autoptr(GError) local_error = NULL; + if (!refresh_cached_update (self, &local_error)) + { + sd_journal_print (LOG_WARNING, "Failed to refresh CachedUpdate property: %s", + local_error->message); + } +} + typedef void (*InvocationCompleter)(RPMOSTreeOS*, GDBusMethodInvocation*, GUnixFDList*, @@ -772,8 +789,15 @@ os_merge_or_start_deployment_txn (RPMOSTreeOS *interface, fd_list, &local_error); if (transaction) - rpmostreed_transaction_monitor_add (self->transaction_monitor, - transaction); + rpmostreed_transaction_monitor_add (self->transaction_monitor, transaction); + + /* For the AutomaticUpdateTrigger "check" and "download" cases, we want to make sure + * we refresh CachedUpdate after; "deploy" will do this through sysroot_changed */ + const char *method_name = g_dbus_method_invocation_get_method_name (invocation); + if (g_str_equal (method_name, "AutomaticUpdateTrigger") && + (default_flags & (RPMOSTREE_TRANSACTION_DEPLOY_FLAG_DOWNLOAD_ONLY | + RPMOSTREE_TRANSACTION_DEPLOY_FLAG_DOWNLOAD_METADATA_ONLY))) + g_signal_connect (transaction, "closed", G_CALLBACK (on_auto_update_done), self); } if (transaction) @@ -892,6 +916,75 @@ os_handle_update_deployment (RPMOSTreeOS *interface, rpmostree_os_complete_update_deployment); } +/* compat shim for call completer */ +static void automatic_update_trigger_completer (RPMOSTreeOS *os, + GDBusMethodInvocation *invocation, + GUnixFDList *dummy, + const gchar *address) +{ /* enabled */ + rpmostree_os_complete_automatic_update_trigger (os, invocation, TRUE, address); +} + + +/* we make this a separate method to keep the D-Bus API clean, but the actual + * implementation is done by our dear friend deploy_transaction_execute(). ❤️ + */ + +static gboolean +os_handle_automatic_update_trigger (RPMOSTreeOS *interface, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + g_auto(GVariantDict) dict; + g_variant_dict_init (&dict, arg_options); + const char *mode = vardict_lookup_ptr (&dict, "mode", "&s") ?: "auto"; + g_autoptr(GError) local_error = NULL; + GError **error = &local_error; + + RpmostreedAutomaticUpdatePolicy autoupdate_policy; + if (g_str_equal (mode, "auto")) + autoupdate_policy = rpmostreed_get_automatic_update_policy (rpmostreed_daemon_get ()); + else + { + if (!rpmostree_str_to_auto_update_policy (mode, &autoupdate_policy, error)) + { + g_dbus_method_invocation_take_error (invocation, g_steal_pointer (&local_error)); + return TRUE; + } + } + + /* Now we translate policy into flags the deploy transaction understands. But avoid + * starting it at all if we're not even on. The benefit of this approach is that we keep + * the Deploy transaction simpler. */ + + RpmOstreeTransactionDeployFlags dfault = 0; + switch (autoupdate_policy) + { + case RPMOSTREED_AUTOMATIC_UPDATE_POLICY_NONE: + { + /* NB: we return the empty string here rather than NULL, because gdbus converts this + * to a gvariant, which doesn't support NULL strings */ /* enabled */ + rpmostree_os_complete_automatic_update_trigger (interface, invocation, FALSE, ""); + return TRUE; + } + case RPMOSTREED_AUTOMATIC_UPDATE_POLICY_CHECK: + dfault = RPMOSTREE_TRANSACTION_DEPLOY_FLAG_DOWNLOAD_METADATA_ONLY; + break; + default: + g_assert_not_reached (); + } + + return os_merge_or_start_deployment_txn ( + interface, + invocation, + dfault, + NULL, + NULL, + NULL, + automatic_update_trigger_completer); +} + + static gboolean os_handle_rollback (RPMOSTreeOS *interface, GDBusMethodInvocation *invocation, @@ -1596,6 +1689,39 @@ out: return TRUE; } +static gboolean +refresh_cached_update (RpmostreedOS *self, GError **error) +{ + const char *name = rpmostree_os_get_name (RPMOSTREE_OS (self)); + OstreeSysroot *sysroot = rpmostreed_sysroot_get_root (rpmostreed_sysroot_get ()); + OstreeRepo *repo = ostree_sysroot_repo (sysroot); + + /* if we're not handling the system sysroot, then let's just skip all this (e.g. `make + * check` tests) */ + const char *sysroot_path = gs_file_get_path_cached (ostree_sysroot_get_path (sysroot)); + if (!g_str_equal (sysroot_path, "/")) + return TRUE; + + /* Note here we're *not* using rpmostree_syscore_get_origin_merge_deployment(): cached + * updates are always relative to the booted/merge deployment; e.g. we still want to be + * able to show details about a pending deployment. */ + g_autoptr(OstreeDeployment) merge_deployment = + ostree_sysroot_get_merge_deployment (sysroot, name); + + g_autoptr(GVariant) cached_update = NULL; + + if (!rpmostreed_update_generate_variant (sysroot, merge_deployment, repo, + &cached_update, error)) + return FALSE; + + rpmostree_os_set_cached_update (RPMOSTREE_OS (self), cached_update); + + /* for backwards compatibility */ + gboolean has_cached_updates = (cached_update != NULL); + rpmostree_os_set_has_cached_update_rpm_diff (RPMOSTREE_OS (self), has_cached_updates); + return TRUE; +} + static gboolean rpmostreed_os_load_internals (RpmostreedOS *self, GError **error) { @@ -1603,16 +1729,12 @@ rpmostreed_os_load_internals (RpmostreedOS *self, GError **error) OstreeDeployment *booted = NULL; /* owned by sysroot */ g_autofree gchar* booted_id = NULL; - glnx_unref_object OstreeDeployment *merge_deployment = NULL; /* transfered */ - g_autoptr(GPtrArray) deployments = NULL; OstreeSysroot *ot_sysroot; OstreeRepo *ot_repo; GVariant *booted_variant = NULL; GVariant *default_variant = NULL; GVariant *rollback_variant = NULL; - g_autoptr(GVariant) cached_update = NULL; - gboolean has_cached_updates = FALSE; name = rpmostree_os_get_name (RPMOSTREE_OS (self)); g_debug ("loading %s", name); @@ -1661,25 +1783,6 @@ rpmostreed_os_load_internals (RpmostreedOS *self, GError **error) } } - merge_deployment = ostree_sysroot_get_merge_deployment (ot_sysroot, name); - if (merge_deployment) - { - g_autoptr(RpmOstreeOrigin) origin = NULL; - - /* Don't fail here for unknown origin types */ - origin = rpmostree_origin_parse_deployment (merge_deployment, NULL); - if (origin) - { - cached_update = rpmostreed_commit_generate_cached_details_variant (merge_deployment, - ot_repo, - rpmostree_origin_get_refspec (origin), - error); - if (!cached_update) - return FALSE; - has_cached_updates = cached_update != NULL; - } - } - if (!booted_variant) booted_variant = rpmostreed_deployment_generate_blank_variant (); rpmostree_os_set_booted_deployment (RPMOSTREE_OS (self), @@ -1695,10 +1798,10 @@ rpmostreed_os_load_internals (RpmostreedOS *self, GError **error) rpmostree_os_set_rollback_deployment (RPMOSTREE_OS (self), rollback_variant); - rpmostree_os_set_cached_update (RPMOSTREE_OS (self), cached_update); - rpmostree_os_set_has_cached_update_rpm_diff (RPMOSTREE_OS (self), - has_cached_updates); - g_dbus_interface_skeleton_flush(G_DBUS_INTERFACE_SKELETON (self)); + if (!refresh_cached_update (self, error)) + return FALSE; + + g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (self)); return TRUE; } @@ -1706,6 +1809,7 @@ rpmostreed_os_load_internals (RpmostreedOS *self, GError **error) static void rpmostreed_os_iface_init (RPMOSTreeOSIface *iface) { + iface->handle_automatic_update_trigger = os_handle_automatic_update_trigger; iface->handle_cleanup = os_handle_cleanup; iface->handle_get_deployment_boot_config = os_handle_get_deployment_boot_config; iface->handle_kernel_args = os_handle_kernel_args; diff --git a/src/daemon/rpmostreed-sysroot.c b/src/daemon/rpmostreed-sysroot.c index f6bee926..bb64bca8 100644 --- a/src/daemon/rpmostreed-sysroot.c +++ b/src/daemon/rpmostreed-sysroot.c @@ -409,6 +409,20 @@ handle_unregister_client (RPMOSTreeSysroot *object, return TRUE; } +static gboolean +reset_config_properties (RpmostreedSysroot *self, + GError **error) +{ + RpmostreedDaemon *daemon = rpmostreed_daemon_get (); + + RpmostreedAutomaticUpdatePolicy policy = rpmostreed_get_automatic_update_policy (daemon); + const char *policy_str = rpmostree_auto_update_policy_to_str (policy, NULL); + g_assert (policy_str); + rpmostree_sysroot_set_automatic_update_policy (RPMOSTREE_SYSROOT (self), policy_str); + + return TRUE; +} + static gboolean handle_reload_config (RPMOSTreeSysroot *object, GDBusMethodInvocation *invocation) @@ -417,7 +431,11 @@ handle_reload_config (RPMOSTreeSysroot *object, g_autoptr(GError) local_error = NULL; GError **error = &local_error; - if (!rpmostreed_daemon_reload_config (rpmostreed_daemon_get (), NULL, error)) + gboolean changed = FALSE; + if (!rpmostreed_daemon_reload_config (rpmostreed_daemon_get (), &changed, error)) + goto out; + + if (changed && !reset_config_properties (self, error)) goto out; if (!rpmostreed_sysroot_reload (self, error)) @@ -742,6 +760,9 @@ rpmostreed_sysroot_populate (RpmostreedSysroot *self, if (!sysroot_populate_deployments_unlocked (self, NULL, error)) return FALSE; + if (!reset_config_properties (self, error)) + return FALSE; + if (self->monitor == NULL) { const char *sysroot_path = gs_file_get_path_cached (ostree_sysroot_get_path (self->ot_sysroot)); diff --git a/src/daemon/rpmostreed-transaction-types.c b/src/daemon/rpmostreed-transaction-types.c index b2578f2b..2cbf6a0b 100644 --- a/src/daemon/rpmostreed-transaction-types.c +++ b/src/daemon/rpmostreed-transaction-types.c @@ -176,6 +176,9 @@ package_diff_transaction_execute (RpmostreedTransaction *transaction, GCancellable *cancellable, GError **error) { + /* XXX: we should just unify this with deploy_transaction_execute to take advantage of the + * new pkglist metadata when possible */ + PackageDiffTransaction *self = (PackageDiffTransaction *) transaction; RpmOstreeSysrootUpgraderFlags upgrader_flags = 0; @@ -598,6 +601,10 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, /* Mainly for the `install` and `override` commands */ const gboolean no_pull_base = ((self->flags & RPMOSTREE_TRANSACTION_DEPLOY_FLAG_NO_PULL_BASE) > 0); + /* Used to background check for updates; this essentially means downloading the minimum + * amount of metadata only to check if there's an upgrade */ + const gboolean download_metadata_only = + ((self->flags & RPMOSTREE_TRANSACTION_DEPLOY_FLAG_DOWNLOAD_METADATA_ONLY) > 0); RpmOstreeSysrootUpgraderFlags upgrader_flags = 0; if (self->flags & RPMOSTREE_TRANSACTION_DEPLOY_FLAG_ALLOW_DOWNGRADE) @@ -605,6 +612,11 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, if (dry_run) upgrader_flags |= RPMOSTREE_SYSROOT_UPGRADER_FLAGS_DRY_RUN; + /* DOWNLOAD_METADATA_ONLY isn't directly exposed at the D-Bus API level, so we shouldn't + * ever run into these conflicting options */ + if (download_metadata_only) + g_assert (!(no_pull_base || cache_only || download_only)); + if (cache_only) { /* practically, we could unite those two into a single flag, though it's nice to be @@ -701,8 +713,11 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, else g_string_append (txn_title, "upgrade"); + /* so users know we were probably fired by the automated timer when looking at status */ if (cache_only) g_string_append (txn_title, " (cache only)"); + else if (download_metadata_only) + g_string_append (txn_title, " (check only)"); else if (download_only) g_string_append (txn_title, " (download only)"); @@ -865,7 +880,11 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, { gboolean base_changed; - if (!rpmostree_sysroot_upgrader_pull_base (upgrader, NULL, 0, progress, + OstreeRepoPullFlags flags = OSTREE_REPO_PULL_FLAGS_NONE; + if (download_metadata_only) + flags |= OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY; + + if (!rpmostree_sysroot_upgrader_pull_base (upgrader, NULL, flags, progress, &base_changed, cancellable, error)) return FALSE; @@ -933,6 +952,48 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, changed = TRUE; } + if (download_metadata_only) + { + /* We have to short-circuit the usual path here; we already downloaded the ostree + * metadata, so now we just need to update the rpmmd data (but only if we actually + * have pkgs layered). This is still just a heuristic, since e.g. an InactiveRequest + * may in fact become active in the new base, but we don't have the full tree. */ + + /* XXX: in jigdo mode we'll want to do this unconditionally */ + if (g_hash_table_size (rpmostree_origin_get_packages (origin)) > 0) + { + /* XXX: dedupe a bit more with RefreshMd path */ + g_autoptr(RpmOstreeContext) ctx = + rpmostree_context_new_system (repo, cancellable, error); + + /* Note here that we use the cfg merge deployment for releasever: the download + * metadata only path is currently used only by the auto-update checker, and there + * we want to show updates/vulnerabilities relative to the *booted* releasever. + * Anyway, given that we don't yet do etc merges on boot, it shouldn't be too + * common for users to stay long on e.g. f26 when they have f27 already deployed + * and ready to reboot into. */ + g_autoptr(OstreeDeployment) cfg_merge_deployment = + ostree_sysroot_get_merge_deployment (sysroot, self->osname); + + g_autofree char *source_root = + rpmostree_get_deployment_root (sysroot, cfg_merge_deployment); + if (!rpmostree_context_setup (ctx, NULL, source_root, NULL, cancellable, error)) + return FALSE; + + /* we always want to force a refetch of the metadata */ + dnf_context_set_cache_age (rpmostree_context_get_dnf (ctx), 0); + + /* point libdnf to our repos dir */ + rpmostree_context_configure_from_deployment (ctx, sysroot, cfg_merge_deployment); + + if (!rpmostree_context_download_metadata (ctx, cancellable, error)) + return FALSE; + } + + /* Note early return */ + return TRUE; + } + RpmOstreeSysrootUpgraderLayeringType layering_type; gboolean layering_changed = FALSE; if (!rpmostree_sysroot_upgrader_prep_layering (upgrader, &layering_type, &layering_changed, diff --git a/src/daemon/rpmostreed-transaction-types.h b/src/daemon/rpmostreed-transaction-types.h index d752f589..eacc7de7 100644 --- a/src/daemon/rpmostreed-transaction-types.h +++ b/src/daemon/rpmostreed-transaction-types.h @@ -19,6 +19,7 @@ #pragma once #include "rpmostreed-types.h" +#include "rpmostreed-daemon.h" #include @@ -56,6 +57,7 @@ typedef enum { RPMOSTREE_TRANSACTION_DEPLOY_FLAG_NO_OVERRIDES = (1 << 6), RPMOSTREE_TRANSACTION_DEPLOY_FLAG_CACHE_ONLY = (1 << 7), RPMOSTREE_TRANSACTION_DEPLOY_FLAG_DOWNLOAD_ONLY = (1 << 8), + RPMOSTREE_TRANSACTION_DEPLOY_FLAG_DOWNLOAD_METADATA_ONLY = (1 << 9), } RpmOstreeTransactionDeployFlags; diff --git a/src/libpriv/libsd-time-util.c b/src/libpriv/libsd-time-util.c new file mode 100644 index 00000000..61fca58c --- /dev/null +++ b/src/libpriv/libsd-time-util.c @@ -0,0 +1,128 @@ +/*** + This file was originally part of systemd. + + Copyright 2010 Lennart Poettering + + systemd 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.1 of the License, or + (at your option) any later version. + + systemd 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 systemd; If not, see . +***/ + +#include +#include + +#include "libsd-time-util.h" + +static clockid_t map_clock_id(clockid_t c) { + + /* Some more exotic archs (s390, ppc, …) lack the "ALARM" flavour of the clocks. Thus, clock_gettime() will + * fail for them. Since they are essentially the same as their non-ALARM pendants (their only difference is + * when timers are set on them), let's just map them accordingly. This way, we can get the correct time even on + * those archs. */ + + switch (c) { + + case CLOCK_BOOTTIME_ALARM: + return CLOCK_BOOTTIME; + + case CLOCK_REALTIME_ALARM: + return CLOCK_REALTIME; + + default: + return c; + } +} + +static usec_t timespec_load(const struct timespec *ts) { + g_assert(ts); + + if (ts->tv_sec < 0 || ts->tv_nsec < 0) + return USEC_INFINITY; + + if ((usec_t) ts->tv_sec > (UINT64_MAX - (ts->tv_nsec / NSEC_PER_USEC)) / USEC_PER_SEC) + return USEC_INFINITY; + + return + (usec_t) ts->tv_sec * USEC_PER_SEC + + (usec_t) ts->tv_nsec / NSEC_PER_USEC; +} + +static usec_t now(clockid_t clock_id) { + struct timespec ts; + + g_assert_cmpint (clock_gettime(map_clock_id(clock_id), &ts), ==, 0); + + return timespec_load(&ts); +} + +char *libsd_format_timestamp_relative(char *buf, size_t l, usec_t t) { + const char *s; + usec_t n, d; + + if (t <= 0 || t == USEC_INFINITY) + return NULL; + + n = now(CLOCK_REALTIME); + if (n > t) { + d = n - t; + s = "ago"; + } else { + d = t - n; + s = "left"; + } + + if (d >= USEC_PER_YEAR) + snprintf(buf, l, USEC_FMT " years " USEC_FMT " months %s", + d / USEC_PER_YEAR, + (d % USEC_PER_YEAR) / USEC_PER_MONTH, s); + else if (d >= USEC_PER_MONTH) + snprintf(buf, l, USEC_FMT " months " USEC_FMT " days %s", + d / USEC_PER_MONTH, + (d % USEC_PER_MONTH) / USEC_PER_DAY, s); + else if (d >= USEC_PER_WEEK) + snprintf(buf, l, USEC_FMT " weeks " USEC_FMT " days %s", + d / USEC_PER_WEEK, + (d % USEC_PER_WEEK) / USEC_PER_DAY, s); + else if (d >= 2*USEC_PER_DAY) + snprintf(buf, l, USEC_FMT " days %s", d / USEC_PER_DAY, s); + else if (d >= 25*USEC_PER_HOUR) + snprintf(buf, l, "1 day " USEC_FMT "h %s", + (d - USEC_PER_DAY) / USEC_PER_HOUR, s); + else if (d >= 6*USEC_PER_HOUR) + snprintf(buf, l, USEC_FMT "h %s", + d / USEC_PER_HOUR, s); + else if (d >= USEC_PER_HOUR) + snprintf(buf, l, USEC_FMT "h " USEC_FMT "min %s", + d / USEC_PER_HOUR, + (d % USEC_PER_HOUR) / USEC_PER_MINUTE, s); + else if (d >= 5*USEC_PER_MINUTE) + snprintf(buf, l, USEC_FMT "min %s", + d / USEC_PER_MINUTE, s); + else if (d >= USEC_PER_MINUTE) + snprintf(buf, l, USEC_FMT "min " USEC_FMT "s %s", + d / USEC_PER_MINUTE, + (d % USEC_PER_MINUTE) / USEC_PER_SEC, s); + else if (d >= USEC_PER_SEC) + snprintf(buf, l, USEC_FMT "s %s", + d / USEC_PER_SEC, s); + else if (d >= USEC_PER_MSEC) + snprintf(buf, l, USEC_FMT "ms %s", + d / USEC_PER_MSEC, s); + else if (d > 0) + snprintf(buf, l, USEC_FMT"us %s", + d, s); + else + snprintf(buf, l, "now"); + + buf[l-1] = 0; + return buf; +} diff --git a/src/libpriv/libsd-time-util.h b/src/libpriv/libsd-time-util.h new file mode 100644 index 00000000..fe109576 --- /dev/null +++ b/src/libpriv/libsd-time-util.h @@ -0,0 +1,67 @@ +#pragma once + +/*** + This file was originally part of systemd. + + Copyright 2010 Lennart Poettering + + systemd 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.1 of the License, or + (at your option) any later version. + + systemd 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 systemd; If not, see . +***/ + +#include +#include +#include +#include +#include +#include + +typedef uint64_t usec_t; +typedef uint64_t nsec_t; + +#define PRI_NSEC PRIu64 +#define PRI_USEC PRIu64 +#define NSEC_FMT "%" PRI_NSEC +#define USEC_FMT "%" PRI_USEC + +#define USEC_INFINITY ((usec_t) -1) +#define NSEC_INFINITY ((nsec_t) -1) + +#define MSEC_PER_SEC 1000ULL +#define USEC_PER_SEC ((usec_t) 1000000ULL) +#define USEC_PER_MSEC ((usec_t) 1000ULL) +#define NSEC_PER_SEC ((nsec_t) 1000000000ULL) +#define NSEC_PER_MSEC ((nsec_t) 1000000ULL) +#define NSEC_PER_USEC ((nsec_t) 1000ULL) + +#define USEC_PER_MINUTE ((usec_t) (60ULL*USEC_PER_SEC)) +#define NSEC_PER_MINUTE ((nsec_t) (60ULL*NSEC_PER_SEC)) +#define USEC_PER_HOUR ((usec_t) (60ULL*USEC_PER_MINUTE)) +#define NSEC_PER_HOUR ((nsec_t) (60ULL*NSEC_PER_MINUTE)) +#define USEC_PER_DAY ((usec_t) (24ULL*USEC_PER_HOUR)) +#define NSEC_PER_DAY ((nsec_t) (24ULL*NSEC_PER_HOUR)) +#define USEC_PER_WEEK ((usec_t) (7ULL*USEC_PER_DAY)) +#define NSEC_PER_WEEK ((nsec_t) (7ULL*NSEC_PER_DAY)) +#define USEC_PER_MONTH ((usec_t) (2629800ULL*USEC_PER_SEC)) +#define NSEC_PER_MONTH ((nsec_t) (2629800ULL*NSEC_PER_SEC)) +#define USEC_PER_YEAR ((usec_t) (31557600ULL*USEC_PER_SEC)) +#define NSEC_PER_YEAR ((nsec_t) (31557600ULL*NSEC_PER_SEC)) + +/* We assume a maximum timezone length of 6. TZNAME_MAX is not defined on Linux, but glibc internally initializes this + * to 6. Let's rely on that. */ +#define FORMAT_TIMESTAMP_MAX (3+1+10+1+8+1+6+1+6+1) +#define FORMAT_TIMESTAMP_WIDTH 28 /* when outputting, assume this width */ +#define FORMAT_TIMESTAMP_RELATIVE_MAX 256 +#define FORMAT_TIMESPAN_MAX 64 + +char *libsd_format_timestamp_relative(char *buf, size_t l, usec_t t); diff --git a/src/libpriv/rpmostree-core.c b/src/libpriv/rpmostree-core.c index 3a87ca27..577954fd 100644 --- a/src/libpriv/rpmostree-core.c +++ b/src/libpriv/rpmostree-core.c @@ -46,10 +46,6 @@ #define RPMOSTREE_MESSAGE_PKG_REPOS SD_ID128_MAKE(0e,ea,67,9b,bf,a3,4d,43,80,2d,ec,99,b2,74,eb,e7) #define RPMOSTREE_MESSAGE_PKG_IMPORT SD_ID128_MAKE(df,8b,b5,4f,04,fa,47,08,ac,16,11,1b,bf,4b,a3,52) -#define RPMOSTREE_DIR_CACHE_REPOMD "repomd" -#define RPMOSTREE_DIR_CACHE_SOLV "solv" -#define RPMOSTREE_DIR_LOCK "lock" - static OstreeRepo * get_pkgcache_repo (RpmOstreeContext *self); /* Given a string, look for ostree:// or rojig:// prefix and diff --git a/src/libpriv/rpmostree-core.h b/src/libpriv/rpmostree-core.h index 86f4eb2e..19e88f11 100644 --- a/src/libpriv/rpmostree-core.h +++ b/src/libpriv/rpmostree-core.h @@ -27,6 +27,10 @@ #include "libglnx.h" #define RPMOSTREE_CORE_CACHEDIR "/var/cache/rpm-ostree/" +#define RPMOSTREE_DIR_CACHE_REPOMD "repomd" +#define RPMOSTREE_DIR_CACHE_SOLV "solv" +#define RPMOSTREE_DIR_LOCK "lock" + /* See http://lists.rpm.org/pipermail/rpm-maint/2017-October/006681.html */ #define RPMOSTREE_RPMDB_LOCATION "usr/share/rpm" #define RPMOSTREE_SYSIMAGE_DIR "usr/lib/sysimage" diff --git a/src/libpriv/rpmostree-rpm-util.c b/src/libpriv/rpmostree-rpm-util.c index 6d62f27a..885114d1 100644 --- a/src/libpriv/rpmostree-rpm-util.c +++ b/src/libpriv/rpmostree-rpm-util.c @@ -143,7 +143,7 @@ static char * pkg_nevra_strdup (Header h1) { return rpmostree_header_custom_nevra_strdup (h1, PKG_NEVRA_FLAGS_NAME | - PKG_NEVRA_FLAGS_EPOCH_VERSION_RELEASE | + PKG_NEVRA_FLAGS_EVR | PKG_NEVRA_FLAGS_ARCH); } @@ -893,9 +893,9 @@ rpmostree_get_refts_for_commit (OstreeRepo *repo, return TRUE; } -static gint -pkg_array_compare (DnfPackage **p_pkg1, - DnfPackage **p_pkg2) +gint +rpmostree_pkg_array_compare (DnfPackage **p_pkg1, + DnfPackage **p_pkg2) { return dnf_package_cmp (*p_pkg1, *p_pkg2); } @@ -915,7 +915,7 @@ rpmostree_sighandler_reset_cleanup (RpmSighandlerResetCleanup *cleanup) static void print_pkglist (GPtrArray *pkglist) { - g_ptr_array_sort (pkglist, (GCompareFunc) pkg_array_compare); + g_ptr_array_sort (pkglist, (GCompareFunc) rpmostree_pkg_array_compare); for (guint i = 0; i < pkglist->len; i++) { @@ -1124,7 +1124,7 @@ GPtrArray* rpmostree_sack_get_sorted_packages (DnfSack *sack) { g_autoptr(GPtrArray) pkglist = rpmostree_sack_get_packages (sack); - g_ptr_array_sort (pkglist, (GCompareFunc)pkg_array_compare); + g_ptr_array_sort (pkglist, (GCompareFunc)rpmostree_pkg_array_compare); return g_steal_pointer (&pkglist); } diff --git a/src/libpriv/rpmostree-rpm-util.h b/src/libpriv/rpmostree-rpm-util.h index af81fbfb..04c4d463 100644 --- a/src/libpriv/rpmostree-rpm-util.h +++ b/src/libpriv/rpmostree-rpm-util.h @@ -114,6 +114,10 @@ rpmostree_get_refts_for_commit (OstreeRepo *repo, GCancellable *cancellable, GError **error); +gint +rpmostree_pkg_array_compare (DnfPackage **p_pkg1, + DnfPackage **p_pkg2); + void rpmostree_print_transaction (DnfContext *context); diff --git a/src/libpriv/rpmostree-util.c b/src/libpriv/rpmostree-util.c index 5b6c39bf..22bf9d9a 100644 --- a/src/libpriv/rpmostree-util.c +++ b/src/libpriv/rpmostree-util.c @@ -30,7 +30,7 @@ #include "rpmostree-util.h" #include "rpmostree-origin.h" #include "rpmostree-output.h" -#include "rpmostree.h" +#include "libsd-locale-util.h" #include "libglnx.h" #define RPMOSTREE_OLD_PKGCACHE_DIR "extensions/rpmostree/pkgcache" @@ -777,7 +777,7 @@ rpmostree_diff_print_formatted (GPtrArray *removed, { gboolean first; - g_assert (modified_old->len == modified_new->len); + g_assert_cmpuint (modified_old->len, ==, modified_new->len); first = TRUE; for (guint i = 0; i < modified_old->len; i++) @@ -842,6 +842,50 @@ rpmostree_diff_print_formatted (GPtrArray *removed, } } +static void +variant_diff_print_modified (guint max_key_len, + GVariant *modified, + const char *type) +{ + guint n = g_variant_n_children (modified); + for (guint i = 0; i < n; i++) + { + const char *name, *evr_old, *evr_new; + g_variant_get_child (modified, i, "(u&s(&ss)(&ss))", + NULL, &name, &evr_old, NULL, &evr_new, NULL); + g_print (" %*s%s %s %s -> %s\n", max_key_len, i == 0 ? type : "", i == 0 ? ":" : " ", + name, evr_old, evr_new); + } +} + +static void +variant_diff_print_singles (guint max_key_len, + GVariant *singles, + const char *type) +{ + guint n = g_variant_n_children (singles); + for (guint i = 0; i < n; i++) + { + const char *name, *evr, *arch; + g_variant_get_child (singles, i, "(u&s&s&s)", NULL, &name, &evr, &arch); + g_print (" %*s%s %s-%s.%s\n", max_key_len, i == 0 ? type : "", i == 0 ? ":" : " ", + name, evr, arch); + } +} + +void +rpmostree_variant_diff_print_formatted (guint max_key_len, + GVariant *upgraded, + GVariant *downgraded, + GVariant *removed, + GVariant *added) +{ + variant_diff_print_modified (max_key_len, upgraded, "Upgraded"); + variant_diff_print_modified (max_key_len, downgraded, "Downgraded"); + variant_diff_print_singles (max_key_len, removed, "Removed"); + variant_diff_print_singles (max_key_len, added, "Added"); +} + static int pkg_cmp_end (RpmOstreePackage *a, RpmOstreePackage *b) { @@ -957,3 +1001,33 @@ rpmostree_variant_bsearch_str (GVariant *array, *out_pos = imid; return FALSE; } + +const char* +rpmostree_auto_update_policy_to_str (RpmostreedAutomaticUpdatePolicy policy, + GError **error) +{ + switch (policy) + { + case RPMOSTREED_AUTOMATIC_UPDATE_POLICY_NONE: + return "none"; + case RPMOSTREED_AUTOMATIC_UPDATE_POLICY_CHECK: + return "check"; + default: + return glnx_null_throw (error, "Invalid policy value %u", policy); + } +} + +gboolean +rpmostree_str_to_auto_update_policy (const char *str, + RpmostreedAutomaticUpdatePolicy *out_policy, + GError **error) +{ + g_assert (str); + if (g_str_equal (str, "none") || g_str_equal (str, "off")) + *out_policy = RPMOSTREED_AUTOMATIC_UPDATE_POLICY_NONE; + else if (g_str_equal (str, "check")) + *out_policy = RPMOSTREED_AUTOMATIC_UPDATE_POLICY_CHECK; + else + return glnx_throw (error, "Invalid value for AutomaticUpdatePolicy: '%s'", str); + return TRUE; +} diff --git a/src/libpriv/rpmostree-util.h b/src/libpriv/rpmostree-util.h index 2ba379d8..e4da0eba 100644 --- a/src/libpriv/rpmostree-util.h +++ b/src/libpriv/rpmostree-util.h @@ -26,6 +26,7 @@ #include #include #include "libglnx.h" +#include "rpmostree.h" #define _N(single, plural, n) ( (n) == 1 ? (single) : (plural) ) #define _NS(n) _N("", "s", n) @@ -99,6 +100,13 @@ rpmostree_diff_print_formatted (GPtrArray *removed, GPtrArray *modified_old, GPtrArray *modified_new); +void +rpmostree_variant_diff_print_formatted (guint max_key_len, + GVariant *upgraded, + GVariant *downgraded, + GVariant *removed, + GVariant *added); + void rpmostree_diff_print (GPtrArray *removed, GPtrArray *added, @@ -191,3 +199,24 @@ gboolean rpmostree_variant_bsearch_str (GVariant *array, const char *str, int *out_pos); + +/* these are kept here for easier sharing with the client */ + +typedef enum { + RPMOSTREED_AUTOMATIC_UPDATE_POLICY_NONE, + RPMOSTREED_AUTOMATIC_UPDATE_POLICY_CHECK, +} RpmostreedAutomaticUpdatePolicy; + +const char* +rpmostree_auto_update_policy_to_str (RpmostreedAutomaticUpdatePolicy policy, + GError **error); + +gboolean +rpmostree_str_to_auto_update_policy (const char *str, + RpmostreedAutomaticUpdatePolicy *out_policy, + GError **error); + +typedef enum { + RPM_OSTREE_PKG_TYPE_BASE, + RPM_OSTREE_PKG_TYPE_LAYER, +} RpmOstreePkgTypes; diff --git a/tests/common/libtest.sh b/tests/common/libtest.sh index 20f662ad..457c52c2 100644 --- a/tests/common/libtest.sh +++ b/tests/common/libtest.sh @@ -487,20 +487,21 @@ $files EOF (cd $test_tmpdir/yumrepo/specs && rpmbuild -ba $name.spec \ + --define "_topdir $PWD" \ --define "_sourcedir $PWD" \ --define "_specdir $PWD" \ --define "_builddir $PWD/.build" \ --define "_srcrpmdir $PWD" \ --define "_rpmdir $test_tmpdir/yumrepo/packages" \ --define "_buildrootdir $PWD") - (cd yumrepo && createrepo_c --no-database .) - if test '!' -f yumrepo.repo; then - cat > yumrepo.repo.tmp << EOF + (cd $test_tmpdir/yumrepo && createrepo_c --no-database .) + if test '!' -f $test_tmpdir/yumrepo.repo; then + cat > $test_tmpdir/yumrepo.repo.tmp << EOF [test-repo] name=test-repo baseurl=file:///$PWD/yumrepo EOF - mv yumrepo.repo{.tmp,} + mv $test_tmpdir/yumrepo.repo{.tmp,} fi } diff --git a/tests/common/libvm.sh b/tests/common/libvm.sh index e80291fb..99c00f0a 100644 --- a/tests/common/libvm.sh +++ b/tests/common/libvm.sh @@ -48,7 +48,7 @@ vm_raw_rsync() { vm_rsync() { if ! test -f .vagrant/using_sshfs; then pushd ${topsrcdir} - vm_raw_rsync --exclude .git/ . $VM:/var/roothome/sync + vm_raw_rsync --delete --exclude .git/ . $VM:/var/roothome/sync popd fi } diff --git a/tests/utils/inject-pkglist.c b/tests/utils/inject-pkglist.c new file mode 100644 index 00000000..5d009796 --- /dev/null +++ b/tests/utils/inject-pkglist.c @@ -0,0 +1,99 @@ +/* +Given a ref, read its pkglist, inject it in a new commit that is for our +purposes identical to the one the ref is pointing to, then reset the ref to that +commit. Essentially, we replace the tip with a copy, except that it has the +pkglist metadata. + +This is used by tests that test features that require the new pkglist metadata +and is also really useful for debugging. +*/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "libglnx.h" +#include "rpmostree-rpm-util.h" + +static gboolean +impl (const char *repo_path, + const char *refspec, + GError **error) +{ + g_autofree char *remote = NULL; + g_autofree char *ref = NULL; + if (!ostree_parse_refspec (refspec, &remote, &ref, error)) + return FALSE; + + g_autoptr(OstreeRepo) repo = ostree_repo_open_at (AT_FDCWD, repo_path, NULL, error); + if (!repo) + return FALSE; + + g_autofree char *checksum = NULL; + if (!ostree_repo_resolve_rev (repo, refspec, FALSE, &checksum, error)) + return FALSE; + + g_autoptr(GVariant) commit = NULL; + if (!ostree_repo_load_commit (repo, checksum, &commit, NULL, error)) + return FALSE; + + g_autoptr(GVariant) meta = g_variant_get_child_value (commit, 0); + g_autoptr(GVariantDict) meta_dict = g_variant_dict_new (meta); + if (g_variant_dict_contains (meta_dict, "rpmostree.rpmdb.pkglist")) + { + g_print ("Refspec '%s' already has pkglist metadata; exiting.\n", refspec); + return TRUE; + } + + /* just an easy way to checkout the rpmdb */ + g_autoptr(RpmOstreeRefSack) rsack = + rpmostree_get_refsack_for_commit (repo, checksum, NULL, error); + if (!rsack) + return FALSE; + g_assert (rsack->tmpdir.initialized); + + g_autoptr(GVariant) pkglist = NULL; + if (!rpmostree_create_rpmdb_pkglist_variant (rsack->tmpdir.fd, ".", &pkglist, NULL, error)) + return FALSE; + + g_variant_dict_insert_value (meta_dict, "rpmostree.rpmdb.pkglist", pkglist); + g_autoptr(GVariant) new_meta = g_variant_ref_sink (g_variant_dict_end (meta_dict)); + + g_autoptr(GFile) root = NULL; + if (!ostree_repo_read_commit (repo, checksum, &root, NULL, NULL, error)) + return FALSE; + + g_autofree char *new_checksum = NULL; + g_autofree char *parent = ostree_commit_get_parent (commit); + if (!ostree_repo_write_commit (repo, parent, "", "", new_meta, OSTREE_REPO_FILE (root), + &new_checksum, NULL, error)) + return FALSE; + + if (!ostree_repo_set_ref_immediate (repo, remote, ref, new_checksum, NULL, error)) + return FALSE; + + g_print("%s => %s\n", refspec, new_checksum); + + return TRUE; +} + +int +main (int argc, char *argv[]) +{ + if (argc != 3) + errx (EXIT_FAILURE, "Usage: %s ", argv[0]); + + const char *repo_path = argv[1]; + const char *refspec = argv[2]; + + g_autoptr(GError) local_error = NULL; + if (!impl (repo_path, refspec, &local_error)) + errx (EXIT_FAILURE, "%s", local_error->message); + g_assert (local_error == NULL); + + return EXIT_SUCCESS; +} diff --git a/tests/vmcheck/overlay.sh b/tests/vmcheck/overlay.sh index adb02e82..5da1cadb 100755 --- a/tests/vmcheck/overlay.sh +++ b/tests/vmcheck/overlay.sh @@ -12,6 +12,26 @@ if test -z "${INSIDE_VM:-}"; then fi vm_rsync + + # ✀✀✀ BEGIN selinux-policy hack (part 1) for + # https://github.com/fedora-selinux/selinux-policy-contrib/pull/45 + selhack=selinux-tmp-hack + if ! vm_cmd sesearch -A -s init_t -t install_t -c dbus | grep -q allow; then + echo "Activating selinux-tmp-hack" + d=$(mktemp -d) + cat > $d/$selhack.te << 'EOF' +policy_module(selinux-tmp-hack, 1.0.0) +gen_require(` + type install_t; +') +init_dbus_chat(install_t) +EOF + make -C $d -f /usr/share/selinux/devel/Makefile $selhack.pp + vm_send /var/roothome/sync $d/$selhack.pp + rm -rf $d + fi + # ✀✀✀ END selinux-policy hack ✀✀✀ + vm_cmd env INSIDE_VM=1 /var/roothome/sync/tests/vmcheck/overlay.sh vm_reboot exit 0 @@ -54,6 +74,20 @@ INSTTREE=/var/roothome/sync/insttree rsync -rlv $INSTTREE/usr/ vmcheck/usr/ rsync -rlv $INSTTREE/etc/ vmcheck/usr/etc/ +## ✀✀✀ BEGIN selinux-policy hack (part 2) for +## https://github.com/fedora-selinux/selinux-policy-contrib/pull/45 +selhack=selinux-tmp-hack +pp=/var/roothome/sync/$selhack.pp +if [ -f $pp ]; then + seld=usr/share/selinux/packages/$selhack + mkdir -p vmcheck/$seld + cp $pp vmcheck/$seld + mkdir vmcheck/var/tmp # bwrap wrapper will mount tmpfs there + /var/roothome/sync/scripts/bwrap-script-shell.sh /ostree/repo/tmp/vmcheck \ + semodule -v -n -i /$seld/$selhack.pp +fi +## ✀✀✀ END selinux-policy hack ✀✀✀ + # ✀✀✀ BEGIN hack to get --keep-metadata if ! ostree commit --help | grep -q -e --keep-metadata; then # this is fine, rsync doesn't modify in place @@ -63,7 +97,14 @@ if ! ostree commit --help | grep -q -e --keep-metadata; then fi # ✀✀✀ END hack to get --keep-metadata ✀✀✀ +# if the commit already has pkglist metadata (i.e. the tree was composed with at +# least v2018.1), make sure it gets preserved, because it's useful for playing +# around (but note it's not a requirement for our tests) commit_opts= +if ostree show $commit --raw | grep -q rpmostree.rpmdb.pkglist; then + commit_opts="${commit_opts} --keep-metadata=rpmostree.rpmdb.pkglist" +fi + source_opt= # make this its own var since it contains spaces if [ $origin != vmcheck ]; then source_title="${origin}" @@ -82,4 +123,5 @@ fi ostree commit --parent=$commit -b vmcheck --consume --no-bindings \ --link-checkout-speedup ${commit_opts} "${source_opt}" \ --selinux-policy=vmcheck --tree=dir=vmcheck + ostree admin deploy vmcheck diff --git a/tests/vmcheck/test-autoupdate.sh b/tests/vmcheck/test-autoupdate.sh new file mode 100755 index 00000000..4d33ce76 --- /dev/null +++ b/tests/vmcheck/test-autoupdate.sh @@ -0,0 +1,186 @@ +#!/bin/bash +# +# Copyright (C) 2018 Jonathan Lebon +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +set -euo pipefail + +. ${commondir}/libtest.sh +. ${commondir}/libvm.sh + +set -x + +# first, let's make sure the timer is disabled so it doesn't mess up with our +# tests +vm_cmd systemctl disable --now rpm-ostreed-automatic.timer + +# Really testing this like a user requires a remote ostree server setup. +# Let's start by setting up the repo. +REMOTE_OSTREE=/ostree/repo/tmp/vmcheck-remote +vm_cmd mkdir -p $REMOTE_OSTREE +vm_cmd ostree init --repo=$REMOTE_OSTREE --mode=archive +vm_start_httpd ostree_server $REMOTE_OSTREE 8888 + +# We need to build up a history on the server. Rather than wasting time +# composing trees for real, we just use client package layering to create new +# trees that we then "lift" into the server before cleaning them up client-side. + +# steal a commit from the system repo and make a branch out of it +lift_commit() { + checksum=$1; shift + branch=$1; shift + vm_cmd ostree pull-local --repo=$REMOTE_OSTREE --disable-fsync \ + /ostree/repo $checksum + vm_cmd ostree --repo=$REMOTE_OSTREE refs $branch --delete + vm_cmd ostree --repo=$REMOTE_OSTREE refs $checksum --create=$branch +} + +# use a previously stolen commit to create an update on our vmcheck branch, +# complete with version string and pkglist metadata +create_update() { + branch=$1; shift + vm_cmd ostree commit --repo=$REMOTE_OSTREE -b vmcheck \ + --tree=ref=$branch --add-metadata-string=version=$branch --fsync=no + # avoid libtool wrapper here since we're running on the VM and it would try to + # cd to topsrcdir/use gcc; libs are installed anyway + vm_cmd /var/roothome/sync/.libs/inject-pkglist $REMOTE_OSTREE vmcheck +} + +# (delete ref but don't prune for easier debugging) +vm_cmd ostree refs --repo=$REMOTE_OSTREE vmcheck --delete + +# 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 +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} +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} +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 +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_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"]|index("layered-cake") >= 0' +echo "ok prep" + +# start it up again since we rebooted +vm_start_httpd ostree_server $REMOTE_OSTREE 8888 + +change_policy() { + policy=$1; shift + vm_cmd cp /usr/etc/rpm-ostreed.conf /etc + cat > tmp.sh << EOF +echo -e "[Daemon]\nAutomaticUpdatePolicy=$policy" > /etc/rpm-ostreed.conf +EOF + vm_cmdfile tmp.sh + vm_rpmostree reload +} + +# make sure that off means off +change_policy off +vm_rpmostree status | grep 'auto updates disabled' +vm_rpmostree upgrade --trigger-automatic-update-policy > out.txt +assert_file_has_content out.txt "Automatic updates are not enabled; exiting" +echo "ok disabled" + +# ok, let's test out check +change_policy check +vm_rpmostree status | grep 'auto updates enabled (check' + +# build an *older version* and check that we don't report an update +vm_build_rpm layered-cake version 2.1 release 2 +vm_rpmostree upgrade --trigger-automatic-update-policy +vm_rpmostree status -v > out.txt +assert_not_file_has_content out.txt "Available update" + +# build a *newer version* and check that we report an update +vm_build_rpm layered-cake version 2.1 release 4 +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" +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 +! grep -A999 'Available update' out.txt | grep "Version" +! grep -A999 'Available update' out.txt | grep "Timestamp" +! grep -A999 'Available update' out.txt | grep "Commit" +echo "ok check mode layered only" + +# ok now let's add ostree updates in the picture +create_update v2 +vm_rpmostree upgrade --trigger-automatic-update-policy + +# make sure we only pulled down the commit metadata +if vm_cmd ostree checkout vmcheckmote:vmcheck --subpath /usr/share/rpm; then + assert_not_reached "Was able to checkout /usr/share/rpm?" +fi + +assert_update() { + vm_assert_status_jq \ + '.["cached-update"]["origin"] == "vmcheckmote:vmcheck"' \ + '.["cached-update"]["version"] == "v2"' \ + '.["cached-update"]["ref-has-new-commit"] == true' \ + '.["cached-update"]["gpg-enabled"] == false' + + # 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' + + 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_update +echo "ok check mode ostree" + +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"]|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' +} + +# now let's upgrade and check that it matches what we expect +vm_rpmostree upgrade +assert_default_deployment_is_update +echo "ok upgrade" diff --git a/tests/vmcheck/test.sh b/tests/vmcheck/test.sh index 2317d7c6..1a2399e9 100755 --- a/tests/vmcheck/test.sh +++ b/tests/vmcheck/test.sh @@ -76,6 +76,8 @@ if vm_cmd test -f /etc/rpm-ostreed.conf; then fi vm_cmd cp -f /usr/etc/rpm-ostreed.conf /etc +vm_cmd ostree remote delete --if-exists vmcheckmote + origdir=$(pwd) echo -n '' > ${LOG} @@ -186,6 +188,7 @@ for tf in $(find . -name 'test-*.sh' | sort); do # and clean up any leftovers from our tmp osname=$(vm_get_booted_deployment_info osname) vm_cmd rm -rf /ostree/deploy/$osname/var/tmp/vmcheck + vm_cmd ostree remote delete --if-exists vmcheckmote done