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