admin: Add an unlock command, and libostree API

I'm trying to improve the developer experience on OSTree-managed
systems, and I had an epiphany the other day - there's no reason we
have to be absolutely against mutating the current rootfs live.  The
key should be making it easy to rollback/reset to a known good state.

I see this command as useful for two related but distinct workflows:

 - `ostree admin unlock` will assume you're doing "development".  The
   semantics hare are that we mount an overlayfs on `/usr`, but the
   overlay data is in `/var/tmp`, and is thus discarded on reboot.
 - `ostree admin unlock --hotfix` first clones your current deployment,
   then creates an overlayfs over `/usr` persistent
   to this deployment.  Persistent in that now the initramfs switchroot
   tool knows how to mount it as well.  In this model, if you want
   to discard the hotfix, at the moment you roll back/reboot into
   the clone.

Note originally, I tried using `rofiles-fuse` over `/usr` for this,
but then everything immediately explodes because the default (at least
CentOS 7) SELinux policy denies tons of things (including `sshd_t`
access to `fusefs_t`).  Sigh.

So the switch to `overlayfs` came after experimentation.  It still
seems to have some issues...specifically `unix_chkpwd` is broken,
possibly because it's setuid?  Basically I can't ssh in anymore.

But I *can* `rpm -Uvh strace.rpm` which is handy.

NOTE: I haven't tested the hotfix path fully yet, specifically
the initramfs bits.
This commit is contained in:
Colin Walters 2016-03-18 15:32:58 -04:00
parent 0b1d301d81
commit 09238da065
20 changed files with 691 additions and 47 deletions

View File

@ -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

View File

@ -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 \

View File

@ -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

View File

@ -0,0 +1,88 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
<!--
Copyright 2016 Colin Walters <walters@verbum.org>
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.
-->
<refentry id="ostree">
<refentryinfo>
<title>ostree admin unlock</title>
<productname>OSTree</productname>
<authorgroup>
<author>
<contrib>Developer</contrib>
<firstname>Colin</firstname>
<surname>Walters</surname>
<email>walters@verbum.org</email>
</author>
</authorgroup>
</refentryinfo>
<refmeta>
<refentrytitle>ostree admin unlock</refentrytitle>
<manvolnum>1</manvolnum>
</refmeta>
<refnamediv>
<refname>ostree-admin-unlock</refname>
<refpurpose>Prepare the current deployment for hotfix or development</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>ostree admin unlock</command> <arg choice="opt" rep="repeat">OPTIONS</arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para>
Remove the read-only bind mount on <literal>/usr</literal>
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.
</para>
</refsect1>
<refsect1>
<title>Options</title>
<variablelist>
<varlistentry>
<term><option>--hotfix</option></term>
<listitem><para>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 <literal>sshd</literal>. 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).
</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
</refentry>

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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,

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,104 @@
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
*
* Copyright (C) 2016 Colin Walters <walters@verbum.org>
*
* 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 <glib/gi18n.h>
#include <err.h>
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;
}

View File

@ -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. */

View File

@ -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);

View File

@ -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 }
};

View File

@ -26,6 +26,10 @@
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <sys/mount.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/statvfs.h>
#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;
}

View File

@ -22,3 +22,5 @@
#pragma once
int perrorv (const char *format, ...) __attribute__ ((format (printf, 1, 2)));
int path_is_on_readonly_fs (char *path);

View File

@ -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 ();

View File

@ -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