From 8d58901962adf0d419ef3304d3d40772bcda658a Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 8 Nov 2023 17:19:19 -0500 Subject: [PATCH] sysroot: Stabilize deployment finalization, add API and CLI It's about time we do this; deployment finalization locking is a useful feature. An absolutely key thing here is that we've slowly been moving towards the deployments as the primary "source of truth". Specifically in bootc for example, we will GC container images not referenced by a deployment. This is then neecessary to support a "pull but don't apply automatically" model. This stabilizes the existing `ostree admin deploy --lock-finalization` CLI, and adds a new `ostree admin unlock-finalization`. We still check the old lock file path, but there's a new boolean value as part of the staged deployment data which is intended to be the source of truth in the future. At some point then we can drop the rpm-ostree lockfile handling. Closes: https://github.com/ostreedev/ostree/issues/3025 --- Makefile-man.am | 1 + apidoc/ostree-sections.txt | 2 + man/ostree-admin-unlock-finalization.xml | 80 +++++++++++++++++++ src/libostree/libostree-devel.sym | 2 + src/libostree/ostree-deployment-private.h | 1 + src/libostree/ostree-deployment.c | 15 ++++ src/libostree/ostree-deployment.h | 2 + src/libostree/ostree-sysroot-deploy.c | 71 +++++++++++++++- src/libostree/ostree-sysroot-private.h | 3 + src/libostree/ostree-sysroot.c | 2 + src/libostree/ostree-sysroot.h | 9 ++- src/ostree/ot-admin-builtin-deploy.c | 13 ++- src/ostree/ot-admin-builtin-status.c | 4 +- .../ot-admin-builtin-unlock-finalization.c | 57 +++++++++++++ src/ostree/ot-admin-builtins.h | 1 + tests/kolainst/destructive/staged-deploy.sh | 8 +- 16 files changed, 259 insertions(+), 12 deletions(-) create mode 100644 man/ostree-admin-unlock-finalization.xml create mode 100644 src/ostree/ot-admin-builtin-unlock-finalization.c diff --git a/Makefile-man.am b/Makefile-man.am index 096560b1ad..f5180d701e 100644 --- a/Makefile-man.am +++ b/Makefile-man.am @@ -30,6 +30,7 @@ ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-stateroot-init.1 ost ostree-admin-status.1 ostree-admin-set-origin.1 ostree-admin-switch.1 \ ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin-unlock.1 \ ostree-admin-pin.1 ostree-admin-post-copy.1 ostree-admin-set-default.1 \ +ostree-admin-unlock-finalization.1 \ ostree-admin.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1 \ ostree-commit.1 ostree-create-usb.1 ostree-export.1 \ ostree-config.1 ostree-diff.1 ostree-find-remotes.1 ostree-fsck.1 \ diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index 7a8966421f..3fde6e6fd9 100644 --- a/apidoc/ostree-sections.txt +++ b/apidoc/ostree-sections.txt @@ -191,6 +191,7 @@ ostree_deployment_get_origin_relpath ostree_deployment_get_unlocked ostree_deployment_is_pinned ostree_deployment_is_staged +ostree_deployment_is_finalization_locked ostree_deployment_set_index ostree_deployment_set_bootserial ostree_deployment_set_bootconfig @@ -591,6 +592,7 @@ ostree_sysroot_write_origin_file ostree_sysroot_stage_tree ostree_sysroot_stage_tree_with_options ostree_sysroot_stage_overlay_initrd +ostree_sysroot_unlock_finalization ostree_sysroot_deploy_tree ostree_sysroot_deploy_tree_with_options ostree_sysroot_get_merge_deployment diff --git a/man/ostree-admin-unlock-finalization.xml b/man/ostree-admin-unlock-finalization.xml new file mode 100644 index 0000000000..f49e098152 --- /dev/null +++ b/man/ostree-admin-unlock-finalization.xml @@ -0,0 +1,80 @@ + + + + + + + + + ostree admin unlock-finalization + OSTree + + + + Developer + Colin + Walters + walters@verbum.org + + + + + + ostree admin unlock-finalization + 1 + + + + ostree-admin-unlock-finalization + Ensure staged deployment will be queued for next boot + + + + + ostree admin unlock-finalization OPTIONS + + + + + Description + + + This command requires a staged deployment. If it has been "finalization locked", + which means it will not be queued for the next boot by default, then this command + will unlock it. If it is already finalization-unlocked, then this command will + be a no-op. + + + + + Options + + + + ="PATH" + + + Path to the system to use rather than the current one. + + + + + diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym index 02d2ac2950..5236cc9c49 100644 --- a/src/libostree/libostree-devel.sym +++ b/src/libostree/libostree-devel.sym @@ -23,6 +23,8 @@ LIBOSTREE_2023.8 { global: ostree_sysroot_update_post_copy; + ostree_deployment_is_finalization_locked; + ostree_sysroot_unlock_finalization; } LIBOSTREE_2023.4; /* Stub section for the stable release *after* this development one; don't diff --git a/src/libostree/ostree-deployment-private.h b/src/libostree/ostree-deployment-private.h index 2a28bffd92..f6766c39bd 100644 --- a/src/libostree/ostree-deployment-private.h +++ b/src/libostree/ostree-deployment-private.h @@ -51,6 +51,7 @@ struct _OstreeDeployment GKeyFile *origin; OstreeDeploymentUnlockedState unlocked; gboolean staged; + gboolean finalization_locked; char **overlay_initrds; char *overlay_initrds_id; }; diff --git a/src/libostree/ostree-deployment.c b/src/libostree/ostree-deployment.c index 1480d74656..8be2fdd507 100644 --- a/src/libostree/ostree-deployment.c +++ b/src/libostree/ostree-deployment.c @@ -461,3 +461,18 @@ ostree_deployment_is_staged (OstreeDeployment *self) { return self->staged; } + +/** + * ostree_deployment_is_finalization_locked: + * @self: Deployment + * + * Returns: `TRUE` if deployment is queued to be "finalized" at shutdown time, but requires + * additional action. + * + * Since: 2023.8 + */ +gboolean +ostree_deployment_is_finalization_locked (OstreeDeployment *self) +{ + return self->finalization_locked; +} diff --git a/src/libostree/ostree-deployment.h b/src/libostree/ostree-deployment.h index 0d4a5d7b02..0536d9810c 100644 --- a/src/libostree/ostree-deployment.h +++ b/src/libostree/ostree-deployment.h @@ -71,6 +71,8 @@ GKeyFile *ostree_deployment_get_origin (OstreeDeployment *self); _OSTREE_PUBLIC gboolean ostree_deployment_is_staged (OstreeDeployment *self); _OSTREE_PUBLIC +gboolean ostree_deployment_is_finalization_locked (OstreeDeployment *self); +_OSTREE_PUBLIC gboolean ostree_deployment_is_pinned (OstreeDeployment *self); _OSTREE_PUBLIC diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 116143c160..936df29192 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -3657,6 +3657,10 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname, g_autoptr (GVariantBuilder) builder = g_variant_builder_new ((GVariantType *)"a{sv}"); g_variant_builder_add (builder, "{sv}", "target", serialize_deployment_to_variant (deployment)); + if (opts->locked) + g_variant_builder_add (builder, "{sv}", _OSTREE_SYSROOT_STAGED_KEY_LOCKED, + g_variant_new_boolean (TRUE)); + if (merge_deployment) g_variant_builder_add (builder, "{sv}", "merge-deployment", serialize_deployment_to_variant (merge_deployment)); @@ -3706,6 +3710,54 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname, return TRUE; } +/** + * ostree_sysroot_unlock_finalization: + * @self: Sysroot + * @deployment: Deployment which must be staged and finalization locked. + * @error: Error + * + * Given the target deployment (which must be the staged deployment) this API + * will set it to a "finalization unlocked" state. + * + * Since: 2023.8 + */ +_OSTREE_PUBLIC +gboolean +ostree_sysroot_unlock_finalization (OstreeSysroot *self, OstreeDeployment *deployment, + GError **error) +{ + GCancellable *cancellable = NULL; + g_assert (ostree_deployment_is_staged (deployment)); + g_assert (ostree_deployment_is_finalization_locked (deployment)); + + /* Read the staged state from disk */ + glnx_autofd int fd = -1; + if (!glnx_openat_rdonly (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED, TRUE, &fd, error)) + return FALSE; + + g_autoptr (GBytes) contents = ot_fd_readall_or_mmap (fd, 0, error); + if (!contents) + return FALSE; + g_autoptr (GVariant) staged_deployment_data + = g_variant_new_from_bytes ((GVariantType *)"a{sv}", contents, TRUE); + g_autoptr (GVariantDict) staged_deployment_dict = g_variant_dict_new (staged_deployment_data); + + g_variant_dict_insert (staged_deployment_dict, _OSTREE_SYSROOT_STAGED_KEY_LOCKED, "b", FALSE); + g_autoptr (GVariant) new_staged_deployment_data = g_variant_dict_end (staged_deployment_dict); + + if (!glnx_file_replace_contents_at (fd, _OSTREE_SYSROOT_RUNSTATE_STAGED, + g_variant_get_data (new_staged_deployment_data), + g_variant_get_size (new_staged_deployment_data), + GLNX_FILE_REPLACE_NODATASYNC, cancellable, error)) + return FALSE; + + /* Delete the lock if there was any. */ + if (!ot_ensure_unlinked_at (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED_LOCKED, error)) + return FALSE; + + return TRUE; +} + /* Invoked at shutdown time by ostree-finalize-staged.service */ static gboolean _ostree_sysroot_finalize_staged_inner (OstreeSysroot *self, GCancellable *cancellable, @@ -3722,11 +3774,22 @@ _ostree_sysroot_finalize_staged_inner (OstreeSysroot *self, GCancellable *cancel } /* Check if finalization is locked. */ - if (!glnx_fstatat_allow_noent (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED_LOCKED, NULL, 0, error)) - return FALSE; - if (errno == 0) + gboolean locked = false; + (void)g_variant_lookup (self->staged_deployment_data, _OSTREE_SYSROOT_STAGED_KEY_LOCKED, "b", + &locked); + if (locked) + g_debug ("staged is locked via metadata"); + else + { + if (!glnx_fstatat_allow_noent (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED_LOCKED, NULL, 0, + error)) + return FALSE; + if (errno == 0) + locked = TRUE; + } + if (locked) { - ot_journal_print (LOG_INFO, "Not finalizing; found " _OSTREE_SYSROOT_RUNSTATE_STAGED_LOCKED); + ot_journal_print (LOG_INFO, "Not finalizing; deployment is locked for finalization"); return TRUE; } diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h index 8e6945b293..5be07c24ba 100644 --- a/src/libostree/ostree-sysroot-private.h +++ b/src/libostree/ostree-sysroot-private.h @@ -93,6 +93,9 @@ struct OstreeSysroot OstreeSysrootDebugFlags debug_flags; }; +/* Key in staged deployment variant for finalization locking */ +#define _OSTREE_SYSROOT_STAGED_KEY_LOCKED "locked" + #define OSTREE_SYSROOT_LOCKFILE "ostree/lock" /* We keep some transient state in /run */ #define _OSTREE_SYSROOT_RUNSTATE_STAGED "/run/ostree/staged-deployment" diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index 91b63f945a..62adc6221c 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -1106,6 +1106,8 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error) * canonical "staged_deployment" reference. */ self->staged_deployment->staged = TRUE; + (void)g_variant_dict_lookup (staged_deployment_dict, _OSTREE_SYSROOT_STAGED_KEY_LOCKED, + "b", &self->staged_deployment->finalization_locked); } } diff --git a/src/libostree/ostree-sysroot.h b/src/libostree/ostree-sysroot.h index b7ba6ac97b..4ddcd61d3b 100644 --- a/src/libostree/ostree-sysroot.h +++ b/src/libostree/ostree-sysroot.h @@ -178,7 +178,10 @@ gboolean ostree_sysroot_stage_overlay_initrd (OstreeSysroot *self, int fd, char typedef struct { - gboolean unused_bools[8]; + /* If set to true, then this deployment will be staged but "locked" and not automatically applied + * on reboot. */ + gboolean locked; + gboolean unused_bools[7]; int unused_ints[8]; char **override_kernel_argv; char **overlay_initrds; @@ -215,6 +218,10 @@ gboolean ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char OstreeDeployment **out_new_deployment, GCancellable *cancellable, GError **error); +_OSTREE_PUBLIC +gboolean ostree_sysroot_unlock_finalization (OstreeSysroot *self, OstreeDeployment *deployment, + GError **error); + _OSTREE_PUBLIC gboolean ostree_sysroot_deployment_set_mutable (OstreeSysroot *self, OstreeDeployment *deployment, gboolean is_mutable, GCancellable *cancellable, diff --git a/src/ostree/ot-admin-builtin-deploy.c b/src/ostree/ot-admin-builtin-deploy.c index c0faaab908..69a543362f 100644 --- a/src/ostree/ot-admin-builtin-deploy.c +++ b/src/ostree/ot-admin-builtin-deploy.c @@ -60,7 +60,7 @@ static GOptionEntry options[] = { "Do not apply configuration (/etc and kernel arguments) from booted deployment", NULL }, { "retain", 0, 0, G_OPTION_ARG_NONE, &opt_retain, "Do not delete previous deployments", NULL }, { "stage", 0, 0, G_OPTION_ARG_NONE, &opt_stage, "Complete deployment at OS shutdown", NULL }, - { "lock-finalization", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &opt_lock_finalization, + { "lock-finalization", 0, 0, G_OPTION_ARG_NONE, &opt_lock_finalization, "Prevent automatic deployment finalization on shutdown", NULL }, { "retain-pending", 0, 0, G_OPTION_ARG_NONE, &opt_retain_pending, "Do not delete pending deployments", NULL }, @@ -123,6 +123,10 @@ ot_admin_builtin_deploy (int argc, char **argv, OstreeCommandInvocation *invocat return FALSE; } + // Locking implies staging + if (opt_lock_finalization) + opt_stage = TRUE; + const char *refspec = argv[1]; OstreeRepo *repo = ostree_sysroot_repo (sysroot); @@ -236,6 +240,7 @@ ot_admin_builtin_deploy (int argc, char **argv, OstreeCommandInvocation *invocat g_auto (GStrv) kargs_strv = kargs ? ostree_kernel_args_to_strv (kargs) : NULL; OstreeSysrootDeployTreeOpts opts = { + .locked = opt_lock_finalization, .override_kernel_argv = kargs_strv, .overlay_initrds = overlay_initrd_chksums ? (char **)overlay_initrd_chksums->pdata : NULL, }; @@ -247,9 +252,11 @@ ot_admin_builtin_deploy (int argc, char **argv, OstreeCommandInvocation *invocat return glnx_throw (error, "--stage cannot currently be combined with --retain arguments"); if (opt_not_as_default) return glnx_throw (error, "--stage cannot currently be combined with --not-as-default"); - /* touch file *before* we stage to avoid races */ + /* For compatibility with older versions of ostree, also write this legacy file. + * This can likely be safely deleted in the middle of 2024 say. */ if (opt_lock_finalization) { + g_debug ("Writing legacy finalization lockfile"); if (!glnx_shutil_mkdir_p_at (AT_FDCWD, dirname (strdupa (_OSTREE_SYSROOT_RUNSTATE_STAGED_LOCKED)), 0755, cancellable, error)) @@ -262,7 +269,7 @@ ot_admin_builtin_deploy (int argc, char **argv, OstreeCommandInvocation *invocat _OSTREE_SYSROOT_RUNSTATE_STAGED_LOCKED); } /* use old API if we can to exercise it in CI */ - if (!overlay_initrd_chksums) + if (!(overlay_initrd_chksums || opt_lock_finalization)) { if (!ostree_sysroot_stage_tree (sysroot, opt_osname, revision, origin, merge_deployment, kargs_strv, &new_deployment, cancellable, error)) diff --git a/src/ostree/ot-admin-builtin-status.c b/src/ostree/ot-admin-builtin-status.c index 3addfd1615..f9fa19b85c 100644 --- a/src/ostree/ot-admin-builtin-status.c +++ b/src/ostree/ot-admin-builtin-status.c @@ -98,7 +98,9 @@ deployment_print_status (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploym GKeyFile *origin = ostree_deployment_get_origin (deployment); const char *deployment_status = ""; - if (ostree_deployment_is_staged (deployment)) + if (ostree_deployment_is_finalization_locked (deployment)) + deployment_status = " (finalization locked)"; + else if (ostree_deployment_is_staged (deployment)) deployment_status = " (staged)"; else if (is_pending) deployment_status = " (pending)"; diff --git a/src/ostree/ot-admin-builtin-unlock-finalization.c b/src/ostree/ot-admin-builtin-unlock-finalization.c new file mode 100644 index 0000000000..7b3b16c718 --- /dev/null +++ b/src/ostree/ot-admin-builtin-unlock-finalization.c @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * 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, see . + */ + +#include "config.h" + +#include "ostree-sysroot-private.h" + +#include "ostree.h" +#include "ot-admin-builtins.h" +#include "ot-admin-functions.h" +#include "ot-main.h" +#include "otutil.h" + +#include + +static GOptionEntry options[] = { { NULL } }; + +gboolean +ot_admin_builtin_unlock_finalization (int argc, char **argv, OstreeCommandInvocation *invocation, + GCancellable *cancellable, GError **error) +{ + g_autoptr (GOptionContext) context = g_option_context_new (""); + + g_autoptr (OstreeSysroot) sysroot = NULL; + if (!ostree_admin_option_context_parse (context, options, &argc, &argv, + OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER, invocation, &sysroot, + cancellable, error)) + return FALSE; + + OstreeDeployment *staged = ostree_sysroot_get_staged_deployment (sysroot); + if (!staged) + return glnx_throw (error, "No staged deployment"); + if (!ostree_deployment_is_finalization_locked (staged)) + { + // Make this not an error to allow + g_print ("Staged deployment is already prepared for finalization\n"); + return 0; + } + + return ostree_sysroot_unlock_finalization (sysroot, staged, error); +} diff --git a/src/ostree/ot-admin-builtins.h b/src/ostree/ot-admin-builtins.h index 1f94e414d9..c239aea87c 100644 --- a/src/ostree/ot-admin-builtins.h +++ b/src/ostree/ot-admin-builtins.h @@ -49,6 +49,7 @@ BUILTINPROTO (switch); BUILTINPROTO (upgrade); BUILTINPROTO (kargs); BUILTINPROTO (post_copy); +BUILTINPROTO (unlock_finalization); #undef BUILTINPROTO diff --git a/tests/kolainst/destructive/staged-deploy.sh b/tests/kolainst/destructive/staged-deploy.sh index ff6f8d7a8d..b9e97253c7 100755 --- a/tests/kolainst/destructive/staged-deploy.sh +++ b/tests/kolainst/destructive/staged-deploy.sh @@ -83,13 +83,15 @@ EOF test '!' -f /run/ostree/staged-deployment test '!' -f /run/ostree/staged-deployment - ostree admin deploy --stage staged-deploy --lock-finalization + ostree admin status > status.txt + assert_not_file_has_content status.txt 'finalization locked' + ostree admin deploy staged-deploy --lock-finalization + ostree admin status > status.txt + assert_file_has_content status.txt 'finalization locked' test -f /run/ostree/staged-deployment - test -f /run/ostree/staged-deployment-locked # check that we can cleanup the staged deployment ostree admin undeploy 0 test ! -f /run/ostree/staged-deployment - test ! -f /run/ostree/staged-deployment-locked echo "ok cleanup staged" # And verify that re-staging cleans the previous lock