diff --git a/Makefile-boot.am b/Makefile-boot.am index d3d2f67386..5b512b6ce7 100644 --- a/Makefile-boot.am +++ b/Makefile-boot.am @@ -39,7 +39,7 @@ endif if BUILDOPT_SYSTEMD systemdsystemunit_DATA = src/boot/ostree-prepare-root.service \ - src/boot/ostree-remount.service + src/boot/ostree-remount.service src/boot/ostree-finalize-staged.service systemdtmpfilesdir = $(prefix)/lib/tmpfiles.d dist_systemdtmpfiles_DATA = src/boot/ostree-tmpfiles.conf @@ -65,6 +65,7 @@ EXTRA_DIST += src/boot/dracut/module-setup.sh \ src/boot/mkinitcpio/ostree \ src/boot/ostree-prepare-root.service \ src/boot/ostree-remount.service \ + src/boot/ostree-finalize-staged.service \ src/boot/grub2/grub2-15_ostree \ src/boot/grub2/ostree-grub-generator \ $(NULL) diff --git a/Makefile-ostree.am b/Makefile-ostree.am index cccbe30068..bdd51a72db 100644 --- a/Makefile-ostree.am +++ b/Makefile-ostree.am @@ -67,6 +67,7 @@ ostree_SOURCES += \ src/ostree/ot-admin-builtin-init-fs.c \ src/ostree/ot-admin-builtin-diff.c \ src/ostree/ot-admin-builtin-deploy.c \ + src/ostree/ot-admin-builtin-finalize-staged.c \ src/ostree/ot-admin-builtin-undeploy.c \ src/ostree/ot-admin-builtin-instutil.c \ src/ostree/ot-admin-builtin-cleanup.c \ diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index 55f2e7a956..94206e03e3 100644 --- a/apidoc/ostree-sections.txt +++ b/apidoc/ostree-sections.txt @@ -170,6 +170,7 @@ ostree_deployment_get_origin ostree_deployment_get_origin_relpath ostree_deployment_get_unlocked ostree_deployment_is_pinned +ostree_deployment_is_staged ostree_deployment_set_index ostree_deployment_set_bootserial ostree_deployment_set_bootconfig @@ -506,6 +507,7 @@ ostree_sysroot_cleanup ostree_sysroot_prepare_cleanup ostree_sysroot_repo ostree_sysroot_get_repo +ostree_sysroot_get_staged_deployment ostree_sysroot_init_osname ostree_sysroot_deployment_set_kargs ostree_sysroot_deployment_set_mutable @@ -514,6 +516,7 @@ ostree_sysroot_deployment_set_pinned ostree_sysroot_write_deployments ostree_sysroot_write_deployments_with_options ostree_sysroot_write_origin_file +ostree_sysroot_stage_tree ostree_sysroot_deploy_tree ostree_sysroot_get_merge_deployment ostree_sysroot_query_deployments_for diff --git a/src/boot/ostree-finalize-staged.service b/src/boot/ostree-finalize-staged.service new file mode 100644 index 0000000000..570138cd6c --- /dev/null +++ b/src/boot/ostree-finalize-staged.service @@ -0,0 +1,36 @@ +# Copyright (C) 2018 Red Hat, Inc. +# +# 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. + +# For some implementation discussion, see: +# https://lists.freedesktop.org/archives/systemd-devel/2018-March/040557.html +[Unit] +Description=OSTree Finalize Staged Deployment +ConditionPathExists=/run/ostree-booted +DefaultDependencies=no + +RequiresMountsFor=/sysroot +After=basic.target +Before=multi-user.target final.target +Conflicts=final.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStop=/usr/bin/ostree admin finalize-staged + +[Install] +WantedBy=multi-user.target diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym index 3377ae126b..07e11cb6ac 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.5 { + ostree_sysroot_stage_tree; + ostree_sysroot_get_staged_deployment; + ostree_deployment_is_staged; } LIBOSTREE_2018.3; /* Stub section for the stable release *after* this development one; don't diff --git a/src/libostree/ostree-cmdprivate.c b/src/libostree/ostree-cmdprivate.c index 49d3f5e5b8..de82521cc3 100644 --- a/src/libostree/ostree-cmdprivate.c +++ b/src/libostree/ostree-cmdprivate.c @@ -26,7 +26,7 @@ #include "ostree-core-private.h" #include "ostree-repo-pull-private.h" #include "ostree-repo-static-delta-private.h" -#include "ostree-sysroot.h" +#include "ostree-sysroot-private.h" #include "ostree-bootloader-grub2.h" #include "otutil.h" @@ -52,7 +52,8 @@ ostree_cmd__private__ (void) _ostree_repo_static_delta_dump, _ostree_repo_static_delta_query_exists, _ostree_repo_static_delta_delete, - _ostree_repo_verify_bindings + _ostree_repo_verify_bindings, + _ostree_sysroot_finalize_staged, }; return &table; diff --git a/src/libostree/ostree-cmdprivate.h b/src/libostree/ostree-cmdprivate.h index 1ac5a1c827..592157bf3c 100644 --- a/src/libostree/ostree-cmdprivate.h +++ b/src/libostree/ostree-cmdprivate.h @@ -34,6 +34,7 @@ typedef struct { gboolean (* ostree_static_delta_query_exists) (OstreeRepo *repo, const char *delta_id, gboolean *out_exists, GCancellable *cancellable, GError **error); gboolean (* ostree_static_delta_delete) (OstreeRepo *repo, const char *delta_id, GCancellable *cancellable, GError **error); gboolean (* ostree_repo_verify_bindings) (const char *collection_id, const char *ref_name, GVariant *commit, GError **error); + gboolean (* ostree_finalize_staged) (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error); } OstreeCmdPrivateVTable; /* Note this not really "public", we just export the symbol, but not the header */ diff --git a/src/libostree/ostree-deployment-private.h b/src/libostree/ostree-deployment-private.h index 114e2f6331..ad77317d79 100644 --- a/src/libostree/ostree-deployment-private.h +++ b/src/libostree/ostree-deployment-private.h @@ -36,6 +36,7 @@ G_BEGIN_DECLS * @bootconfig: Bootloader configuration * @origin: How to construct an upgraded version of this tree * @unlocked: The unlocked state + * @staged: TRUE iff this deployment is staged */ struct _OstreeDeployment { @@ -50,6 +51,7 @@ struct _OstreeDeployment OstreeBootconfigParser *bootconfig; GKeyFile *origin; OstreeDeploymentUnlockedState unlocked; + gboolean staged; }; void _ostree_deployment_set_bootcsum (OstreeDeployment *self, const char *bootcsum); diff --git a/src/libostree/ostree-deployment.c b/src/libostree/ostree-deployment.c index 75a5bd1dae..820c263212 100644 --- a/src/libostree/ostree-deployment.c +++ b/src/libostree/ostree-deployment.c @@ -339,3 +339,16 @@ ostree_deployment_is_pinned (OstreeDeployment *self) return FALSE; return g_key_file_get_boolean (self->origin, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", NULL); } + +/** + * ostree_deployment_is_staged: + * @self: Deployment + * + * Returns: `TRUE` if deployment should be "finalized" at shutdown time + * Since: 2018.3 + */ +gboolean +ostree_deployment_is_staged (OstreeDeployment *self) +{ + return self->staged; +} diff --git a/src/libostree/ostree-deployment.h b/src/libostree/ostree-deployment.h index 612222a2ab..756e39d2b4 100644 --- a/src/libostree/ostree-deployment.h +++ b/src/libostree/ostree-deployment.h @@ -73,7 +73,8 @@ OstreeBootconfigParser *ostree_deployment_get_bootconfig (OstreeDeployment *self _OSTREE_PUBLIC GKeyFile *ostree_deployment_get_origin (OstreeDeployment *self); - +_OSTREE_PUBLIC +gboolean ostree_deployment_is_staged (OstreeDeployment *self); _OSTREE_PUBLIC gboolean ostree_deployment_is_pinned (OstreeDeployment *self); diff --git a/src/libostree/ostree-sysroot-cleanup.c b/src/libostree/ostree-sysroot-cleanup.c index 1d46222b9a..3698767fc0 100644 --- a/src/libostree/ostree-sysroot-cleanup.c +++ b/src/libostree/ostree-sysroot-cleanup.c @@ -308,6 +308,15 @@ cleanup_old_deployments (OstreeSysroot *self, g_hash_table_replace (active_boot_checksums, bootcsum, bootcsum); } + /* And also the staged deployment, if any */ + if (self->staged_deployment) + { + char *deployment_path = ostree_sysroot_get_deployment_dirpath (self, self->staged_deployment); + g_hash_table_replace (active_deployment_dirs, deployment_path, deployment_path); + char *bootcsum = g_strdup (ostree_deployment_get_bootcsum (self->staged_deployment)); + g_hash_table_replace (active_boot_checksums, bootcsum, bootcsum); + } + /* Find all deployment directories, both active and inactive */ g_autoptr(GPtrArray) all_deployment_dirs = NULL; if (!list_all_deployment_directories (self, &all_deployment_dirs, diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 927809e93b..b593ce38f8 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -684,10 +684,15 @@ selinux_relabel_dir (OstreeSysroot *sysroot, static gboolean selinux_relabel_var_if_needed (OstreeSysroot *sysroot, OstreeSePolicy *sepolicy, - int os_deploy_dfd, + OstreeDeployment *deployment, GCancellable *cancellable, GError **error) { + const char *osdeploypath = glnx_strjoina ("ostree/deploy/", ostree_deployment_get_osname (deployment)); + glnx_autofd int os_deploy_dfd = -1; + if (!glnx_opendirat (sysroot->sysroot_fd, osdeploypath, TRUE, &os_deploy_dfd, error)) + return FALSE; + /* This is a bit of a hack; we should change the code at some * point in the distant future to only create (and label) /var * when doing a deployment. @@ -743,12 +748,10 @@ prepare_deployment_etc (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeployment *deployment, int deployment_dfd, - OstreeSePolicy **out_sepolicy, GCancellable *cancellable, GError **error) { GLNX_AUTO_PREFIX_ERROR ("Preparing /etc", error); - g_autoptr(OstreeSePolicy) sepolicy = NULL; struct stat stbuf; if (!glnx_fstatat_allow_noent (deployment_dfd, "etc", &stbuf, AT_SYMLINK_NOFOLLOW, error)) @@ -781,7 +784,7 @@ prepare_deployment_etc (OstreeSysroot *sysroot, /* Here, we initialize SELinux policy from the /usr/etc inside * the root - this is before we've finalized the configuration * merge into /etc. */ - sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error); + g_autoptr(OstreeSePolicy) sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error); if (!sepolicy) return FALSE; if (ostree_sepolicy_get_name (sepolicy) != NULL) @@ -796,8 +799,6 @@ prepare_deployment_etc (OstreeSysroot *sysroot, } - if (out_sepolicy) - *out_sepolicy = g_steal_pointer (&sepolicy); return TRUE; } @@ -831,7 +832,6 @@ write_origin_file_internal (OstreeSysroot *sysroot, ostree_deployment_get_csum (deployment), ostree_deployment_get_deployserial (deployment)); - gsize len; g_autofree char *contents = g_key_file_to_data (origin, &len, error); if (!contents) @@ -2324,46 +2324,47 @@ allocate_deployserial (OstreeSysroot *self, return TRUE; } -/** - * ostree_sysroot_deploy_tree: - * @self: Sysroot - * @osname: (allow-none): osname to use for merge deployment - * @revision: Checksum to add - * @origin: (allow-none): Origin to use for upgrades - * @provided_merge_deployment: (allow-none): Use this deployment for merge path - * @override_kernel_argv: (allow-none) (array zero-terminated=1) (element-type utf8): Use these as kernel arguments; if %NULL, inherit options from provided_merge_deployment - * @out_new_deployment: (out): The new deployment path - * @cancellable: Cancellable - * @error: Error - * - * Check out deployment tree with revision @revision, performing a 3 - * way merge with @provided_merge_deployment for configuration. +void +_ostree_deployment_set_bootconfig_from_kargs (OstreeDeployment *deployment, + char **override_kernel_argv) +{ + /* Create an empty boot configuration; we will merge things into + * it as we go. + */ + g_autoptr(OstreeBootconfigParser) bootconfig = ostree_bootconfig_parser_new (); + ostree_deployment_set_bootconfig (deployment, bootconfig); + + /* After this, install_deployment_kernel() will set the other boot + * options and write it out to disk. + */ + if (override_kernel_argv) + { + g_autoptr(OstreeKernelArgs) kargs = _ostree_kernel_args_new (); + _ostree_kernel_args_append_argv (kargs, override_kernel_argv); + g_autofree char *new_options = _ostree_kernel_args_to_string (kargs); + ostree_bootconfig_parser_set (bootconfig, "options", new_options); + } +} + +/* The first part of writing a deployment. This primarily means doing the + * hardlink farm checkout, but we also compute some initial state. */ -gboolean -ostree_sysroot_deploy_tree (OstreeSysroot *self, - const char *osname, - const char *revision, - GKeyFile *origin, - OstreeDeployment *provided_merge_deployment, - char **override_kernel_argv, - OstreeDeployment **out_new_deployment, - GCancellable *cancellable, - GError **error) +static gboolean +sysroot_initialize_deployment (OstreeSysroot *self, + const char *osname, + const char *revision, + GKeyFile *origin, + char **override_kernel_argv, + OstreeDeployment **out_new_deployment, + GCancellable *cancellable, + GError **error) { g_return_val_if_fail (osname != NULL || self->booted_deployment != NULL, FALSE); if (osname == NULL) osname = ostree_deployment_get_osname (self->booted_deployment); - const char *osdeploypath = glnx_strjoina ("ostree/deploy/", osname); - glnx_autofd int os_deploy_dfd = -1; - if (!glnx_opendirat (self->sysroot_fd, osdeploypath, TRUE, &os_deploy_dfd, error)) - return FALSE; - OstreeRepo *repo = ostree_sysroot_repo (self); - g_autoptr(OstreeDeployment) merge_deployment = NULL; - if (provided_merge_deployment != NULL) - merge_deployment = g_object_ref (provided_merge_deployment); gint new_deployserial; if (!allocate_deployserial (self, osname, revision, &new_deployserial, @@ -2388,66 +2389,328 @@ ostree_sysroot_deploy_tree (OstreeSysroot *self, return FALSE; _ostree_deployment_set_bootcsum (new_deployment, kernel_layout->bootcsum); + _ostree_deployment_set_bootconfig_from_kargs (new_deployment, override_kernel_argv); - /* Initial empty boot configuration. */ - g_autoptr(OstreeBootconfigParser) bootconfig = ostree_bootconfig_parser_new (); - ostree_deployment_set_bootconfig (new_deployment, bootconfig); + if (!prepare_deployment_etc (self, repo, new_deployment, deployment_dfd, + cancellable, error)) + return FALSE; - /* Handle kernel arguments. After this, install_deployment_kernel() will set - * the other boot options and write it out to disk. - */ - if (override_kernel_argv) - { - /* We have an override set, use it */ - g_autoptr(OstreeKernelArgs) kargs = _ostree_kernel_args_new (); - _ostree_kernel_args_append_argv (kargs, override_kernel_argv); - g_autofree char *new_options = _ostree_kernel_args_to_string (kargs); - ostree_bootconfig_parser_set (bootconfig, "options", new_options); - } - else if (provided_merge_deployment) + ot_transfer_out_value (out_new_deployment, &new_deployment); + return TRUE; +} + +static gboolean +sysroot_finalize_deployment (OstreeSysroot *self, + OstreeDeployment *deployment, + char **override_kernel_argv, + OstreeDeployment *merge_deployment, + GCancellable *cancellable, + GError **error) +{ + g_autofree char *deployment_path = ostree_sysroot_get_deployment_dirpath (self, deployment); + glnx_autofd int deployment_dfd = -1; + if (!glnx_opendirat (self->sysroot_fd, deployment_path, TRUE, &deployment_dfd, error)) + return FALSE; + + /* Only use the merge if we didn't get an override */ + if (!override_kernel_argv && merge_deployment) { - /* Use the merge options by default */ - OstreeBootconfigParser *merge_bootconfig = ostree_deployment_get_bootconfig (provided_merge_deployment); + /* Override the bootloader arguments */ + OstreeBootconfigParser *merge_bootconfig = ostree_deployment_get_bootconfig (merge_deployment); if (merge_bootconfig) { const char *opts = ostree_bootconfig_parser_get (merge_bootconfig, "options"); - ostree_bootconfig_parser_set (bootconfig, "options", opts); + ostree_bootconfig_parser_set (ostree_deployment_get_bootconfig (deployment), "options", opts); } - } - g_autoptr(OstreeSePolicy) sepolicy = NULL; - if (!prepare_deployment_etc (self, repo, new_deployment, deployment_dfd, - &sepolicy, cancellable, error)) - return FALSE; + } if (merge_deployment) { - if (!merge_configuration_from (self, merge_deployment, - new_deployment, deployment_dfd, + /* And do the /etc merge */ + if (!merge_configuration_from (self, merge_deployment, deployment, deployment_dfd, cancellable, error)) return FALSE; } - if (!selinux_relabel_var_if_needed (self, sepolicy, os_deploy_dfd, - cancellable, error)) + g_autoptr(OstreeSePolicy) sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error); + if (!sepolicy) return FALSE; + if (!selinux_relabel_var_if_needed (self, sepolicy, deployment, cancellable, error)) + return FALSE; + + /* Rewrite the origin using the final merged selinux config, just to be + * conservative about getting the right labels. + */ + if (!write_origin_file_internal (self, sepolicy, deployment, + ostree_deployment_get_origin (deployment), + GLNX_FILE_REPLACE_NODATASYNC, + cancellable, error)) + return FALSE; + + /* Seal it */ if (!(self->debug_flags & OSTREE_SYSROOT_DEBUG_MUTABLE_DEPLOYMENTS)) { - if (!ostree_sysroot_deployment_set_mutable (self, new_deployment, FALSE, + if (!ostree_sysroot_deployment_set_mutable (self, deployment, FALSE, cancellable, error)) return FALSE; } - /* Don't fsync here, as we assume that's all done in - * ostree_sysroot_write_deployments(). + return TRUE; +} + +/** + * ostree_sysroot_deploy_tree: + * @self: Sysroot + * @osname: (allow-none): osname to use for merge deployment + * @revision: Checksum to add + * @origin: (allow-none): Origin to use for upgrades + * @provided_merge_deployment: (allow-none): Use this deployment for merge path + * @override_kernel_argv: (allow-none) (array zero-terminated=1) (element-type utf8): Use these as kernel arguments; if %NULL, inherit options from provided_merge_deployment + * @out_new_deployment: (out): The new deployment path + * @cancellable: Cancellable + * @error: Error + * + * Check out deployment tree with revision @revision, performing a 3 + * way merge with @provided_merge_deployment for configuration. + * + * While this API is not deprecated, you most likely want to use the + * ostree_sysroot_stage_tree() API. + */ +gboolean +ostree_sysroot_deploy_tree (OstreeSysroot *self, + const char *osname, + const char *revision, + GKeyFile *origin, + OstreeDeployment *provided_merge_deployment, + char **override_kernel_argv, + OstreeDeployment **out_new_deployment, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(OstreeDeployment) deployment = NULL; + if (!sysroot_initialize_deployment (self, osname, revision, origin, override_kernel_argv, + &deployment, cancellable, error)) + return FALSE; + + if (!sysroot_finalize_deployment (self, deployment, override_kernel_argv, + provided_merge_deployment, + cancellable, error)) + return FALSE; + + *out_new_deployment = g_steal_pointer (&deployment); + return TRUE; +} + +/* Serialize information about a deployment to a variant, used by the staging + * code. + */ +static GVariant * +serialize_deployment_to_variant (OstreeDeployment *deployment) +{ + g_auto(GVariantBuilder) builder = OT_VARIANT_BUILDER_INITIALIZER; + g_variant_builder_init (&builder, (GVariantType*)"a{sv}"); + g_autofree char *name = + g_strdup_printf ("%s.%d", ostree_deployment_get_csum (deployment), + ostree_deployment_get_deployserial (deployment)); + g_variant_builder_add (&builder, "{sv}", "name", + g_variant_new_string (name)); + g_variant_builder_add (&builder, "{sv}", "osname", + g_variant_new_string (ostree_deployment_get_osname (deployment))); + g_variant_builder_add (&builder, "{sv}", "bootcsum", + g_variant_new_string (ostree_deployment_get_bootcsum (deployment))); + + return g_variant_builder_end (&builder); +} + +static gboolean +require_str_key (GVariantDict *dict, + const char *name, + const char **ret, + GError **error) +{ + if (!g_variant_dict_lookup (dict, name, "&s", ret)) + return glnx_throw (error, "Missing key: %s", name); + return TRUE; +} + +/* Reverse of the above; convert a variant to a deployment. Note that the + * deployment may not actually be present; this should be verified by + * higher level code. + */ +OstreeDeployment * +_ostree_sysroot_deserialize_deployment_from_variant (GVariant *v, + GError **error) +{ + g_autoptr(GVariantDict) dict = g_variant_dict_new (v); + const char *name = NULL; + if (!require_str_key (dict, "name", &name, error)) + return FALSE; + const char *bootcsum = NULL; + if (!require_str_key (dict, "bootcsum", &bootcsum, error)) + return FALSE; + const char *osname = NULL; + if (!require_str_key (dict, "osname", &osname, error)) + return FALSE; + g_autofree char *checksum = NULL; + gint deployserial; + if (!_ostree_sysroot_parse_deploy_path_name (name, &checksum, &deployserial, error)) + return NULL; + return ostree_deployment_new (-1, osname, checksum, deployserial, + bootcsum, -1); +} + + +/** + * ostree_sysroot_stage_tree: + * @self: Sysroot + * @osname: (allow-none): osname to use for merge deployment + * @revision: Checksum to add + * @origin: (allow-none): Origin to use for upgrades + * @merge_deployment: (allow-none): Use this deployment for merge path + * @override_kernel_argv: (allow-none) (array zero-terminated=1) (element-type utf8): Use these as kernel arguments; if %NULL, inherit options from provided_merge_deployment + * @out_new_deployment: (out): The new deployment path + * @cancellable: Cancellable + * @error: Error + * + * Like ostree_sysroot_deploy_tree(), but "finalization" only occurs at OS + * shutdown time. + */ +gboolean +ostree_sysroot_stage_tree (OstreeSysroot *self, + const char *osname, + const char *revision, + GKeyFile *origin, + OstreeDeployment *merge_deployment, + char **override_kernel_argv, + OstreeDeployment **out_new_deployment, + GCancellable *cancellable, + GError **error) +{ + /* This is a bit of a hack. When adding a new service we have to end up getting + * into the presets for downstream distros; see e.g. https://src.fedoraproject.org/rpms/ostree/pull-request/7 + * + * Then again, it's perhaps a bit nicer to only start the service on-demand anyways. */ - if (!write_origin_file_internal (self, sepolicy, new_deployment, NULL, - GLNX_FILE_REPLACE_NODATASYNC, - cancellable, error)) + const char *const systemctl_argv[] = {"systemctl", "start", "ostree-finalize-staged.service", NULL}; + int estatus; + if (!g_spawn_sync (NULL, (char**)systemctl_argv, NULL, G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, NULL, &estatus, error)) + return FALSE; + if (!g_spawn_check_exit_status (estatus, error)) + return FALSE; + + g_autoptr(OstreeDeployment) deployment = NULL; + if (!sysroot_initialize_deployment (self, osname, revision, origin, override_kernel_argv, + &deployment, cancellable, error)) + return FALSE; + + /* Write out the origin file using the sepolicy from the non-merged root for + * now (i.e. using /usr/etc policy, not /etc); in practice we don't really + * expect people to customize the label for it. + */ + { g_autofree char *deployment_path = ostree_sysroot_get_deployment_dirpath (self, deployment); + glnx_autofd int deployment_dfd = -1; + if (!glnx_opendirat (self->sysroot_fd, deployment_path, FALSE, + &deployment_dfd, error)) + return FALSE; + g_autoptr(OstreeSePolicy) sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error); + if (!sepolicy) + return FALSE; + if (!write_origin_file_internal (self, sepolicy, deployment, + ostree_deployment_get_origin (deployment), + GLNX_FILE_REPLACE_NODATASYNC, + cancellable, error)) + return FALSE; + } + + /* After here we defer action until shutdown. The remaining arguments (merge + * deployment, kargs) are serialized to a state file in /run. + */ + + /* "target" is the staged deployment */ + g_autoptr(GVariantBuilder) builder = g_variant_builder_new ((GVariantType*)"a{sv}"); + g_variant_builder_add (builder, "{sv}", "target", + serialize_deployment_to_variant (deployment)); + + if (merge_deployment) + g_variant_builder_add (builder, "{sv}", "merge-deployment", + serialize_deployment_to_variant (merge_deployment)); + + if (override_kernel_argv) + g_variant_builder_add (builder, "{sv}", "kargs", + g_variant_new_strv ((const char *const*)override_kernel_argv, -1)); + + const char *parent = dirname (strdupa (_OSTREE_SYSROOT_RUNSTATE_STAGED)); + if (!glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, cancellable, error)) + return FALSE; + + g_autoptr(GVariant) state = g_variant_ref_sink (g_variant_builder_end (builder)); + if (!glnx_file_replace_contents_at (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED, + g_variant_get_data (state), g_variant_get_size (state), + GLNX_FILE_REPLACE_NODATASYNC, + cancellable, error)) + return FALSE; + + /* If we have a previous one, clean it up */ + if (self->staged_deployment) + { + if (!_ostree_sysroot_rmrf_deployment (self, self->staged_deployment, cancellable, error)) + return FALSE; + } + + if (!_ostree_sysroot_reload_staged (self, error)) + return FALSE; + + return TRUE; +} + +/* Invoked at shutdown time by ostree-finalize-staged.service */ +gboolean +_ostree_sysroot_finalize_staged (OstreeSysroot *self, + GCancellable *cancellable, + GError **error) +{ + /* It's totally fine if there's no staged deployment; perhaps down the line + * though we could teach the ostree cmdline to tell systemd to activate the + * service when a staged deployment is created. + */ + if (!self->staged_deployment) + return TRUE; + + g_assert (self->staged_deployment_data); + + g_autoptr(OstreeDeployment) merge_deployment = NULL; + g_autoptr(GVariant) merge_deployment_v = NULL; + if (g_variant_lookup (self->staged_deployment_data, "merge-deployment", "@a{sv}", + &merge_deployment_v)) + { + merge_deployment = + _ostree_sysroot_deserialize_deployment_from_variant (merge_deployment_v, error); + if (!merge_deployment) + return FALSE; + } + g_autofree char **kargs = NULL; + g_variant_lookup (self->staged_deployment_data, "kargs", "^a&s", &kargs); + + /* Unlink the staged state now; if we're interrupted in the middle, + * we don't want e.g. deal with the partially written /etc merge. + */ + if (!glnx_unlinkat (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED, 0, error)) + return FALSE; + + if (!sysroot_finalize_deployment (self, self->staged_deployment, NULL, merge_deployment, + cancellable, error)) + return FALSE; + + /* TODO: Proxy across flags too? */ + OstreeSysrootSimpleWriteDeploymentFlags flags = 0; + if (!ostree_sysroot_simple_write_deployment (self, ostree_deployment_get_osname (self->staged_deployment), + self->staged_deployment, merge_deployment, flags, + cancellable, error)) return FALSE; - ot_transfer_out_value (out_new_deployment, &new_deployment); return TRUE; } diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h index 01b370e8a2..a2f3b869fb 100644 --- a/src/libostree/ostree-sysroot-private.h +++ b/src/libostree/ostree-sysroot-private.h @@ -61,6 +61,8 @@ struct OstreeSysroot { int bootversion; int subbootversion; OstreeDeployment *booted_deployment; + OstreeDeployment *staged_deployment; + GVariant *staged_deployment_data; struct timespec loaded_ts; /* Only access through ostree_sysroot_[_get]repo() */ @@ -71,6 +73,7 @@ struct OstreeSysroot { #define OSTREE_SYSROOT_LOCKFILE "ostree/lock" /* We keep some transient state in /run */ +#define _OSTREE_SYSROOT_RUNSTATE_STAGED "/run/ostree/staged-deployment" #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR "/run/ostree/deployment-state/" #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT "unlocked-development" @@ -105,6 +108,22 @@ _ostree_sysroot_list_deployment_dirs_for_os (int deploydir_dfd, GCancellable *cancellable, GError **error); +void +_ostree_deployment_set_bootconfig_from_kargs (OstreeDeployment *deployment, + char **override_kernel_argv); + +gboolean +_ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error); + +gboolean +_ostree_sysroot_finalize_staged (OstreeSysroot *self, + GCancellable *cancellable, + GError **error); + +OstreeDeployment * +_ostree_sysroot_deserialize_deployment_from_variant (GVariant *v, + GError **error); + char * _ostree_sysroot_get_origin_relpath (GFile *path, guint32 *out_device, @@ -118,6 +137,8 @@ _ostree_sysroot_rmrf_deployment (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error); +char * _ostree_sysroot_get_runstate_path (OstreeDeployment *deployment, const char *key); + char *_ostree_sysroot_join_lines (GPtrArray *lines); gboolean _ostree_sysroot_query_bootloader (OstreeSysroot *sysroot, diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index f77d770371..f4a8eadedb 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -82,6 +82,8 @@ ostree_sysroot_finalize (GObject *object) g_clear_object (&self->repo); g_clear_pointer (&self->deployments, g_ptr_array_unref); g_clear_object (&self->booted_deployment); + g_clear_object (&self->staged_deployment); + g_clear_pointer (&self->staged_deployment_data, (GDestroyNotify)g_variant_unref); glnx_release_lock_file (&self->lock); @@ -584,14 +586,14 @@ parse_bootlink (const char *bootlink, return TRUE; } -static char * -get_unlocked_development_path (OstreeDeployment *deployment) +char * +_ostree_sysroot_get_runstate_path (OstreeDeployment *deployment, const char *key) { return g_strdup_printf ("%s%s.%d/%s", _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR, ostree_deployment_get_csum (deployment), ostree_deployment_get_deployserial (deployment), - _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT); + key); } static gboolean @@ -636,9 +638,10 @@ parse_deployment (OstreeSysroot *self, return FALSE; /* See if this is the booted deployment */ + const gboolean root_is_ostree_booted = + (self->ostree_booted && self->root_is_sysroot); const gboolean looking_for_booted_deployment = - (self->ostree_booted && self->root_is_sysroot && - !self->booted_deployment); + (root_is_ostree_booted && !self->booted_deployment); gboolean is_booted_deployment = FALSE; if (looking_for_booted_deployment) { @@ -665,7 +668,8 @@ parse_deployment (OstreeSysroot *self, ostree_deployment_set_origin (ret_deployment, origin); ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_NONE; - g_autofree char *unlocked_development_path = get_unlocked_development_path (ret_deployment); + g_autofree char *unlocked_development_path = + _ostree_sysroot_get_runstate_path (ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT); struct stat stbuf; if (lstat (unlocked_development_path, &stbuf) == 0) ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT; @@ -789,6 +793,60 @@ ensure_repo (OstreeSysroot *self, return TRUE; } +/* Reload the staged deployment from the file in /run */ +gboolean +_ostree_sysroot_reload_staged (OstreeSysroot *self, + GError **error) +{ + GLNX_AUTO_PREFIX_ERROR ("Loading staged deployment", error); + const gboolean root_is_ostree_booted = + self->ostree_booted && self->root_is_sysroot; + if (!root_is_ostree_booted) + return TRUE; /* Note early return */ + + g_assert (self->booted_deployment); + + g_clear_object (&self->staged_deployment); + g_clear_pointer (&self->staged_deployment_data, (GDestroyNotify)g_variant_unref); + + /* Read the staged state from disk */ + glnx_autofd int fd = -1; + if (!ot_openat_ignore_enoent (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED, &fd, error)) + return FALSE; + if (fd != -1) + { + 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); + + /* Parse it */ + g_autoptr(GVariant) target = NULL; + g_autofree char **kargs = NULL; + g_variant_dict_lookup (staged_deployment_dict, "target", "@a{sv}", &target); + g_variant_dict_lookup (staged_deployment_dict, "kargs", "^a&s", &kargs); + if (target) + { + self->staged_deployment = + _ostree_sysroot_deserialize_deployment_from_variant (target, error); + if (!self->staged_deployment) + return FALSE; + _ostree_deployment_set_bootconfig_from_kargs (self->staged_deployment, kargs); + self->staged_deployment_data = g_steal_pointer (&staged_deployment_data); + /* We set this flag for ostree_deployment_is_staged() because that API + * doesn't have access to the sysroot, which currently has the + * canonical "staged_deployment" reference. + */ + self->staged_deployment->staged = TRUE; + } + } + + return TRUE; +} + gboolean ostree_sysroot_load_if_changed (OstreeSysroot *self, gboolean *out_changed, @@ -857,6 +915,7 @@ ostree_sysroot_load_if_changed (OstreeSysroot *self, g_clear_pointer (&self->deployments, g_ptr_array_unref); g_clear_object (&self->booted_deployment); + g_clear_object (&self->staged_deployment); self->bootversion = -1; self->subbootversion = -1; @@ -880,17 +939,23 @@ ostree_sysroot_load_if_changed (OstreeSysroot *self, } } - if (self->ostree_booted && self->root_is_sysroot - && !self->booted_deployment) + const gboolean root_is_ostree_booted = + self->ostree_booted && self->root_is_sysroot; + if (root_is_ostree_booted && !self->booted_deployment) return glnx_throw (error, "Unexpected state: /run/ostree-booted found and in / sysroot but not in a booted deployment"); + /* Ensure the entires are sorted */ g_ptr_array_sort (deployments, compare_deployments_by_boot_loader_version_reversed); + /* And then set their index variables */ for (guint i = 0; i < deployments->len; i++) { OstreeDeployment *deployment = deployments->pdata[i]; ostree_deployment_set_index (deployment, i); } + if (!_ostree_sysroot_reload_staged (self, error)) + return FALSE; + /* Determine whether we're "physical" or not, the first time we initialize */ if (!self->loaded) { @@ -949,6 +1014,20 @@ ostree_sysroot_get_booted_deployment (OstreeSysroot *self) return self->booted_deployment; } +/** + * ostree_sysroot_get_staged_deployment: + * @self: Sysroot + * + * Returns: (transfer none): The currently staged deployment, or %NULL if none + */ +OstreeDeployment * +ostree_sysroot_get_staged_deployment (OstreeSysroot *self) +{ + g_return_val_if_fail (self->loaded, NULL); + + return self->staged_deployment; +} + /** * ostree_sysroot_get_deployments: * @self: Sysroot @@ -1769,7 +1848,8 @@ ostree_sysroot_deployment_unlock (OstreeSysroot *self, break; case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT: { - g_autofree char *devpath = get_unlocked_development_path (deployment); + g_autofree char *devpath = + _ostree_sysroot_get_runstate_path (deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT); g_autofree char *devpath_parent = dirname (g_strdup (devpath)); if (!glnx_shutil_mkdir_p_at (AT_FDCWD, devpath_parent, 0755, cancellable, error)) diff --git a/src/libostree/ostree-sysroot.h b/src/libostree/ostree-sysroot.h index e4763d3755..47cbb022c2 100644 --- a/src/libostree/ostree-sysroot.h +++ b/src/libostree/ostree-sysroot.h @@ -74,6 +74,8 @@ _OSTREE_PUBLIC GPtrArray *ostree_sysroot_get_deployments (OstreeSysroot *self); _OSTREE_PUBLIC OstreeDeployment *ostree_sysroot_get_booted_deployment (OstreeSysroot *self); +_OSTREE_PUBLIC +OstreeDeployment *ostree_sysroot_get_staged_deployment (OstreeSysroot *self); _OSTREE_PUBLIC GFile *ostree_sysroot_get_deployment_directory (OstreeSysroot *self, @@ -174,6 +176,17 @@ gboolean ostree_sysroot_deploy_tree (OstreeSysroot *self, GCancellable *cancellable, GError **error); +_OSTREE_PUBLIC +gboolean ostree_sysroot_stage_tree (OstreeSysroot *self, + const char *osname, + const char *revision, + GKeyFile *origin, + OstreeDeployment *merge_deployment, + char **override_kernel_argv, + OstreeDeployment **out_new_deployment, + GCancellable *cancellable, + GError **error); + _OSTREE_PUBLIC gboolean ostree_sysroot_deployment_set_mutable (OstreeSysroot *self, OstreeDeployment *deployment, diff --git a/src/ostree/ot-admin-builtin-deploy.c b/src/ostree/ot-admin-builtin-deploy.c index d9905212ee..f6c0c16158 100644 --- a/src/ostree/ot-admin-builtin-deploy.c +++ b/src/ostree/ot-admin-builtin-deploy.c @@ -34,6 +34,7 @@ #include static gboolean opt_retain; +static gboolean opt_stage; static gboolean opt_retain_pending; static gboolean opt_retain_rollback; static gboolean opt_not_as_default; @@ -50,6 +51,7 @@ static GOptionEntry options[] = { { "origin-file", 0, 0, G_OPTION_ARG_FILENAME, &opt_origin_path, "Specify origin file", "FILENAME" }, { "no-prune", 0, 0, G_OPTION_ARG_NONE, &opt_no_prune, "Don't prune the repo when done", 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 }, { "retain-pending", 0, 0, G_OPTION_ARG_NONE, &opt_retain_pending, "Do not delete pending deployments", NULL }, { "retain-rollback", 0, 0, G_OPTION_ARG_NONE, &opt_retain_rollback, "Do not delete rollback deployments", NULL }, { "not-as-default", 0, 0, G_OPTION_ARG_NONE, &opt_not_as_default, "Append rather than prepend new deployment", NULL }, @@ -157,31 +159,45 @@ ot_admin_builtin_deploy (int argc, char **argv, OstreeCommandInvocation *invocat g_autoptr(OstreeDeployment) new_deployment = NULL; g_auto(GStrv) kargs_strv = _ostree_kernel_args_to_strv (kargs); - if (!ostree_sysroot_deploy_tree (sysroot, opt_osname, revision, origin, merge_deployment, - kargs_strv, &new_deployment, cancellable, error)) - return FALSE; - - OstreeSysrootSimpleWriteDeploymentFlags flags = OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NO_CLEAN; - if (opt_retain) - flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN; - else + if (opt_stage) { - if (opt_retain_pending) - flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_PENDING; - if (opt_retain_rollback) - flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_ROLLBACK; + if (opt_retain_pending || opt_retain_rollback) + 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"); + if (!ostree_sysroot_stage_tree (sysroot, opt_osname, revision, origin, merge_deployment, + kargs_strv, &new_deployment, cancellable, error)) + return FALSE; } + else + { + if (!ostree_sysroot_deploy_tree (sysroot, opt_osname, revision, origin, merge_deployment, + kargs_strv, &new_deployment, cancellable, error)) + return FALSE; - if (opt_not_as_default) - flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT; - - if (!ostree_sysroot_simple_write_deployment (sysroot, opt_osname, new_deployment, - merge_deployment, flags, cancellable, error)) - return FALSE; + OstreeSysrootSimpleWriteDeploymentFlags flags = OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NO_CLEAN; + if (opt_retain) + flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN; + else + { + if (opt_retain_pending) + flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_PENDING; + if (opt_retain_rollback) + flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_ROLLBACK; + } + + if (opt_not_as_default) + flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT; + + if (!ostree_sysroot_simple_write_deployment (sysroot, opt_osname, new_deployment, + merge_deployment, flags, cancellable, error)) + return FALSE; + } - /* And finally, cleanup of any leftover data. + /* And finally, cleanup of any leftover data. In stage mode, we + * don't do a full cleanup as we didn't touch the bootloader. */ - if (opt_no_prune) + if (opt_no_prune || opt_stage) { if (!ostree_sysroot_prepare_cleanup (sysroot, cancellable, error)) return FALSE; diff --git a/src/ostree/ot-admin-builtin-finalize-staged.c b/src/ostree/ot-admin-builtin-finalize-staged.c new file mode 100644 index 0000000000..6740f82aa9 --- /dev/null +++ b/src/ostree/ot-admin-builtin-finalize-staged.c @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 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, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "config.h" + +#include + +#include "ot-main.h" +#include "ot-admin-builtins.h" +#include "ot-admin-functions.h" +#include "ostree.h" +#include "otutil.h" + +#include "ostree-cmdprivate.h" +#include "ostree.h" + +/* Called by ostree-finalize-staged.service, and in turn + * invokes a cmdprivate function inside the shared library. + */ +gboolean +ot_admin_builtin_finalize_staged (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error) +{ + /* Just a sanity check; we shouldn't be called outside of the service though. + */ + struct stat stbuf; + if (fstatat (AT_FDCWD, "/run/ostree-booted", &stbuf, 0) < 0) + return TRUE; + + g_autoptr(GFile) sysroot_file = g_file_new_for_path ("/"); + g_autoptr(OstreeSysroot) sysroot = ostree_sysroot_new (sysroot_file); + + if (!ostree_sysroot_load (sysroot, cancellable, error)) + return FALSE; + if (!ostree_cmd__private__()->ostree_finalize_staged (sysroot, cancellable, error)) + return FALSE; + + return TRUE; +} diff --git a/src/ostree/ot-admin-builtin-status.c b/src/ostree/ot-admin-builtin-status.c index 096155c688..55be69942c 100644 --- a/src/ostree/ot-admin-builtin-status.c +++ b/src/ostree/ot-admin-builtin-status.c @@ -96,7 +96,9 @@ deployment_print_status (OstreeSysroot *sysroot, GKeyFile *origin = ostree_deployment_get_origin (deployment); const char *deployment_status = ""; - if (is_pending) + if (ostree_deployment_is_staged (deployment)) + deployment_status = " (staged)"; + else if (is_pending) deployment_status = " (pending)"; else if (is_rollback) deployment_status = " (rollback)"; @@ -199,6 +201,16 @@ ot_admin_builtin_status (int argc, char **argv, OstreeCommandInvocation *invocat } else { + OstreeDeployment *staged = ostree_sysroot_get_staged_deployment (sysroot); + if (staged) + { + if (!deployment_print_status (sysroot, repo, staged, + FALSE, FALSE, FALSE, + cancellable, + error)) + return FALSE; + } + for (guint i = 0; i < deployments->len; i++) { OstreeDeployment *deployment = deployments->pdata[i]; diff --git a/src/ostree/ot-admin-builtins.h b/src/ostree/ot-admin-builtins.h index a81f4d62d4..d88fc0b907 100644 --- a/src/ostree/ot-admin-builtins.h +++ b/src/ostree/ot-admin-builtins.h @@ -40,6 +40,7 @@ BUILTINPROTO(undeploy); BUILTINPROTO(deploy); BUILTINPROTO(cleanup); BUILTINPROTO(pin); +BUILTINPROTO(finalize_staged); BUILTINPROTO(unlock); BUILTINPROTO(status); BUILTINPROTO(set_origin); diff --git a/src/ostree/ot-builtin-admin.c b/src/ostree/ot-builtin-admin.c index 1262c5a589..b26eea8157 100644 --- a/src/ostree/ot-builtin-admin.c +++ b/src/ostree/ot-builtin-admin.c @@ -57,6 +57,9 @@ static OstreeCommand admin_subcommands[] = { { "pin", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_pin, "Change the \"pinning\" state of a deployment" }, + { "finalize-staged", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN, + ot_admin_builtin_finalize_staged, + "Internal command to run at shutdown time" }, { "status", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_status, "List deployments" }, diff --git a/tests/installed/destructive.yml b/tests/installed/destructive.yml index 9529c7e980..5bd4d7a7ab 100644 --- a/tests/installed/destructive.yml +++ b/tests/installed/destructive.yml @@ -16,6 +16,10 @@ # Next copy all of the tests/ directory - name: Copy test data synchronize: src=../../ dest=/root/tests/ archive=yes + + # First, the Ansible-based tests + - import_tasks: destructive/staged-deploy.yml + - find: paths: /root/tests/installed/destructive patterns: "itest-*.sh" diff --git a/tests/installed/destructive/staged-deploy.yml b/tests/installed/destructive/staged-deploy.yml new file mode 100644 index 0000000000..bf50467581 --- /dev/null +++ b/tests/installed/destructive/staged-deploy.yml @@ -0,0 +1,24 @@ +# Test the deploy --stage functionality + +- name: Write staged-deploy commit + shell: | + ostree --repo=/ostree/repo commit --parent="${commit}" -b staged-deploy --tree=ref="${commit}" --no-bindings + ostree admin deploy --stage --karg-proc-cmdline --karg=ostreetest=yes staged-deploy + environment: + commit: "{{ rpmostree_status['deployments'][0]['checksum'] }}" +- include_tasks: ../tasks/reboot.yml +- name: Check that deploy-staged service worked + shell: | + # Assert that the previous boot had a journal entry for it + journalctl -b "-1" -u ostree-finalize-staged.service | grep -q -e 'Transaction complete' + # And that we have the new kernel argument + grep -q -e 'ostreetest=yes' /proc/cmdline +- name: Rollback + shell: rpm-ostree rollback +- include_tasks: ../tasks/reboot.yml +- shell: | + ostree refs --delete staged-deploy + rpm-ostree cleanup -rp +# And now we shouldn't have the kernel commandline entry +- name: Check we do not have new kernel cmdline entry + shell: grep -qv -e 'ostreetest=yes' /proc/cmdline diff --git a/tests/installed/tasks/reboot.yml b/tests/installed/tasks/reboot.yml new file mode 100644 index 0000000000..fd07710273 --- /dev/null +++ b/tests/installed/tasks/reboot.yml @@ -0,0 +1,71 @@ +# This file is copied from atomic-host-tests + +# vim: set ft=ansible: +# There is no clean way to restart hosts in ansible. The general issue is that +# the shutdown command may close sshd before ansible has time to "return" from +# the task, even with async & poll. This is due to the fact that asynchronous +# tasks still require a small synchronous bootstrapping script which takes 1 sec +# to complete, during which it is vulnerable to erroring out if sshd dies. +# To mitigate this, we prefix a sleep command before the shutdown so +# ansible has time to move on. For more info on this issue, see: +# https://github.com/ansible/ansible/issues/10616 +# +# The Ansible docs now recommend this combination of tasks to handle reboots +# https://support.ansible.com/hc/en-us/articles/201958037-Reboot-a-server-and-wait-for-it-to-come-back + +# remember the real ansible_host for following local actions +# (otherwise ansible will target the localhost) +- set_fact: + real_ansible_host: "{{ ansible_host }}" + timeout: "{{ cli_reboot_timeout | default('120') }}" + +# Have to account for both because Fedora STR uses the old version of these +# inventory values for some reason. +- when: ansible_port is defined + set_fact: + real_ansible_port: "{{ ansible_port }}" + +- when: ansible_ssh_port is defined + set_fact: + real_ansible_port: "{{ ansible_ssh_port }}" + +- name: Get original bootid + command: cat /proc/sys/kernel/random/boot_id + register: orig_bootid + +- name: restart hosts + when: (not skip_shutdown is defined) or (not skip_shutdown) + shell: sleep 3 && shutdown -r now + async: 1 + poll: 0 + ignore_errors: true + +# NB: The following tasks use local actions, so we need to explicitly ensure +# that they don't use sudo, which may require a password, and is not necessary +# anyway. + +- name: wait for hosts to come back up + local_action: + wait_for host={{ real_ansible_host }} + port={{ real_ansible_port | default('22') }} + state=started + delay=30 + timeout={{ timeout }} + search_regex="OpenSSH" + become: false + +# I'm not sure the retries are even necessary, but I'm keeping them in +- name: Wait until bootid changes + command: cat /proc/sys/kernel/random/boot_id + register: new_bootid + until: new_bootid.stdout != orig_bootid.stdout + retries: 6 + delay: 10 + +# provide an empty iterator when a list is not provided +# http://docs.ansible.com/ansible/playbooks_conditionals.html#loops-and-conditionals +- name: check services have started + service: + name: "{{ item }}" + state: started + with_items: "{{ wait_for_services|default([]) }}"