diff --git a/Makefile-man.am b/Makefile-man.am index 615bf0f0..ce7e93cd 100644 --- a/Makefile-man.am +++ b/Makefile-man.am @@ -19,7 +19,17 @@ if ENABLE_MAN -man1_files = ostree.1 ostree-admin-cleanup.1 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.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 ostree-prune.1 ostree-pull-local.1 ostree-pull.1 ostree-refs.1 ostree-remote.1 ostree-reset.1 ostree-rev-parse.1 ostree-show.1 ostree-summary.1 ostree-static-delta.1 ostree-trivial-httpd.1 +man1_files = ostree.1 ostree-admin-cleanup.1 \ +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.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 \ +ostree-prune.1 ostree-pull-local.1 ostree-pull.1 ostree-refs.1 \ +ostree-remote.1 ostree-reset.1 ostree-rev-parse.1 ostree-show.1 \ +ostree-summary.1 ostree-static-delta.1 ostree-trivial-httpd.1 if BUILDOPT_FUSE man1_files += rofiles-fuse.1 diff --git a/Makefile-ostree.am b/Makefile-ostree.am index ff7e372b..0ef5c4ee 100644 --- a/Makefile-ostree.am +++ b/Makefile-ostree.am @@ -66,6 +66,7 @@ ostree_SOURCES += \ src/ostree/ot-admin-builtin-status.c \ src/ostree/ot-admin-builtin-switch.c \ src/ostree/ot-admin-builtin-upgrade.c \ + src/ostree/ot-admin-builtin-unlock.c \ src/ostree/ot-admin-builtins.h \ src/ostree/ot-admin-instutil-builtin-selinux-ensure-labeled.c \ src/ostree/ot-admin-instutil-builtin-set-kargs.c \ diff --git a/buildutil/tap-test b/buildutil/tap-test index 38080bb3..e7914541 100755 --- a/buildutil/tap-test +++ b/buildutil/tap-test @@ -12,7 +12,7 @@ tempdir=$(mktemp -d /var/tmp/tap-test.XXXXXX) touch ${tempdir}/.testtmp function cleanup () { if test -n "${TEST_SKIP_CLEANUP:-}"; then - echo "Skipping cleanup of ${test_tmpdir}" + echo "Skipping cleanup of ${tempdir}" else if test -f ${tempdir}/.test; then rm "${tempdir}" -rf fi diff --git a/man/ostree-admin-unlock.xml b/man/ostree-admin-unlock.xml new file mode 100644 index 00000000..ca02bbde --- /dev/null +++ b/man/ostree-admin-unlock.xml @@ -0,0 +1,88 @@ + + + + + + + + + ostree admin unlock + OSTree + + + + Developer + Colin + Walters + walters@verbum.org + + + + + + ostree admin unlock + 1 + + + + ostree-admin-unlock + Prepare the current deployment for hotfix or development + + + + + ostree admin unlock OPTIONS + + + + + Description + + + Remove the read-only bind mount on /usr + and replace it with a writable overlay filesystem. This + default invocation of "unlock" is intended for + development/testing purposes. All changes in the overlay + are lost on reboot. However, this command also supports + "hotfixes", see below. + + + + + Options + + + + + + If this option is provided, the + current deployment will be cloned as a rollback + target. This option is intended for things like + emergency security updates to userspace components + such as sshd. The semantics here + differ from the default "development" unlock mode + in that reboots will retain any changes (which is what + you likely want for security hotfixes). + + + + + diff --git a/src/libostree/libostree.sym b/src/libostree/libostree.sym index a85f0dbb..c78986e5 100644 --- a/src/libostree/libostree.sym +++ b/src/libostree/libostree.sym @@ -318,4 +318,7 @@ global: ostree_repo_list_refs_ext; ostree_sysroot_init_osname; ostree_sysroot_load_if_changed; + ostree_sysroot_deployment_unlock; + ostree_deployment_get_unlocked; + ostree_deployment_unlocked_state_to_string; } LIBOSTREE_2016.3; diff --git a/src/libostree/ostree-deployment-private.h b/src/libostree/ostree-deployment-private.h index b5ebb957..856a3987 100644 --- a/src/libostree/ostree-deployment-private.h +++ b/src/libostree/ostree-deployment-private.h @@ -24,6 +24,21 @@ G_BEGIN_DECLS +struct _OstreeDeployment +{ + GObject parent_instance; + + int index; /* Global offset */ + char *osname; /* osname */ + char *csum; /* OSTree checksum of tree */ + int deployserial; /* How many times this particular csum appears in deployment list */ + char *bootcsum; /* Checksum of kernel+initramfs */ + int bootserial; /* An integer assigned to this tree per its ${bootcsum} */ + OstreeBootconfigParser *bootconfig; /* Bootloader configuration */ + GKeyFile *origin; /* How to construct an upgraded version of this tree */ + OstreeDeploymentUnlockedState unlocked; /* The unlocked state */ +}; + void _ostree_deployment_set_bootcsum (OstreeDeployment *self, const char *bootcsum); G_END_DECLS diff --git a/src/libostree/ostree-deployment.c b/src/libostree/ostree-deployment.c index 3a80474e..7b93e6cc 100644 --- a/src/libostree/ostree-deployment.c +++ b/src/libostree/ostree-deployment.c @@ -23,20 +23,6 @@ #include "ostree-deployment-private.h" #include "libglnx.h" -struct _OstreeDeployment -{ - GObject parent_instance; - - int index; /* Global offset */ - char *osname; /* osname */ - char *csum; /* OSTree checksum of tree */ - int deployserial; /* How many times this particular csum appears in deployment list */ - char *bootcsum; /* Checksum of kernel+initramfs */ - int bootserial; /* An integer assigned to this tree per its ${bootcsum} */ - OstreeBootconfigParser *bootconfig; /* Bootloader configuration */ - GKeyFile *origin; /* How to construct an upgraded version of this tree */ -}; - typedef GObjectClass OstreeDeploymentClass; G_DEFINE_TYPE (OstreeDeployment, ostree_deployment, G_TYPE_OBJECT) @@ -258,6 +244,7 @@ ostree_deployment_new (int index, self->deployserial = deployserial; self->bootcsum = g_strdup (bootcsum); self->bootserial = bootserial; + self->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_NONE; return self; } @@ -279,3 +266,24 @@ ostree_deployment_get_origin_relpath (OstreeDeployment *self) ostree_deployment_get_csum (self), ostree_deployment_get_deployserial (self)); } + +const char * +ostree_deployment_unlocked_state_to_string (OstreeDeploymentUnlockedState state) +{ + switch (state) + { + case OSTREE_DEPLOYMENT_UNLOCKED_NONE: + return "none"; + case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX: + return "hotfix"; + case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT: + return "development"; + } + g_assert_not_reached (); +} + +OstreeDeploymentUnlockedState +ostree_deployment_get_unlocked (OstreeDeployment *self) +{ + return self->unlocked; +} diff --git a/src/libostree/ostree-deployment.h b/src/libostree/ostree-deployment.h index a474b350..bde0cf37 100644 --- a/src/libostree/ostree-deployment.h +++ b/src/libostree/ostree-deployment.h @@ -78,4 +78,16 @@ OstreeDeployment *ostree_deployment_clone (OstreeDeployment *self); _OSTREE_PUBLIC char *ostree_deployment_get_origin_relpath (OstreeDeployment *self); +typedef enum { + OSTREE_DEPLOYMENT_UNLOCKED_NONE, + OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT, + OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX +} OstreeDeploymentUnlockedState; + +_OSTREE_PUBLIC +const char *ostree_deployment_unlocked_state_to_string (OstreeDeploymentUnlockedState state); + +_OSTREE_PUBLIC +OstreeDeploymentUnlockedState ostree_deployment_get_unlocked (OstreeDeployment *self); + G_END_DECLS diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h index 229893d2..d210a36f 100644 --- a/src/libostree/ostree-sysroot-private.h +++ b/src/libostree/ostree-sysroot-private.h @@ -58,6 +58,9 @@ struct OstreeSysroot { }; #define OSTREE_SYSROOT_LOCKFILE "ostree/lock" +/* We keep some transient state in /run */ +#define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR "/run/ostree/deployment-state/" +#define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT "unlocked-development" gboolean _ostree_sysroot_read_boot_loader_configs (OstreeSysroot *self, diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index aa08838a..6ee3dff9 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -24,6 +24,7 @@ #include "ostree-core-private.h" #include "ostree-sysroot-private.h" +#include "ostree-deployment-private.h" #include "ostree-bootloader-uboot.h" #include "ostree-bootloader-syslinux.h" #include "ostree-bootloader-grub2.h" @@ -646,6 +647,16 @@ parse_bootlink (const char *bootlink, return ret; } +static char * +get_unlocked_development_path (OstreeDeployment *deployment) +{ + 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); +} + static gboolean parse_deployment (OstreeSysroot *self, const char *boot_link, @@ -667,6 +678,8 @@ parse_deployment (OstreeSysroot *self, g_autofree char *treebootserial_target = NULL; g_autofree char *deploy_dir = NULL; GKeyFile *origin = NULL; + g_autofree char *unlocked_development_path = NULL; + struct stat stbuf; if (!ensure_sysroot_fd (self, error)) goto out; @@ -704,6 +717,24 @@ parse_deployment (OstreeSysroot *self, if (origin) ostree_deployment_set_origin (ret_deployment, origin); + ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_NONE; + unlocked_development_path = get_unlocked_development_path (ret_deployment); + if (lstat (unlocked_development_path, &stbuf) == 0) + ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT; + else + { + g_autofree char *existing_unlocked_state = + g_key_file_get_string (origin, "origin", "unlocked", NULL); + + if (g_strcmp0 (existing_unlocked_state, "hotfix") == 0) + { + ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX; + } + /* TODO: warn on unknown unlock types? */ + } + + g_debug ("Deployment %s.%d unlocked=%d", treecsum, deployserial, ret_deployment->unlocked); + ret = TRUE; if (out_deployment) *out_deployment = g_steal_pointer (&ret_deployment); @@ -1481,6 +1512,10 @@ ostree_sysroot_init_osname (OstreeSysroot *self, * * If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN is * specified, then all current deployments will be kept. + * + * If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT is + * specified, then instead of prepending, the new deployment will be + * added right after the booted or merge deployment, instead of first. */ gboolean ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot, @@ -1497,6 +1532,8 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot, g_autoptr(GPtrArray) deployments = NULL; g_autoptr(GPtrArray) new_deployments = g_ptr_array_new_with_free_func (g_object_unref); gboolean retain = (flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN) > 0; + const gboolean make_default = !((flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT) > 0); + gboolean added_new = FALSE; deployments = ostree_sysroot_get_deployments (sysroot); booted_deployment = ostree_sysroot_get_booted_deployment (sysroot); @@ -1504,23 +1541,44 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot, if (osname == NULL && booted_deployment) osname = ostree_deployment_get_osname (booted_deployment); - g_ptr_array_add (new_deployments, g_object_ref (new_deployment)); + if (make_default) + { + g_ptr_array_add (new_deployments, g_object_ref (new_deployment)); + added_new = TRUE; + } for (i = 0; i < deployments->len; i++) { OstreeDeployment *deployment = deployments->pdata[i]; + const gboolean is_merge_or_booted = + ostree_deployment_equal (deployment, booted_deployment) || + ostree_deployment_equal (deployment, merge_deployment); /* Keep deployments with different osnames, as well as the * booted and merge deployments */ if (retain || - (osname != NULL && - strcmp (ostree_deployment_get_osname (deployment), osname) != 0) || - ostree_deployment_equal (deployment, booted_deployment) || - ostree_deployment_equal (deployment, merge_deployment)) + (osname != NULL && strcmp (ostree_deployment_get_osname (deployment), osname) != 0) || + is_merge_or_booted) { g_ptr_array_add (new_deployments, g_object_ref (deployment)); } + + if (!added_new) + { + g_ptr_array_add (new_deployments, g_object_ref (new_deployment)); + added_new = TRUE; + } + } + + /* In this non-default case , an improvement in the future would be + * to put the new deployment right after the current default in the + * order. + */ + if (!added_new) + { + g_ptr_array_add (new_deployments, g_object_ref (new_deployment)); + added_new = TRUE; } if (!ostree_sysroot_write_deployments (sysroot, new_deployments, cancellable, error)) @@ -1533,3 +1591,263 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot, out: return ret; } + +static gboolean +clone_deployment (OstreeSysroot *sysroot, + OstreeDeployment *target_deployment, + OstreeDeployment *merge_deployment, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + __attribute__((cleanup(_ostree_kernel_args_cleanup))) OstreeKernelArgs *kargs = NULL; + glnx_unref_object OstreeDeployment *new_deployment = NULL; + + /* Ensure we have a clean slate */ + if (!ostree_sysroot_prepare_cleanup (sysroot, cancellable, error)) + { + g_prefix_error (error, "Performing initial cleanup: "); + goto out; + } + + kargs = _ostree_kernel_args_new (); + + { OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (merge_deployment); + g_auto(GStrv) previous_args = g_strsplit (ostree_bootconfig_parser_get (bootconfig, "options"), " ", -1); + + _ostree_kernel_args_append_argv (kargs, previous_args); + } + + { + g_auto(GStrv) kargs_strv = _ostree_kernel_args_to_strv (kargs); + + if (!ostree_sysroot_deploy_tree (sysroot, + ostree_deployment_get_osname (target_deployment), + ostree_deployment_get_csum (target_deployment), + ostree_deployment_get_origin (target_deployment), + merge_deployment, + kargs_strv, + &new_deployment, + cancellable, error)) + goto out; + } + + /* Hotfixes push the deployment as rollback target, so it shouldn't + * be the default. + */ + if (!ostree_sysroot_simple_write_deployment (sysroot, ostree_deployment_get_osname (target_deployment), + new_deployment, merge_deployment, + OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT, + cancellable, error)) + goto out; + + ret = TRUE; + out: + return ret; +} + +/** + * ostree_sysroot_deployment_unlock: + * @self: Sysroot + * @deployment: Deployment + * @unlocked_state: Transition to this unlocked state + * @cancellable: Cancellable + * @error: Error + * + * Configure the target deployment @deployment such that it + * is writable. There are multiple modes, essentially differing + * in whether or not any changes persist across reboot. + * + * The `OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX` state is persistent + * across reboots. + */ +gboolean +ostree_sysroot_deployment_unlock (OstreeSysroot *self, + OstreeDeployment *deployment, + OstreeDeploymentUnlockedState unlocked_state, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + OstreeDeploymentUnlockedState current_unlocked = + ostree_deployment_get_unlocked (deployment); + glnx_unref_object OstreeDeployment *deployment_clone = + ostree_deployment_clone (deployment); + glnx_unref_object OstreeDeployment *merge_deployment = NULL; + GKeyFile *origin_clone = ostree_deployment_get_origin (deployment_clone); + const char hotfix_ovl_options[] = "lowerdir=usr,upperdir=.usr-ovl-upper,workdir=.usr-ovl-work"; + const char *ovl_options = NULL; + g_autofree char *deployment_path = NULL; + glnx_fd_close int deployment_dfd = -1; + pid_t mount_child; + + /* This function cannot re-lock */ + g_return_val_if_fail (unlocked_state != OSTREE_DEPLOYMENT_UNLOCKED_NONE, FALSE); + + if (current_unlocked != OSTREE_DEPLOYMENT_UNLOCKED_NONE) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Deployment is already in unlocked state: %s", + ostree_deployment_unlocked_state_to_string (current_unlocked)); + goto out; + } + + merge_deployment = ostree_sysroot_get_merge_deployment (self, ostree_deployment_get_osname (deployment)); + if (!merge_deployment) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "No previous deployment to duplicate"); + goto out; + } + + /* For hotfixes, we push a rollback target */ + if (unlocked_state == OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX) + { + if (!clone_deployment (self, deployment, merge_deployment, cancellable, error)) + goto out; + } + + /* Crack it open */ + if (!ostree_sysroot_deployment_set_mutable (self, deployment, TRUE, + cancellable, error)) + goto out; + + deployment_path = ostree_sysroot_get_deployment_dirpath (self, deployment); + + if (!glnx_opendirat (self->sysroot_fd, deployment_path, TRUE, &deployment_dfd, error)) + goto out; + + switch (unlocked_state) + { + case OSTREE_DEPLOYMENT_UNLOCKED_NONE: + g_assert_not_reached (); + break; + case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX: + { + /* Create the overlayfs directories in the deployment root + * directly for hotfixes. The ostree-prepare-root.c helper + * is also set up to detect and mount these. + */ + if (!glnx_shutil_mkdir_p_at (deployment_dfd, ".usr-ovl-upper", 0755, cancellable, error)) + goto out; + if (!glnx_shutil_mkdir_p_at (deployment_dfd, ".usr-ovl-work", 0755, cancellable, error)) + goto out; + ovl_options = hotfix_ovl_options; + } + break; + case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT: + { + /* We're just doing transient development/hacking? Okay, + * stick the overlayfs bits in /var/tmp. + */ + char *development_ovldir = strdupa ("/var/tmp/ostree-unlock-ovl.XXXXXX"); + const char *development_ovl_upper; + const char *development_ovl_work; + + if (!glnx_mkdtempat (AT_FDCWD, development_ovldir, 0700, error)) + goto out; + + development_ovl_upper = glnx_strjoina (development_ovldir, "/upper"); + if (!glnx_shutil_mkdir_p_at (AT_FDCWD, development_ovl_upper, 0755, cancellable, error)) + goto out; + development_ovl_work = glnx_strjoina (development_ovldir, "/work"); + if (!glnx_shutil_mkdir_p_at (AT_FDCWD, development_ovl_work, 0755, cancellable, error)) + goto out; + ovl_options = glnx_strjoina ("lowerdir=usr,upperdir=", development_ovl_upper, + ",workdir=", development_ovl_work); + } + } + + g_assert (ovl_options != NULL); + + /* Here we run `mount()` in a fork()ed child because we need to use + * `chdir()` in order to have the mount path options to overlayfs not + * look ugly. + * + * We can't `chdir()` inside a shared library since there may be + * threads, etc. + */ + { + /* Make a copy of the fd that's *not* FD_CLOEXEC so that we pass + * it to the child. + */ + glnx_fd_close int child_deployment_dfd = dup (deployment_dfd); + + if (child_deployment_dfd < 0) + { + glnx_set_error_from_errno (error); + goto out; + } + + mount_child = fork (); + if (mount_child < 0) + { + glnx_set_prefix_error_from_errno (error, "%s", "fork"); + goto out; + } + else if (mount_child == 0) + { + /* Child process. Do NOT use any GLib API here. */ + if (fchdir (child_deployment_dfd) < 0) + exit (EXIT_FAILURE); + (void) close (child_deployment_dfd); + if (mount ("overlay", "/usr", "overlay", 0, ovl_options) < 0) + exit (EXIT_FAILURE); + exit (EXIT_SUCCESS); + } + else + { + /* Parent */ + int estatus; + + if (TEMP_FAILURE_RETRY (waitpid (mount_child, &estatus, 0)) < 0) + { + glnx_set_prefix_error_from_errno (error, "%s", "waitpid() on mount helper"); + goto out; + } + if (!g_spawn_check_exit_status (estatus, error)) + { + g_prefix_error (error, "overlayfs mount helper: "); + goto out; + } + } + } + + /* Now, write out the flag saying what we did */ + switch (unlocked_state) + { + case OSTREE_DEPLOYMENT_UNLOCKED_NONE: + g_assert_not_reached (); + break; + case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX: + g_key_file_set_string (origin_clone, "origin", "unlocked", + ostree_deployment_unlocked_state_to_string (unlocked_state)); + if (!ostree_sysroot_write_origin_file (self, deployment, origin_clone, + cancellable, error)) + goto out; + break; + case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT: + { + g_autofree char *devpath = get_unlocked_development_path (deployment); + g_autofree char *devpath_parent = dirname (g_strdup (devpath)); + + if (!glnx_shutil_mkdir_p_at (AT_FDCWD, devpath_parent, 0755, cancellable, error)) + goto out; + + if (!g_file_set_contents (devpath, "", 0, error)) + goto out; + } + } + + /* For hotfixes we already pushed a rollback which will bump the + * mtime, but we need to bump it again so that clients get the state + * change for this deployment. For development we need to do this + * regardless. + */ + if (!_ostree_sysroot_bump_mtime (self, error)) + goto out; + + ret = TRUE; + out: + return ret; +} + diff --git a/src/libostree/ostree-sysroot.h b/src/libostree/ostree-sysroot.h index 077862aa..1e60ddbe 100644 --- a/src/libostree/ostree-sysroot.h +++ b/src/libostree/ostree-sysroot.h @@ -163,6 +163,13 @@ gboolean ostree_sysroot_deployment_set_mutable (OstreeSysroot *self, GCancellable *cancellable, GError **error); +_OSTREE_PUBLIC +gboolean ostree_sysroot_deployment_unlock (OstreeSysroot *self, + OstreeDeployment *deployment, + OstreeDeploymentUnlockedState unlocked_state, + GCancellable *cancellable, + GError **error); + _OSTREE_PUBLIC OstreeDeployment *ostree_sysroot_get_merge_deployment (OstreeSysroot *self, const char *osname); @@ -174,7 +181,8 @@ GKeyFile *ostree_sysroot_origin_new_from_refspec (OstreeSysroot *self, typedef enum { OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NONE = 0, - OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN = (1 << 0) + OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN = (1 << 0), + OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT = (1 << 1) } OstreeSysrootSimpleWriteDeploymentFlags; _OSTREE_PUBLIC diff --git a/src/ostree/ot-admin-builtin-status.c b/src/ostree/ot-admin-builtin-status.c index cd275cc6..df4d0745 100644 --- a/src/ostree/ot-admin-builtin-status.c +++ b/src/ostree/ot-admin-builtin-status.c @@ -89,6 +89,9 @@ ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GErro glnx_unref_object OstreeRepo *repo = NULL; OstreeDeployment *booted_deployment = NULL; g_autoptr(GPtrArray) deployments = NULL; + const int is_tty = isatty (1); + const char *red_bold_prefix = is_tty ? "\x1b[31m\x1b[1m" : ""; + const char *red_bold_suffix = is_tty ? "\x1b[22m\x1b[0m" : ""; guint i; context = g_option_context_new ("List deployments"); @@ -118,12 +121,15 @@ ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GErro OstreeDeployment *deployment = deployments->pdata[i]; GKeyFile *origin; const char *ref = ostree_deployment_get_csum (deployment); + OstreeDeploymentUnlockedState unlocked = ostree_deployment_get_unlocked (deployment); g_autofree char *version = version_of_commit (repo, ref); glnx_unref_object OstreeGpgVerifyResult *result = NULL; GString *output_buffer; guint jj, n_signatures; GError *local_error = NULL; + origin = ostree_deployment_get_origin (deployment); + g_print ("%c %s %s.%d\n", deployment == booted_deployment ? '*' : ' ', ostree_deployment_get_osname (deployment), @@ -131,7 +137,15 @@ ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GErro ostree_deployment_get_deployserial (deployment)); if (version) g_print (" Version: %s\n", version); - origin = ostree_deployment_get_origin (deployment); + switch (unlocked) + { + case OSTREE_DEPLOYMENT_UNLOCKED_NONE: + break; + default: + g_print (" %sUnlocked: %s%s\n", red_bold_prefix, + ostree_deployment_unlocked_state_to_string (unlocked), + red_bold_suffix); + } if (!origin) g_print (" origin: none\n"); else diff --git a/src/ostree/ot-admin-builtin-unlock.c b/src/ostree/ot-admin-builtin-unlock.c new file mode 100644 index 00000000..9d265325 --- /dev/null +++ b/src/ostree/ot-admin-builtin-unlock.c @@ -0,0 +1,104 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2016 Colin Walters + * + * 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 "ot-main.h" +#include "ot-admin-builtins.h" +#include "ot-admin-functions.h" +#include "ostree.h" +#include "otutil.h" + +#include "../libostree/ostree-kernel-args.h" + +#include +#include + +static gboolean opt_hotfix; + +static GOptionEntry options[] = { + { "hotfix", 0, 0, G_OPTION_ARG_NONE, &opt_hotfix, "Keep the current deployment as default", NULL }, + { NULL } +}; + +gboolean +ot_admin_builtin_unlock (int argc, char **argv, GCancellable *cancellable, GError **error) +{ + gboolean ret = FALSE; + GOptionContext *context; + glnx_unref_object OstreeSysroot *sysroot = NULL; + glnx_unref_object OstreeRepo *repo = NULL; + g_autoptr(GPtrArray) new_deployments = NULL; + glnx_unref_object OstreeDeployment *merge_deployment = NULL; + OstreeDeployment *booted_deployment = NULL; + OstreeDeploymentUnlockedState target_state; + + context = g_option_context_new ("Make the current deployment mutable (as a hotfix or development)"); + + if (!ostree_admin_option_context_parse (context, options, &argc, &argv, + OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER, + &sysroot, cancellable, error)) + goto out; + + if (argc > 1) + { + ot_util_usage_error (context, "This command takes no extra arguments", error); + goto out; + } + + if (!ostree_sysroot_load (sysroot, cancellable, error)) + goto out; + + booted_deployment = ostree_sysroot_get_booted_deployment (sysroot); + if (!booted_deployment) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Not currently booted into an OSTree system"); + goto out; + } + + target_state = opt_hotfix ? OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX : OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT; + + if (!ostree_sysroot_deployment_unlock (sysroot, booted_deployment, + target_state, cancellable, error)) + goto out; + + switch (target_state) + { + case OSTREE_DEPLOYMENT_UNLOCKED_NONE: + g_assert_not_reached (); + break; + case OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX: + g_print ("Hotfix mode enabled. A writable overlayfs is now mounted on /usr\n" + "for this booted deployment. A non-hotfixed clone has been created\n" + "as the non-default rollback target.\n"); + break; + case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT: + g_print ("Development mode enabled. A writable overlayfs is now mounted on /usr.\n" + "All changes there will be discarded on reboot.\n"); + break; + } + + ret = TRUE; + out: + if (context) + g_option_context_free (context); + return ret; +} diff --git a/src/ostree/ot-admin-builtin-upgrade.c b/src/ostree/ot-admin-builtin-upgrade.c index 5d4796ac..b3b531c0 100644 --- a/src/ostree/ot-admin-builtin-upgrade.c +++ b/src/ostree/ot-admin-builtin-upgrade.c @@ -97,6 +97,9 @@ ot_admin_builtin_upgrade (int argc, char **argv, GCancellable *cancellable, GErr "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. */ diff --git a/src/ostree/ot-admin-builtins.h b/src/ostree/ot-admin-builtins.h index 1a3c1264..f9425119 100644 --- a/src/ostree/ot-admin-builtins.h +++ b/src/ostree/ot-admin-builtins.h @@ -34,6 +34,7 @@ gboolean ot_admin_builtin_init_fs (int argc, char **argv, GCancellable *cancella gboolean ot_admin_builtin_undeploy (int argc, char **argv, GCancellable *cancellable, GError **error); gboolean ot_admin_builtin_deploy (int argc, char **argv, GCancellable *cancellable, GError **error); gboolean ot_admin_builtin_cleanup (int argc, char **argv, GCancellable *cancellable, GError **error); +gboolean ot_admin_builtin_unlock (int argc, char **argv, GCancellable *cancellable, GError **error); gboolean ot_admin_builtin_status (int argc, char **argv, GCancellable *cancellable, GError **error); gboolean ot_admin_builtin_set_origin (int argc, char **argv, GCancellable *cancellable, GError **error); gboolean ot_admin_builtin_diff (int argc, char **argv, GCancellable *cancellable, GError **error); diff --git a/src/ostree/ot-builtin-admin.c b/src/ostree/ot-builtin-admin.c index a4cb0dde..8b866170 100644 --- a/src/ostree/ot-builtin-admin.c +++ b/src/ostree/ot-builtin-admin.c @@ -47,6 +47,7 @@ static OstreeAdminCommand admin_subcommands[] = { { "status", ot_admin_builtin_status }, { "switch", ot_admin_builtin_switch }, { "undeploy", ot_admin_builtin_undeploy }, + { "unlock", ot_admin_builtin_unlock }, { "upgrade", ot_admin_builtin_upgrade }, { NULL, NULL } }; diff --git a/src/switchroot/ostree-mount-util.c b/src/switchroot/ostree-mount-util.c index c6df559c..daec66c5 100644 --- a/src/switchroot/ostree-mount-util.c +++ b/src/switchroot/ostree-mount-util.c @@ -26,6 +26,10 @@ #include #include #include +#include +#include +#include +#include #include "ostree-mount-util.h" @@ -48,3 +52,18 @@ perrorv (const char *format, ...) return 0; } + +int +path_is_on_readonly_fs (char *path) +{ + struct statvfs stvfsbuf; + + if (statvfs (path, &stvfsbuf) == -1) + { + perrorv ("statvfs(%s): ", path); + exit (EXIT_FAILURE); + } + + return (stvfsbuf.f_flag & ST_RDONLY) != 0; +} + diff --git a/src/switchroot/ostree-mount-util.h b/src/switchroot/ostree-mount-util.h index 63e90c67..475b2cab 100644 --- a/src/switchroot/ostree-mount-util.h +++ b/src/switchroot/ostree-mount-util.h @@ -22,3 +22,5 @@ #pragma once int perrorv (const char *format, ...) __attribute__ ((format (printf, 1, 2))); + +int path_is_on_readonly_fs (char *path); diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c index 3de137bb..375867b1 100644 --- a/src/switchroot/ostree-prepare-root.c +++ b/src/switchroot/ostree-prepare-root.c @@ -111,7 +111,6 @@ touch_run_ostree (void) int main(int argc, char *argv[]) { - const char *readonly_bind_mounts[] = { "/usr", NULL }; const char *root_mountpoint = NULL; char *ostree_target = NULL; char *deploy_path = NULL; @@ -119,7 +118,7 @@ main(int argc, char *argv[]) char destpath[PATH_MAX]; char newroot[PATH_MAX]; struct stat stbuf; - int i; + int orig_cwd_dfd; if (argc < 2) { @@ -211,21 +210,70 @@ main(int argc, char *argv[]) } } - /* Set up any read-only bind mounts (notably /usr) */ - for (i = 0; readonly_bind_mounts[i] != NULL; i++) + /* Here we do a dance to chdir to the newroot so that we can have + * the potential overlayfs mount points not look ugly. However...I + * think we could do this a lot earlier and make all of the mounts + * here just be relative. + */ + orig_cwd_dfd = openat (AT_FDCWD, ".", O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY); + if (orig_cwd_dfd < 0) { - snprintf (destpath, sizeof(destpath), "%s%s", newroot, readonly_bind_mounts[i]); - if (mount (destpath, destpath, NULL, MS_BIND, NULL) < 0) + perrorv ("failed to open ."); + exit (EXIT_FAILURE); + } + + if (chdir (newroot) < 0) + { + perrorv ("failed to chdir to newroot"); + exit (EXIT_FAILURE); + } + + /* Do we have a persistent overlayfs for /usr? If so, mount it now. */ + if (lstat (".usr-ovl-work", &stbuf) == 0) + { + const char usr_ovl_options[] = "lowerdir=usr,upperdir=.usr-ovl-upper,workdir=.usr-ovl-work"; + + /* Except overlayfs barfs if we try to mount it on a read-only + * filesystem. For this use case I think admins are going to be + * okay if we remount the rootfs here, rather than waiting until + * later boot and `systemd-remount-fs.service`. + */ + if (path_is_on_readonly_fs (".")) { - perrorv ("failed to bind mount (class:readonly) %s", destpath); - exit (EXIT_FAILURE); + if (mount (".", ".", NULL, MS_REMOUNT | MS_SILENT, NULL) < 0) + { + perrorv ("Failed to remount rootfs writable (for overlayfs)"); + exit (EXIT_FAILURE); + } } - if (mount (destpath, destpath, NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL) < 0) + + if (mount ("overlay", "usr", "overlay", 0, usr_ovl_options) < 0) { - perrorv ("failed to bind mount (class:readonly) %s", destpath); + perrorv ("failed to mount /usr overlayfs"); exit (EXIT_FAILURE); } } + else + { + /* Otherwise, a read-only bind mount for /usr */ + if (mount ("usr", "usr", NULL, MS_BIND, NULL) < 0) + { + perrorv ("failed to bind mount (class:readonly) /usr"); + exit (EXIT_FAILURE); + } + if (mount ("usr", "usr", NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL) < 0) + { + perrorv ("failed to bind mount (class:readonly) /usr"); + exit (EXIT_FAILURE); + } + } + + if (fchdir (orig_cwd_dfd) < 0) + { + perrorv ("failed to chdir to orig root"); + exit (EXIT_FAILURE); + } + (void) close (orig_cwd_dfd); touch_run_ostree (); diff --git a/src/switchroot/ostree-remount.c b/src/switchroot/ostree-remount.c index b8d3a963..aecaf9a8 100644 --- a/src/switchroot/ostree-remount.c +++ b/src/switchroot/ostree-remount.c @@ -37,20 +37,6 @@ #include "ostree-mount-util.h" -static int -path_is_on_readonly_fs (char *path) -{ - struct statvfs stvfsbuf; - - if (statvfs (path, &stvfsbuf) == -1) - { - perrorv ("statvfs(%s): ", path); - exit (EXIT_FAILURE); - } - - return (stvfsbuf.f_flag & ST_RDONLY) != 0; -} - /* Having a writeable /var is necessary for full system functioning. * If /var isn't writeable, we mount tmpfs over it. While this is * somewhat outside of ostree's scope, having all /var twiddling