diff --git a/Makefile-man.am b/Makefile-man.am index e2f88a16e6..4d99cde1b2 100644 --- a/Makefile-man.am +++ b/Makefile-man.am @@ -26,6 +26,7 @@ ostree-admin-config-diff.1 ostree-admin-deploy.1 \ ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-os-init.1 \ 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.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1 \ ostree-commit.1 ostree-export.1 ostree-gpg-sign.1 ostree-config.1 \ ostree-diff.1 ostree-fsck.1 ostree-init.1 ostree-log.1 ostree-ls.1 \ diff --git a/Makefile-ostree.am b/Makefile-ostree.am index c366c84f94..cccbe30068 100644 --- a/Makefile-ostree.am +++ b/Makefile-ostree.am @@ -74,6 +74,7 @@ ostree_SOURCES += \ src/ostree/ot-admin-builtin-set-origin.c \ src/ostree/ot-admin-builtin-status.c \ src/ostree/ot-admin-builtin-switch.c \ + src/ostree/ot-admin-builtin-pin.c \ src/ostree/ot-admin-builtin-upgrade.c \ src/ostree/ot-admin-builtin-unlock.c \ src/ostree/ot-admin-builtins.h \ diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index 4421ed10d6..55f2e7a956 100644 --- a/apidoc/ostree-sections.txt +++ b/apidoc/ostree-sections.txt @@ -169,10 +169,12 @@ ostree_deployment_get_bootconfig ostree_deployment_get_origin ostree_deployment_get_origin_relpath ostree_deployment_get_unlocked +ostree_deployment_is_pinned ostree_deployment_set_index ostree_deployment_set_bootserial ostree_deployment_set_bootconfig ostree_deployment_set_origin +ostree_deployment_origin_remove_transient_state ostree_deployment_clone ostree_deployment_unlocked_state_to_string @@ -508,6 +510,7 @@ ostree_sysroot_init_osname ostree_sysroot_deployment_set_kargs ostree_sysroot_deployment_set_mutable ostree_sysroot_deployment_unlock +ostree_sysroot_deployment_set_pinned ostree_sysroot_write_deployments ostree_sysroot_write_deployments_with_options ostree_sysroot_write_origin_file diff --git a/bash/ostree b/bash/ostree index fe8e3d2c0a..edef6cff87 100644 --- a/bash/ostree +++ b/bash/ostree @@ -399,6 +399,37 @@ _ostree_admin_os_init() { return 0 } +_ostree_admin_pin() { + local boolean_options=" + $main_boolean_options + " + + local options_with_args=" + --sysroot + " + + local options_with_args_glob=$( __ostree_to_extglob "$options_with_args" ) + + case "$prev" in + --sysroot) + __ostree_compreply_dirs_only + return 0 + ;; + $options_with_args_glob ) + return 0 + ;; + esac + + case "$cur" in + -*) + local all_options="$boolean_options $options_with_args" + __ostree_compreply_all_options + ;; + esac + + return 0 +} + _ostree_admin_set_origin() { local boolean_options=" $main_boolean_options diff --git a/man/ostree-admin-pin.xml b/man/ostree-admin-pin.xml new file mode 100644 index 0000000000..db0787ae2d --- /dev/null +++ b/man/ostree-admin-pin.xml @@ -0,0 +1,82 @@ + + + + + + + + + ostree admin pin + OSTree + + + + Developer + Colin + Walters + walters@verbum.org + + + + + + ostree admin pin + 1 + + + + ostree-admin-pin + Explicitly retain deployment at a given index + + + + + ostree admin pin INDEX + + + + + Description + + + Ensures the deployment at INDEX, will not be garbage + collected by default. This is termed "pinning". If the + -u option is provided, undoes a pinning operation. + + + + + Options + + + + , + + + Undoes a pinning operation. + + + + + + diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym index 3f3a45b5bd..285ba5f5f3 100644 --- a/src/libostree/libostree-devel.sym +++ b/src/libostree/libostree-devel.sym @@ -19,6 +19,9 @@ /* Add new symbols here. Release commits should copy this section into -released.sym. */ LIBOSTREE_2018.3 { + ostree_deployment_origin_remove_transient_state; + ostree_sysroot_deployment_set_pinned; + ostree_deployment_is_pinned; } LIBOSTREE_2018.2; /* Stub section for the stable release *after* this development one; don't diff --git a/src/libostree/ostree-deployment.c b/src/libostree/ostree-deployment.c index 6431aa96c6..75a5bd1dae 100644 --- a/src/libostree/ostree-deployment.c +++ b/src/libostree/ostree-deployment.c @@ -121,6 +121,35 @@ ostree_deployment_set_origin (OstreeDeployment *self, GKeyFile *origin) self->origin = g_key_file_ref (origin); } +/** + * ostree_deployment_origin_remove_transient_state: + * @origin: An origin + * + * The intention of an origin file is primarily describe the "inputs" that + * resulted in a deployment, and it's commonly used to derive the new state. For + * example, a key value (in pure libostree mode) is the "refspec". However, + * libostree (or other applications) may want to store "transient" state that + * should not be carried across upgrades. + * + * This function just removes all members of the `libostree-transient` group. + * The name of that group is available to all libostree users; best practice + * would be to prefix values underneath there with a short identifier for your + * software. + * + * Additionally, this function will remove the `origin/unlocked` and + * `origin/override-commit` members; these should be considered transient state + * that should have been under an explicit group. + * + * Since: 2018.3 + */ +void +ostree_deployment_origin_remove_transient_state (GKeyFile *origin) +{ + g_key_file_remove_group (origin, OSTREE_ORIGIN_TRANSIENT_GROUP, NULL); + g_key_file_remove_key (origin, "origin", "override-commit", NULL); + g_key_file_remove_key (origin, "origin", "unlocked", NULL); +} + void _ostree_deployment_set_bootcsum (OstreeDeployment *self, const char *bootcsum) @@ -293,3 +322,20 @@ ostree_deployment_get_unlocked (OstreeDeployment *self) { return self->unlocked; } + +/** + * ostree_deployment_is_pinned: + * @self: Deployment + * + * See ostree_sysroot_deployment_set_pinned(). + * + * Returns: `TRUE` if deployment will not be subject to GC + * Since: 2018.3 + */ +gboolean +ostree_deployment_is_pinned (OstreeDeployment *self) +{ + if (!self->origin) + return FALSE; + return g_key_file_get_boolean (self->origin, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", NULL); +} diff --git a/src/libostree/ostree-deployment.h b/src/libostree/ostree-deployment.h index b4368f4650..612222a2ab 100644 --- a/src/libostree/ostree-deployment.h +++ b/src/libostree/ostree-deployment.h @@ -27,6 +27,17 @@ G_BEGIN_DECLS #define OSTREE_DEPLOYMENT(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), OSTREE_TYPE_DEPLOYMENT, OstreeDeployment)) #define OSTREE_IS_DEPLOYMENT(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), OSTREE_TYPE_DEPLOYMENT)) +/** + * OSTREE_ORIGIN_TRANSIENT_GROUP: + * + * The name of a `GKeyFile` group for data that should not + * be carried across upgrades. For more information, + * see ostree_deployment_origin_remove_transient_state(). + * + * Since: 2018.3 + */ +#define OSTREE_ORIGIN_TRANSIENT_GROUP "libostree-transient" + typedef struct _OstreeDeployment OstreeDeployment; _OSTREE_PUBLIC @@ -62,6 +73,10 @@ OstreeBootconfigParser *ostree_deployment_get_bootconfig (OstreeDeployment *self _OSTREE_PUBLIC GKeyFile *ostree_deployment_get_origin (OstreeDeployment *self); + +_OSTREE_PUBLIC +gboolean ostree_deployment_is_pinned (OstreeDeployment *self); + _OSTREE_PUBLIC void ostree_deployment_set_index (OstreeDeployment *self, int index); _OSTREE_PUBLIC @@ -71,6 +86,9 @@ void ostree_deployment_set_bootconfig (OstreeDeployment *self, OstreeBootconfigP _OSTREE_PUBLIC void ostree_deployment_set_origin (OstreeDeployment *self, GKeyFile *origin); +_OSTREE_PUBLIC +void ostree_deployment_origin_remove_transient_state (GKeyFile *origin); + _OSTREE_PUBLIC OstreeDeployment *ostree_deployment_clone (OstreeDeployment *self); diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 4284b5ae91..8fdf28a393 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -847,7 +847,9 @@ merge_configuration (OstreeSysroot *sysroot, return TRUE; } -/* Write the origin file for a deployment. */ +/* Write the origin file for a deployment; this does not bump the mtime, under + * the assumption the caller may be writing multiple. + */ static gboolean write_origin_file_internal (OstreeSysroot *sysroot, OstreeDeployment *deployment, @@ -903,9 +905,15 @@ ostree_sysroot_write_origin_file (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error) { - return write_origin_file_internal (sysroot, deployment, new_origin, - GLNX_FILE_REPLACE_DATASYNC_NEW, - cancellable, error); + if (!write_origin_file_internal (sysroot, deployment, new_origin, + GLNX_FILE_REPLACE_DATASYNC_NEW, + cancellable, error)) + return FALSE; + + if (!_ostree_sysroot_bump_mtime (sysroot, error)) + return FALSE; + + return TRUE; } typedef struct { diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index 3479944472..2c12b78b33 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -1572,12 +1572,14 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot, /* Retain deployment if: * - we're explicitly asked to, or + * - it's pinned * - the deployment is for another osname, or * - we're keeping pending deployments and this is a pending deployment, or * - this is the merge or boot deployment, or * - we're keeping rollback deployments and this is a rollback deployment */ if (retain + || ostree_deployment_is_pinned (deployment) || !osname_matches || (retain_pending && !passed_crossover) || (is_booted || is_merge) @@ -1832,3 +1834,45 @@ ostree_sysroot_deployment_unlock (OstreeSysroot *self, return TRUE; } + +/** + * ostree_sysroot_deployment_set_pinned: + * @self: Sysroot + * @deployment: A deployment + * @is_pinned: Whether or not deployment will be automatically GC'd + * @error: Error + * + * By default, deployments may be subject to garbage collection. Typical uses of + * libostree only retain at most 2 deployments. If @is_pinned is `TRUE`, a + * metadata bit will be set causing libostree to avoid automatic GC of the + * deployment. However, this is really an "advisory" note; it's still possible + * for e.g. older versions of libostree unaware of pinning to GC the deployment. + * + * This function does nothing and returns successfully if the deployment + * is already in the desired pinning state. + * + * Since: 2018.3 + */ +gboolean +ostree_sysroot_deployment_set_pinned (OstreeSysroot *self, + OstreeDeployment *deployment, + gboolean is_pinned, + GError **error) +{ + const gboolean current_pin = ostree_deployment_is_pinned (deployment); + if (is_pinned == current_pin) + return TRUE; + + g_autoptr(OstreeDeployment) deployment_clone = ostree_deployment_clone (deployment); + GKeyFile *origin_clone = ostree_deployment_get_origin (deployment_clone); + + if (is_pinned) + g_key_file_set_boolean (origin_clone, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", TRUE); + else + g_key_file_remove_key (origin_clone, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", NULL); + + if (!ostree_sysroot_write_origin_file (self, deployment, origin_clone, NULL, error)) + return FALSE; + + return TRUE; +} diff --git a/src/libostree/ostree-sysroot.h b/src/libostree/ostree-sysroot.h index 830ed272d3..e4763d3755 100644 --- a/src/libostree/ostree-sysroot.h +++ b/src/libostree/ostree-sysroot.h @@ -181,6 +181,12 @@ gboolean ostree_sysroot_deployment_set_mutable (OstreeSysroot *self, GCancellable *cancellable, GError **error); +_OSTREE_PUBLIC +gboolean ostree_sysroot_deployment_set_pinned (OstreeSysroot *self, + OstreeDeployment *deployment, + gboolean is_pinned, + GError **error); + _OSTREE_PUBLIC gboolean ostree_sysroot_deployment_unlock (OstreeSysroot *self, OstreeDeployment *deployment, diff --git a/src/ostree/ot-admin-builtin-pin.c b/src/ostree/ot-admin-builtin-pin.c new file mode 100644 index 0000000000..e26ea92637 --- /dev/null +++ b/src/ostree/ot-admin-builtin-pin.c @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 Colin Walters + * + * 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, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include + +#include "ot-main.h" +#include "ot-admin-builtins.h" +#include "ot-admin-functions.h" +#include "ostree.h" +#include "otutil.h" + +/* ATTENTION: + * Please remember to update the bash-completion script (bash/ostree) and + * man page (man/ostree-admin-pin.xml) when changing the option list. + */ + +static gboolean opt_unpin; + +static GOptionEntry options[] = { + { "unpin", 'u', 0, G_OPTION_ARG_NONE, &opt_unpin, "Unset pin", NULL }, + { NULL } +}; + +gboolean +ot_admin_builtin_pin (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error) +{ + g_autoptr(GOptionContext) context = g_option_context_new ("INDEX"); + 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; + + if (argc < 2) + { + ot_util_usage_error (context, "INDEX must be specified", error); + return FALSE; + } + + const char *deploy_index_str = argv[1]; + const int deploy_index = atoi (deploy_index_str); + + g_autoptr(OstreeDeployment) target_deployment = ot_admin_get_indexed_deployment (sysroot, deploy_index, error); + if (!target_deployment) + return FALSE; + + + gboolean current_pin = ostree_deployment_is_pinned (target_deployment); + const gboolean desired_pin = !opt_unpin; + if (current_pin == desired_pin) + g_print ("Deployment is already %s\n", current_pin ? "pinned" : "unpinned"); + else + { + if (!ostree_sysroot_deployment_set_pinned (sysroot, target_deployment, desired_pin, error)) + return FALSE; + g_print ("Deployment is now %s\n", desired_pin ? "pinned" : "unpinned"); + } + + return TRUE; +} diff --git a/src/ostree/ot-admin-builtin-status.c b/src/ostree/ot-admin-builtin-status.c index 0279c5aff2..0299130944 100644 --- a/src/ostree/ot-admin-builtin-status.c +++ b/src/ostree/ot-admin-builtin-status.c @@ -149,6 +149,8 @@ ot_admin_builtin_status (int argc, char **argv, OstreeCommandInvocation *invocat ostree_deployment_unlocked_state_to_string (unlocked), red_bold_suffix); } + if (ostree_deployment_is_pinned (deployment)) + g_print (" Pinned: yes\n"); if (!origin) g_print (" origin: none\n"); else diff --git a/src/ostree/ot-admin-builtin-upgrade.c b/src/ostree/ot-admin-builtin-upgrade.c index e42ded6cab..1b6a025340 100644 --- a/src/ostree/ot-admin-builtin-upgrade.c +++ b/src/ostree/ot-admin-builtin-upgrade.c @@ -88,33 +88,18 @@ ot_admin_builtin_upgrade (int argc, char **argv, OstreeCommandInvocation *invoca g_autoptr(GKeyFile) origin = ostree_sysroot_upgrader_dup_origin (upgrader); if (origin != NULL) { - gboolean origin_changed = FALSE; - + /* Should we consider requiring --discard-hotfix here? */ + ostree_deployment_origin_remove_transient_state (origin); if (opt_override_commit != NULL) { /* Override the commit to pull and deploy. */ g_key_file_set_string (origin, "origin", "override-commit", opt_override_commit); - origin_changed = TRUE; } - else - { - /* Strip any override-commit from the origin file so - * we always upgrade to the latest available commit. */ - origin_changed = g_key_file_remove_key (origin, "origin", - "override-commit", NULL); - } - - /* Should we consider requiring --discard-hotfix here? */ - origin_changed |= g_key_file_remove_key (origin, "origin", "unlocked", NULL); - if (origin_changed) - { - /* XXX GCancellable parameter is not used. */ - if (!ostree_sysroot_upgrader_set_origin (upgrader, origin, NULL, error)) - return FALSE; - } + if (!ostree_sysroot_upgrader_set_origin (upgrader, origin, NULL, error)) + return FALSE; } gboolean changed; diff --git a/src/ostree/ot-admin-builtins.h b/src/ostree/ot-admin-builtins.h index 3999624393..a81f4d62d4 100644 --- a/src/ostree/ot-admin-builtins.h +++ b/src/ostree/ot-admin-builtins.h @@ -39,6 +39,7 @@ BUILTINPROTO(init_fs); BUILTINPROTO(undeploy); BUILTINPROTO(deploy); BUILTINPROTO(cleanup); +BUILTINPROTO(pin); BUILTINPROTO(unlock); BUILTINPROTO(status); BUILTINPROTO(set_origin); diff --git a/src/ostree/ot-builtin-admin.c b/src/ostree/ot-builtin-admin.c index f4e687e982..fd6d9a8f92 100644 --- a/src/ostree/ot-builtin-admin.c +++ b/src/ostree/ot-builtin-admin.c @@ -54,6 +54,9 @@ static OstreeCommand admin_subcommands[] = { { "set-origin", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_set_origin, "Set Origin and create a new origin file" }, + { "pin", OSTREE_BUILTIN_FLAG_NO_REPO, + ot_admin_builtin_pin, + "Change the \"pinning\" state of a deployment" }, { "status", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_status, "List deployments" }, diff --git a/tests/test-admin-deploy-2.sh b/tests/test-admin-deploy-2.sh index 4ecaf67ad9..eab0a3d3db 100755 --- a/tests/test-admin-deploy-2.sh +++ b/tests/test-admin-deploy-2.sh @@ -26,7 +26,7 @@ set -euo pipefail # Exports OSTREE_SYSROOT so --sysroot not needed. setup_os_repository "archive" "syslinux" -echo "1..3" +echo "1..6" ${CMD_PREFIX} ostree --repo=sysroot/ostree/repo pull-local --remote=testos testos-repo testos/buildmaster/x86_64-runtime rev=$(${CMD_PREFIX} ostree --repo=sysroot/ostree/repo rev-parse testos/buildmaster/x86_64-runtime) @@ -63,3 +63,50 @@ assert_has_dir sysroot/boot/ostree/testos-${bootcsum} assert_file_has_content sysroot/ostree/deploy/testos/deploy/${newrev}.0/etc/os-release 'NAME=TestOS' echo "ok manual cleanup" + +assert_n_pinned() { + local n=$1 + ${CMD_PREFIX} ostree admin status > status.txt + local n_pinned="$(grep -F -c -e 'Pinned: yes' < status.txt)" + if test "${n_pinned}" '!=' "${n}"; then + cat status.txt + fatal "${n_pinned} != ${n}" + fi +} +assert_n_deployments() { + local n=$1 + ${CMD_PREFIX} ostree admin status > status.txt + local n_deployments="$(grep -F -c -e 'Version: ' < status.txt)" + if test "${n_deployments}" '!=' "${n}"; then + cat status.txt + fatal "${n_deployments} != ${n}" + fi +} +assert_n_pinned 0 +${CMD_PREFIX} ostree admin pin 0 +assert_n_pinned 1 +${CMD_PREFIX} ostree admin pin -u 0 +assert_n_pinned 0 +echo "ok pin unpin" + +${CMD_PREFIX} ostree admin pin 0 +${CMD_PREFIX} ostree admin pin 1 +assert_n_pinned 2 +assert_n_deployments 2 +os_repository_new_commit +${CMD_PREFIX} ostree admin upgrade --os=testos +assert_n_pinned 2 +assert_n_deployments 3 +echo "ok pin across upgrades" + +${CMD_PREFIX} ostree admin pin -u 1 +os_repository_new_commit +${CMD_PREFIX} ostree admin upgrade --os=testos +assert_n_pinned 1 +assert_n_deployments 3 +os_repository_new_commit +${CMD_PREFIX} ostree admin upgrade --os=testos +assert_n_pinned 1 +assert_n_deployments 3 + +echo "ok pinning"