2257 lines
74 KiB
C
2257 lines
74 KiB
C
/*
|
|
* Copyright (C) 2013 Colin Walters <walters@verbum.org>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.0+
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "otutil.h"
|
|
#include <sys/file.h>
|
|
#include <sys/mount.h>
|
|
#include <err.h>
|
|
#include <sys/wait.h>
|
|
|
|
#include "ostree.h"
|
|
#include "ostree-core-private.h"
|
|
#include "ostree-repo-private.h"
|
|
#include "ostree-sepolicy-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"
|
|
#include "ostree-bootloader-zipl.h"
|
|
|
|
/**
|
|
* SECTION:ostree-sysroot
|
|
* @title: Root partition mount point
|
|
* @short_description: Manage physical root filesystem
|
|
*
|
|
* A #OstreeSysroot object represents a physical root filesystem,
|
|
* which in particular should contain a toplevel /ostree directory.
|
|
* Inside this directory is an #OstreeRepo in /ostree/repo, plus a set
|
|
* of deployments in /ostree/deploy.
|
|
*
|
|
* This class is not by default safe against concurrent use by threads
|
|
* or external processes. You can use ostree_sysroot_lock() to
|
|
* perform locking externally.
|
|
*/
|
|
typedef struct {
|
|
GObjectClass parent_class;
|
|
|
|
/* Signals */
|
|
void (*journal_msg) (OstreeSysroot *sysroot,
|
|
const char *msg);
|
|
} OstreeSysrootClass;
|
|
|
|
enum {
|
|
JOURNAL_MSG_SIGNAL,
|
|
LAST_SIGNAL,
|
|
};
|
|
static guint signals[LAST_SIGNAL] = { 0 };
|
|
|
|
enum {
|
|
PROP_0,
|
|
|
|
PROP_PATH
|
|
};
|
|
|
|
G_DEFINE_TYPE (OstreeSysroot, ostree_sysroot, G_TYPE_OBJECT)
|
|
|
|
static void
|
|
ostree_sysroot_finalize (GObject *object)
|
|
{
|
|
OstreeSysroot *self = OSTREE_SYSROOT (object);
|
|
|
|
g_clear_object (&self->path);
|
|
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);
|
|
|
|
ostree_sysroot_unload (self);
|
|
|
|
G_OBJECT_CLASS (ostree_sysroot_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
ostree_sysroot_set_property(GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
OstreeSysroot *self = OSTREE_SYSROOT (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_PATH:
|
|
self->path = g_value_dup_object (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
ostree_sysroot_get_property(GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
OstreeSysroot *self = OSTREE_SYSROOT (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_PATH:
|
|
g_value_set_object (value, self->path);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
ostree_sysroot_constructed (GObject *object)
|
|
{
|
|
OstreeSysroot *self = OSTREE_SYSROOT (object);
|
|
|
|
/* Ensure the system root path is set. */
|
|
if (self->path == NULL)
|
|
self->path = g_object_ref (_ostree_get_default_sysroot_path ());
|
|
|
|
G_OBJECT_CLASS (ostree_sysroot_parent_class)->constructed (object);
|
|
}
|
|
|
|
static void
|
|
ostree_sysroot_class_init (OstreeSysrootClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->constructed = ostree_sysroot_constructed;
|
|
object_class->get_property = ostree_sysroot_get_property;
|
|
object_class->set_property = ostree_sysroot_set_property;
|
|
object_class->finalize = ostree_sysroot_finalize;
|
|
|
|
g_object_class_install_property (object_class,
|
|
PROP_PATH,
|
|
g_param_spec_object ("path",
|
|
"",
|
|
"",
|
|
G_TYPE_FILE,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
/**
|
|
* OstreeSysroot::journal-msg:
|
|
* @self: Self
|
|
* @msg: Human-readable string (should not contain newlines)
|
|
*
|
|
* libostree will log to the journal various events, such as the /etc merge
|
|
* status, and transaction completion. Connect to this signal to also
|
|
* synchronously receive the text for those messages. This is intended to be
|
|
* used by command line tools which link to libostree as a library.
|
|
*
|
|
* Currently, the structured data is only available via the systemd journal.
|
|
*
|
|
* Since: 2017.10
|
|
*/
|
|
signals[JOURNAL_MSG_SIGNAL] =
|
|
g_signal_new ("journal-msg",
|
|
G_OBJECT_CLASS_TYPE (object_class),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET (OstreeSysrootClass, journal_msg),
|
|
NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
|
|
}
|
|
|
|
static void
|
|
ostree_sysroot_init (OstreeSysroot *self)
|
|
{
|
|
const GDebugKey globalopt_keys[] = {
|
|
{ "skip-sync", OSTREE_SYSROOT_GLOBAL_OPT_SKIP_SYNC },
|
|
};
|
|
const GDebugKey keys[] = {
|
|
{ "mutable-deployments", OSTREE_SYSROOT_DEBUG_MUTABLE_DEPLOYMENTS },
|
|
{ "test-fifreeze", OSTREE_SYSROOT_DEBUG_TEST_FIFREEZE },
|
|
{ "no-xattrs", OSTREE_SYSROOT_DEBUG_NO_XATTRS },
|
|
{ "no-dtb", OSTREE_SYSROOT_DEBUG_TEST_NO_DTB },
|
|
};
|
|
|
|
self->opt_flags = g_parse_debug_string (g_getenv ("OSTREE_SYSROOT_OPTS"),
|
|
globalopt_keys, G_N_ELEMENTS (globalopt_keys));
|
|
self->debug_flags = g_parse_debug_string (g_getenv ("OSTREE_SYSROOT_DEBUG"),
|
|
keys, G_N_ELEMENTS (keys));
|
|
|
|
self->sysroot_fd = -1;
|
|
self->boot_fd = -1;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_new:
|
|
* @path: (allow-none): Path to a system root directory, or %NULL to use the
|
|
* current visible root file system
|
|
*
|
|
* Create a new #OstreeSysroot object for the sysroot at @path. If @path is %NULL,
|
|
* the current visible root file system is used, equivalent to
|
|
* ostree_sysroot_new_default().
|
|
*
|
|
* Returns: (transfer full): An accessor object for an system root located at @path
|
|
*/
|
|
OstreeSysroot*
|
|
ostree_sysroot_new (GFile *path)
|
|
{
|
|
return g_object_new (OSTREE_TYPE_SYSROOT, "path", path, NULL);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_new_default:
|
|
*
|
|
* Returns: (transfer full): An accessor for the current visible root / filesystem
|
|
*/
|
|
OstreeSysroot*
|
|
ostree_sysroot_new_default (void)
|
|
{
|
|
return ostree_sysroot_new (NULL);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_set_mount_namespace_in_use:
|
|
*
|
|
* If this function is invoked, then libostree will assume that
|
|
* a private Linux mount namespace has been created by the process.
|
|
* The primary use case for this is to have e.g. /sysroot mounted
|
|
* read-only by default.
|
|
*
|
|
* If this function has been called, then when a function which requires
|
|
* writable access is invoked, libostree will automatically remount as writable
|
|
* any mount points on which it operates. This currently is just `/sysroot` and
|
|
* `/boot`.
|
|
*
|
|
* If you invoke this function, it must be before ostree_sysroot_load(); it may
|
|
* be invoked before or after ostree_sysroot_initialize().
|
|
*
|
|
* Since: 2020.1
|
|
*/
|
|
void
|
|
ostree_sysroot_set_mount_namespace_in_use (OstreeSysroot *self)
|
|
{
|
|
/* Must be before we're loaded, as otherwise we'd have to close/reopen all our
|
|
fds, e.g. the repo */
|
|
g_return_if_fail (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_LOADED);
|
|
self->mount_namespace_in_use = TRUE;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_get_path:
|
|
* @self: Sysroot
|
|
*
|
|
* Returns: (transfer none) (not nullable): Path to rootfs
|
|
*/
|
|
GFile *
|
|
ostree_sysroot_get_path (OstreeSysroot *self)
|
|
{
|
|
return self->path;
|
|
}
|
|
|
|
/* Open a directory file descriptor for the sysroot if we haven't yet */
|
|
static gboolean
|
|
ensure_sysroot_fd (OstreeSysroot *self,
|
|
GError **error)
|
|
{
|
|
if (self->sysroot_fd == -1)
|
|
{
|
|
if (!glnx_opendirat (AT_FDCWD, gs_file_get_path_cached (self->path), TRUE,
|
|
&self->sysroot_fd, error))
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
_ostree_sysroot_ensure_boot_fd (OstreeSysroot *self, GError **error)
|
|
{
|
|
if (self->boot_fd == -1)
|
|
{
|
|
if (!glnx_opendirat (self->sysroot_fd, "boot", TRUE,
|
|
&self->boot_fd, error))
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
remount_writable (const char *path, gboolean *did_remount, GError **error)
|
|
{
|
|
*did_remount = FALSE;
|
|
struct statvfs stvfsbuf;
|
|
if (statvfs (path, &stvfsbuf) < 0)
|
|
{
|
|
if (errno != ENOENT)
|
|
return glnx_throw_errno_prefix (error, "statvfs(%s)", path);
|
|
else
|
|
return TRUE;
|
|
}
|
|
|
|
if ((stvfsbuf.f_flag & ST_RDONLY) != 0)
|
|
{
|
|
/* OK, let's remount writable. */
|
|
if (mount (path, path, NULL, MS_REMOUNT | MS_RELATIME, "") < 0)
|
|
return glnx_throw_errno_prefix (error, "Remounting %s read-write", path);
|
|
*did_remount = TRUE;
|
|
g_debug ("remounted %s writable", path);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Remount /sysroot read-write if necessary */
|
|
gboolean
|
|
_ostree_sysroot_ensure_writable (OstreeSysroot *self,
|
|
GError **error)
|
|
{
|
|
/* Do nothing if no mount namespace is in use */
|
|
if (!self->mount_namespace_in_use)
|
|
return TRUE;
|
|
|
|
/* If a mount namespace is in use, ensure we're initialized */
|
|
if (!ostree_sysroot_initialize (self, error))
|
|
return FALSE;
|
|
|
|
/* If we aren't operating on a booted system, then we don't
|
|
* do anything with mounts.
|
|
*/
|
|
if (!self->root_is_ostree_booted)
|
|
return TRUE;
|
|
|
|
/* In these cases we also require /boot */
|
|
if (!_ostree_sysroot_ensure_boot_fd (self, error))
|
|
return FALSE;
|
|
|
|
gboolean did_remount_sysroot = FALSE;
|
|
if (!remount_writable ("/sysroot", &did_remount_sysroot, error))
|
|
return FALSE;
|
|
gboolean did_remount_boot = FALSE;
|
|
if (!remount_writable ("/boot", &did_remount_boot, error))
|
|
return FALSE;
|
|
|
|
/* Now close and reopen our file descriptors */
|
|
ostree_sysroot_unload (self);
|
|
if (!ensure_sysroot_fd (self, error))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_get_fd:
|
|
* @self: Sysroot
|
|
*
|
|
* Access a file descriptor that refers to the root directory of this sysroot.
|
|
* ostree_sysroot_initialize() (or ostree_sysroot_load()) must have been invoked
|
|
* prior to calling this function.
|
|
*
|
|
* Returns: A file descriptor valid for the lifetime of @self
|
|
*/
|
|
int
|
|
ostree_sysroot_get_fd (OstreeSysroot *self)
|
|
{
|
|
g_return_val_if_fail (self->sysroot_fd != -1, -1);
|
|
return self->sysroot_fd;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_is_booted:
|
|
* @self: Sysroot
|
|
*
|
|
* Can only be invoked after `ostree_sysroot_initialize()`.
|
|
*
|
|
* Returns: %TRUE iff the sysroot points to a booted deployment
|
|
* Since: 2020.1
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_is_booted (OstreeSysroot *self)
|
|
{
|
|
g_return_val_if_fail (self->loadstate >= OSTREE_SYSROOT_LOAD_STATE_INIT, FALSE);
|
|
return self->root_is_ostree_booted;
|
|
}
|
|
|
|
gboolean
|
|
_ostree_sysroot_bump_mtime (OstreeSysroot *self,
|
|
GError **error)
|
|
{
|
|
/* Allow other systems to monitor for changes */
|
|
if (utimensat (self->sysroot_fd, "ostree/deploy", NULL, 0) < 0)
|
|
{
|
|
glnx_set_prefix_error_from_errno (error, "%s", "futimens");
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_unload:
|
|
* @self: Sysroot
|
|
*
|
|
* Release any resources such as file descriptors referring to the
|
|
* root directory of this sysroot. Normally, those resources are
|
|
* cleared by finalization, but in garbage collected languages that
|
|
* may not be predictable.
|
|
*
|
|
* This undoes the effect of `ostree_sysroot_load()`.
|
|
*/
|
|
void
|
|
ostree_sysroot_unload (OstreeSysroot *self)
|
|
{
|
|
glnx_close_fd (&self->sysroot_fd);
|
|
glnx_close_fd (&self->boot_fd);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_ensure_initialized:
|
|
* @self: Sysroot
|
|
* @cancellable: Cancellable
|
|
* @error: Error
|
|
*
|
|
* Ensure that @self is set up as a valid rootfs, by creating
|
|
* /ostree/repo, among other things.
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_ensure_initialized (OstreeSysroot *self,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
if (!ensure_sysroot_fd (self, error))
|
|
return FALSE;
|
|
|
|
if (!glnx_shutil_mkdir_p_at (self->sysroot_fd, "ostree/repo", 0755,
|
|
cancellable, error))
|
|
return FALSE;
|
|
|
|
if (!glnx_shutil_mkdir_p_at (self->sysroot_fd, "ostree/deploy", 0755,
|
|
cancellable, error))
|
|
return FALSE;
|
|
|
|
g_autoptr(OstreeRepo) repo =
|
|
ostree_repo_create_at (self->sysroot_fd, "ostree/repo",
|
|
OSTREE_REPO_MODE_BARE, NULL,
|
|
cancellable, error);
|
|
if (!repo)
|
|
return FALSE;
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
_ostree_sysroot_emit_journal_msg (OstreeSysroot *self,
|
|
const char *msg)
|
|
{
|
|
g_signal_emit (self, signals[JOURNAL_MSG_SIGNAL], 0, msg);
|
|
}
|
|
|
|
gboolean
|
|
_ostree_sysroot_parse_deploy_path_name (const char *name,
|
|
char **out_csum,
|
|
int *out_serial,
|
|
GError **error)
|
|
{
|
|
|
|
static gsize regex_initialized;
|
|
static GRegex *regex;
|
|
if (g_once_init_enter (®ex_initialized))
|
|
{
|
|
regex = g_regex_new ("^([0-9a-f]+)\\.([0-9]+)$", 0, 0, NULL);
|
|
g_assert (regex);
|
|
g_once_init_leave (®ex_initialized, 1);
|
|
}
|
|
|
|
g_autoptr(GMatchInfo) match = NULL;
|
|
if (!g_regex_match (regex, name, 0, &match))
|
|
return glnx_throw (error, "Invalid deploy name '%s', expected CHECKSUM.TREESERIAL", name);
|
|
|
|
g_autofree char *serial_str = g_match_info_fetch (match, 2);
|
|
*out_csum = g_match_info_fetch (match, 1);
|
|
*out_serial = (int)g_ascii_strtoll (serial_str, NULL, 10);
|
|
return TRUE;
|
|
}
|
|
|
|
/* For a given bootversion, get its subbootversion from `/ostree/boot.$bootversion`. */
|
|
gboolean
|
|
_ostree_sysroot_read_current_subbootversion (OstreeSysroot *self,
|
|
int bootversion,
|
|
int *out_subbootversion,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
if (!ensure_sysroot_fd (self, error))
|
|
return FALSE;
|
|
|
|
g_autofree char *ostree_bootdir_name = g_strdup_printf ("ostree/boot.%d", bootversion);
|
|
struct stat stbuf;
|
|
if (!glnx_fstatat_allow_noent (self->sysroot_fd, ostree_bootdir_name, &stbuf, AT_SYMLINK_NOFOLLOW, error))
|
|
return FALSE;
|
|
if (errno == ENOENT)
|
|
{
|
|
g_debug ("Didn't find $sysroot/ostree/boot.%d symlink; assuming subbootversion 0", bootversion);
|
|
*out_subbootversion = 0;
|
|
}
|
|
else
|
|
{
|
|
g_autofree char *current_subbootdir_name =
|
|
glnx_readlinkat_malloc (self->sysroot_fd, ostree_bootdir_name,
|
|
cancellable, error);
|
|
if (!current_subbootdir_name)
|
|
return FALSE;
|
|
|
|
if (g_str_has_suffix (current_subbootdir_name, ".0"))
|
|
*out_subbootversion = 0;
|
|
else if (g_str_has_suffix (current_subbootdir_name, ".1"))
|
|
*out_subbootversion = 1;
|
|
else
|
|
return glnx_throw (error, "Invalid target '%s' in %s",
|
|
current_subbootdir_name, ostree_bootdir_name);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gint
|
|
compare_boot_loader_configs (OstreeBootconfigParser *a,
|
|
OstreeBootconfigParser *b)
|
|
{
|
|
const char *a_version = ostree_bootconfig_parser_get (a, "version");
|
|
const char *b_version = ostree_bootconfig_parser_get (b, "version");
|
|
|
|
if (a_version && b_version)
|
|
{
|
|
int r = strverscmp (a_version, b_version);
|
|
/* Reverse */
|
|
return -r;
|
|
}
|
|
else if (a_version)
|
|
return -1;
|
|
else
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
compare_loader_configs_for_sorting (gconstpointer a_pp,
|
|
gconstpointer b_pp)
|
|
{
|
|
OstreeBootconfigParser *a = *((OstreeBootconfigParser**)a_pp);
|
|
OstreeBootconfigParser *b = *((OstreeBootconfigParser**)b_pp);
|
|
|
|
return compare_boot_loader_configs (a, b);
|
|
}
|
|
|
|
/* Read all the bootconfigs from `/boot/loader/`. */
|
|
gboolean
|
|
_ostree_sysroot_read_boot_loader_configs (OstreeSysroot *self,
|
|
int bootversion,
|
|
GPtrArray **out_loader_configs,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
if (!ensure_sysroot_fd (self, error))
|
|
return FALSE;
|
|
|
|
g_autoptr(GPtrArray) ret_loader_configs =
|
|
g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
|
|
|
|
g_autofree char *entries_path = g_strdup_printf ("boot/loader.%d/entries", bootversion);
|
|
gboolean entries_exists;
|
|
g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
|
|
if (!ot_dfd_iter_init_allow_noent (self->sysroot_fd, entries_path,
|
|
&dfd_iter, &entries_exists, error))
|
|
return FALSE;
|
|
if (!entries_exists)
|
|
{
|
|
/* Note early return */
|
|
*out_loader_configs = g_steal_pointer (&ret_loader_configs);
|
|
return TRUE;
|
|
}
|
|
|
|
while (TRUE)
|
|
{
|
|
struct dirent *dent;
|
|
struct stat stbuf;
|
|
|
|
if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error))
|
|
return FALSE;
|
|
if (dent == NULL)
|
|
break;
|
|
|
|
if (!glnx_fstatat (dfd_iter.fd, dent->d_name, &stbuf, 0, error))
|
|
return FALSE;
|
|
|
|
if (g_str_has_prefix (dent->d_name, "ostree-") &&
|
|
g_str_has_suffix (dent->d_name, ".conf") &&
|
|
S_ISREG (stbuf.st_mode))
|
|
{
|
|
g_autoptr(OstreeBootconfigParser) config = ostree_bootconfig_parser_new ();
|
|
|
|
if (!ostree_bootconfig_parser_parse_at (config, dfd_iter.fd, dent->d_name, cancellable, error))
|
|
return glnx_prefix_error (error, "Parsing %s", dent->d_name);
|
|
|
|
g_ptr_array_add (ret_loader_configs, g_object_ref (config));
|
|
}
|
|
}
|
|
|
|
/* Callers expect us to give them a sorted array */
|
|
g_ptr_array_sort (ret_loader_configs, compare_loader_configs_for_sorting);
|
|
ot_transfer_out_value(out_loader_configs, &ret_loader_configs);
|
|
return TRUE;
|
|
}
|
|
|
|
/* Get the bootversion from the `/boot/loader` symlink. */
|
|
static gboolean
|
|
read_current_bootversion (OstreeSysroot *self,
|
|
int *out_bootversion,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
int ret_bootversion;
|
|
struct stat stbuf;
|
|
|
|
if (!glnx_fstatat_allow_noent (self->sysroot_fd, "boot/loader", &stbuf, AT_SYMLINK_NOFOLLOW, error))
|
|
return FALSE;
|
|
if (errno == ENOENT)
|
|
{
|
|
g_debug ("Didn't find $sysroot/boot/loader symlink; assuming bootversion 0");
|
|
ret_bootversion = 0;
|
|
}
|
|
else
|
|
{
|
|
if (!S_ISLNK (stbuf.st_mode))
|
|
return glnx_throw (error, "Not a symbolic link: boot/loader");
|
|
|
|
g_autofree char *target =
|
|
glnx_readlinkat_malloc (self->sysroot_fd, "boot/loader", cancellable, error);
|
|
if (!target)
|
|
return FALSE;
|
|
if (g_strcmp0 (target, "loader.0") == 0)
|
|
ret_bootversion = 0;
|
|
else if (g_strcmp0 (target, "loader.1") == 0)
|
|
ret_bootversion = 1;
|
|
else
|
|
return glnx_throw (error, "Invalid target '%s' in boot/loader", target);
|
|
}
|
|
|
|
*out_bootversion = ret_bootversion;
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
load_origin (OstreeSysroot *self,
|
|
OstreeDeployment *deployment,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
g_autofree char *origin_path = ostree_deployment_get_origin_relpath (deployment);
|
|
|
|
glnx_autofd int fd = -1;
|
|
if (!ot_openat_ignore_enoent (self->sysroot_fd, origin_path, &fd, error))
|
|
return FALSE;
|
|
if (fd >= 0)
|
|
{
|
|
g_autofree char *origin_contents =
|
|
glnx_fd_readall_utf8 (fd, NULL, cancellable, error);
|
|
if (!origin_contents)
|
|
return FALSE;
|
|
|
|
g_autoptr(GKeyFile) origin = g_key_file_new ();
|
|
if (!g_key_file_load_from_data (origin, origin_contents, -1, 0, error))
|
|
return glnx_prefix_error (error, "Parsing %s", origin_path);
|
|
|
|
ostree_deployment_set_origin (deployment, origin);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
parse_bootlink (const char *bootlink,
|
|
int *out_entry_bootversion,
|
|
char **out_osname,
|
|
char **out_bootcsum,
|
|
int *out_treebootserial,
|
|
GError **error)
|
|
{
|
|
static gsize regex_initialized;
|
|
static GRegex *regex;
|
|
if (g_once_init_enter (®ex_initialized))
|
|
{
|
|
regex = g_regex_new ("^/ostree/boot.([01])/([^/]+)/([^/]+)/([0-9]+)$", 0, 0, NULL);
|
|
g_assert (regex);
|
|
g_once_init_leave (®ex_initialized, 1);
|
|
}
|
|
|
|
g_autoptr(GMatchInfo) match = NULL;
|
|
if (!g_regex_match (regex, bootlink, 0, &match))
|
|
return glnx_throw (error, "Invalid ostree= argument '%s', expected ostree=/ostree/boot.BOOTVERSION/OSNAME/BOOTCSUM/TREESERIAL", bootlink);
|
|
|
|
g_autofree char *bootversion_str = g_match_info_fetch (match, 1);
|
|
g_autofree char *treebootserial_str = g_match_info_fetch (match, 4);
|
|
*out_entry_bootversion = (int)g_ascii_strtoll (bootversion_str, NULL, 10);
|
|
*out_osname = g_match_info_fetch (match, 2);
|
|
*out_bootcsum = g_match_info_fetch (match, 3);
|
|
*out_treebootserial = (int)g_ascii_strtoll (treebootserial_str, NULL, 10);
|
|
return TRUE;
|
|
}
|
|
|
|
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),
|
|
key);
|
|
}
|
|
|
|
static gboolean
|
|
parse_deployment (OstreeSysroot *self,
|
|
const char *boot_link,
|
|
OstreeDeployment **out_deployment,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
if (!ensure_sysroot_fd (self, error))
|
|
return FALSE;
|
|
|
|
int entry_boot_version;
|
|
g_autofree char *osname = NULL;
|
|
g_autofree char *bootcsum = NULL;
|
|
int treebootserial = -1;
|
|
if (!parse_bootlink (boot_link, &entry_boot_version,
|
|
&osname, &bootcsum, &treebootserial,
|
|
error))
|
|
return FALSE;
|
|
|
|
g_autofree char *errprefix =
|
|
g_strdup_printf ("Parsing deployment %s in stateroot '%s'", boot_link, osname);
|
|
GLNX_AUTO_PREFIX_ERROR(errprefix, error);
|
|
|
|
const char *relative_boot_link = boot_link;
|
|
if (*relative_boot_link == '/')
|
|
relative_boot_link++;
|
|
|
|
g_autofree char *treebootserial_target =
|
|
glnx_readlinkat_malloc (self->sysroot_fd, relative_boot_link,
|
|
cancellable, error);
|
|
if (!treebootserial_target)
|
|
return FALSE;
|
|
|
|
const char *deploy_basename = glnx_basename (treebootserial_target);
|
|
g_autofree char *treecsum = NULL;
|
|
int deployserial = -1;
|
|
if (!_ostree_sysroot_parse_deploy_path_name (deploy_basename,
|
|
&treecsum, &deployserial, error))
|
|
return FALSE;
|
|
|
|
glnx_autofd int deployment_dfd = -1;
|
|
if (!glnx_opendirat (self->sysroot_fd, relative_boot_link, TRUE,
|
|
&deployment_dfd, error))
|
|
return FALSE;
|
|
|
|
/* See if this is the booted deployment */
|
|
const gboolean looking_for_booted_deployment =
|
|
(self->root_is_ostree_booted && !self->booted_deployment);
|
|
gboolean is_booted_deployment = FALSE;
|
|
if (looking_for_booted_deployment)
|
|
{
|
|
struct stat stbuf;
|
|
if (!glnx_fstat (deployment_dfd, &stbuf, error))
|
|
return FALSE;
|
|
/* A bit ugly, we're assigning to a sysroot-owned variable from deep in
|
|
* this parsing code. But eh, if something fails the sysroot state can't
|
|
* be relied on anyways.
|
|
*/
|
|
is_booted_deployment = (stbuf.st_dev == self->root_device &&
|
|
stbuf.st_ino == self->root_inode);
|
|
}
|
|
|
|
g_autoptr(OstreeDeployment) ret_deployment
|
|
= ostree_deployment_new (-1, osname, treecsum, deployserial,
|
|
bootcsum, treebootserial);
|
|
if (!load_origin (self, ret_deployment, cancellable, error))
|
|
return FALSE;
|
|
|
|
ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_NONE;
|
|
g_autofree char *unlocked_development_path =
|
|
_ostree_sysroot_get_runstate_path (ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT);
|
|
g_autofree char *unlocked_transient_path =
|
|
_ostree_sysroot_get_runstate_path (ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_TRANSIENT);
|
|
struct stat stbuf;
|
|
if (lstat (unlocked_development_path, &stbuf) == 0)
|
|
ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT;
|
|
else if (lstat (unlocked_transient_path, &stbuf) == 0)
|
|
ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_TRANSIENT;
|
|
else
|
|
{
|
|
GKeyFile *origin = ostree_deployment_get_origin (ret_deployment);
|
|
g_autofree char *existing_unlocked_state = origin ?
|
|
g_key_file_get_string (origin, "origin", "unlocked", NULL) : 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);
|
|
|
|
if (is_booted_deployment)
|
|
self->booted_deployment = g_object_ref (ret_deployment);
|
|
if (out_deployment)
|
|
*out_deployment = g_steal_pointer (&ret_deployment);
|
|
return TRUE;
|
|
}
|
|
|
|
/* Given a bootloader config, return the value part of the ostree= kernel
|
|
* argument.
|
|
*/
|
|
static char *
|
|
get_ostree_kernel_arg_from_config (OstreeBootconfigParser *config)
|
|
{
|
|
const char *options = ostree_bootconfig_parser_get (config, "options");
|
|
if (!options)
|
|
return NULL;
|
|
|
|
g_auto(GStrv) opts = g_strsplit (options, " ", -1);
|
|
for (char **iter = opts; *iter; iter++)
|
|
{
|
|
const char *opt = *iter;
|
|
if (g_str_has_prefix (opt, "ostree="))
|
|
return g_strdup (opt + strlen ("ostree="));
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* From a BLS config, use its ostree= karg to find the deployment it points to and add it to
|
|
* the inout_deployments array. */
|
|
static gboolean
|
|
list_deployments_process_one_boot_entry (OstreeSysroot *self,
|
|
OstreeBootconfigParser *config,
|
|
GPtrArray *inout_deployments,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
g_autofree char *ostree_arg = get_ostree_kernel_arg_from_config (config);
|
|
if (ostree_arg == NULL)
|
|
return glnx_throw (error, "No ostree= kernel argument found");
|
|
|
|
g_autoptr(OstreeDeployment) deployment = NULL;
|
|
if (!parse_deployment (self, ostree_arg, &deployment,
|
|
cancellable, error))
|
|
return FALSE;
|
|
|
|
ostree_deployment_set_bootconfig (deployment, config);
|
|
char **overlay_initrds = ostree_bootconfig_parser_get_overlay_initrds (config);
|
|
g_autoptr(GPtrArray) initrds_chksums = NULL;
|
|
for (char **it = overlay_initrds; it && *it; it++)
|
|
{
|
|
const char *basename = glnx_basename (*it);
|
|
if (strlen (basename) != (_OSTREE_SHA256_STRING_LEN + strlen (".img")))
|
|
return glnx_throw (error, "Malformed overlay initrd filename: %s", basename);
|
|
|
|
if (!initrds_chksums) /* lazy init */
|
|
initrds_chksums = g_ptr_array_new_full (g_strv_length (overlay_initrds), g_free);
|
|
g_ptr_array_add (initrds_chksums, g_strndup (basename, _OSTREE_SHA256_STRING_LEN));
|
|
}
|
|
|
|
if (initrds_chksums)
|
|
{
|
|
g_ptr_array_add (initrds_chksums, NULL);
|
|
_ostree_deployment_set_overlay_initrds (deployment, (char**)initrds_chksums->pdata);
|
|
}
|
|
|
|
g_ptr_array_add (inout_deployments, g_object_ref (deployment));
|
|
return TRUE;
|
|
}
|
|
|
|
static gint
|
|
compare_deployments_by_boot_loader_version_reversed (gconstpointer a_pp,
|
|
gconstpointer b_pp)
|
|
{
|
|
OstreeDeployment *a = *((OstreeDeployment**)a_pp);
|
|
OstreeDeployment *b = *((OstreeDeployment**)b_pp);
|
|
OstreeBootconfigParser *a_bootconfig = ostree_deployment_get_bootconfig (a);
|
|
OstreeBootconfigParser *b_bootconfig = ostree_deployment_get_bootconfig (b);
|
|
|
|
/* Staged deployments are always first */
|
|
if (ostree_deployment_is_staged (a))
|
|
{
|
|
g_assert (!ostree_deployment_is_staged (b));
|
|
return -1;
|
|
}
|
|
else if (ostree_deployment_is_staged (b))
|
|
return 1;
|
|
|
|
return compare_boot_loader_configs (a_bootconfig, b_bootconfig);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_load:
|
|
* @self: Sysroot
|
|
* @cancellable: Cancellable
|
|
* @error: Error
|
|
*
|
|
* Load deployment list, bootversion, and subbootversion from the
|
|
* rootfs @self.
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_load (OstreeSysroot *self,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
return ostree_sysroot_load_if_changed (self, NULL, cancellable, error);
|
|
}
|
|
|
|
static gboolean
|
|
ensure_repo (OstreeSysroot *self,
|
|
GError **error)
|
|
{
|
|
if (self->repo != NULL)
|
|
return TRUE;
|
|
if (!ensure_sysroot_fd (self, error))
|
|
return FALSE;
|
|
self->repo = ostree_repo_open_at (self->sysroot_fd, "ostree/repo", NULL, error);
|
|
if (!self->repo)
|
|
return FALSE;
|
|
|
|
/* Flag it as having been created via ostree_sysroot_get_repo(), and hold a
|
|
* weak ref for the remote-add handling.
|
|
*/
|
|
g_weak_ref_init (&self->repo->sysroot, self);
|
|
self->repo->sysroot_kind = OSTREE_REPO_SYSROOT_KIND_VIA_SYSROOT;
|
|
|
|
/* Reload the repo config in case any defaults depend on knowing if this is
|
|
* a system repo.
|
|
*/
|
|
if (!ostree_repo_reload_config (self->repo, NULL, error))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_initialize:
|
|
* @self: sysroot
|
|
*
|
|
* Subset of ostree_sysroot_load(); performs basic initialization. Notably, one
|
|
* can invoke `ostree_sysroot_get_fd()` after calling this function.
|
|
*
|
|
* It is not necessary to call this function if ostree_sysroot_load() is
|
|
* invoked.
|
|
*
|
|
* Since: 2020.1
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_initialize (OstreeSysroot *self,
|
|
GError **error)
|
|
{
|
|
if (!ensure_sysroot_fd (self, error))
|
|
return FALSE;
|
|
|
|
if (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_INIT)
|
|
{
|
|
/* Gather some global state; first if we have the global ostree-booted flag;
|
|
* we'll use it to sanity check that we found a booted deployment for example.
|
|
* Second, we also find out whether sysroot == /.
|
|
*/
|
|
if (!glnx_fstatat_allow_noent (AT_FDCWD, OSTREE_PATH_BOOTED, NULL, 0, error))
|
|
return FALSE;
|
|
const gboolean ostree_booted = (errno == 0);
|
|
|
|
{ struct stat root_stbuf;
|
|
if (!glnx_fstatat (AT_FDCWD, "/", &root_stbuf, 0, error))
|
|
return FALSE;
|
|
self->root_device = root_stbuf.st_dev;
|
|
self->root_inode = root_stbuf.st_ino;
|
|
}
|
|
|
|
struct stat self_stbuf;
|
|
if (!glnx_fstatat (AT_FDCWD, gs_file_get_path_cached (self->path), &self_stbuf, 0, error))
|
|
return FALSE;
|
|
|
|
const gboolean root_is_sysroot =
|
|
(self->root_device == self_stbuf.st_dev &&
|
|
self->root_inode == self_stbuf.st_ino);
|
|
|
|
self->root_is_ostree_booted = (ostree_booted && root_is_sysroot);
|
|
self->loadstate = OSTREE_SYSROOT_LOAD_STATE_INIT;
|
|
}
|
|
|
|
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);
|
|
if (!self->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_autofree char **overlay_initrds = NULL;
|
|
g_variant_dict_lookup (staged_deployment_dict, "target", "@a{sv}", &target);
|
|
g_variant_dict_lookup (staged_deployment_dict, "kargs", "^a&s", &kargs);
|
|
g_variant_dict_lookup (staged_deployment_dict, "overlay-initrds", "^a&s", &overlay_initrds);
|
|
if (target)
|
|
{
|
|
g_autoptr(OstreeDeployment) staged =
|
|
_ostree_sysroot_deserialize_deployment_from_variant (target, error);
|
|
if (!staged)
|
|
return FALSE;
|
|
|
|
_ostree_deployment_set_bootconfig_from_kargs (staged, kargs);
|
|
if (!load_origin (self, staged, NULL, error))
|
|
return FALSE;
|
|
|
|
_ostree_deployment_set_overlay_initrds (staged, overlay_initrds);
|
|
|
|
self->staged_deployment = g_steal_pointer (&staged);
|
|
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;
|
|
}
|
|
|
|
/* Loads the current bootversion, subbootversion, and deployments, starting from the
|
|
* bootloader configs which are the source of truth.
|
|
*/
|
|
static gboolean
|
|
sysroot_load_from_bootloader_configs (OstreeSysroot *self,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
struct stat stbuf;
|
|
|
|
int bootversion = 0;
|
|
if (!read_current_bootversion (self, &bootversion, cancellable, error))
|
|
return FALSE;
|
|
|
|
int subbootversion = 0;
|
|
if (!_ostree_sysroot_read_current_subbootversion (self, bootversion, &subbootversion,
|
|
cancellable, error))
|
|
return FALSE;
|
|
|
|
g_autoptr(GPtrArray) boot_loader_configs = NULL;
|
|
if (!_ostree_sysroot_read_boot_loader_configs (self, bootversion, &boot_loader_configs,
|
|
cancellable, error))
|
|
return FALSE;
|
|
|
|
g_autoptr(GPtrArray) deployments = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
|
|
|
|
g_assert (boot_loader_configs); /* Pacify static analysis */
|
|
for (guint i = 0; i < boot_loader_configs->len; i++)
|
|
{
|
|
OstreeBootconfigParser *config = boot_loader_configs->pdata[i];
|
|
|
|
/* Note this also sets self->booted_deployment */
|
|
if (!list_deployments_process_one_boot_entry (self, config, deployments,
|
|
cancellable, error))
|
|
{
|
|
g_clear_object (&self->booted_deployment);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (self->root_is_ostree_booted && !self->booted_deployment)
|
|
{
|
|
if (!glnx_fstatat_allow_noent (self->sysroot_fd, "boot/loader", NULL, AT_SYMLINK_NOFOLLOW, error))
|
|
return FALSE;
|
|
if (errno == ENOENT)
|
|
{
|
|
return glnx_throw (error, "Unexpected state: %s found, but no /boot/loader directory", OSTREE_PATH_BOOTED);
|
|
}
|
|
else
|
|
{
|
|
return glnx_throw (error, "Unexpected state: %s found and in / sysroot, but bootloader entry not found", OSTREE_PATH_BOOTED);
|
|
}
|
|
}
|
|
|
|
if (!_ostree_sysroot_reload_staged (self, error))
|
|
return FALSE;
|
|
|
|
/* Ensure the entires are sorted */
|
|
g_ptr_array_sort (deployments, compare_deployments_by_boot_loader_version_reversed);
|
|
|
|
/* Staged shows up first */
|
|
if (self->staged_deployment)
|
|
g_ptr_array_insert (deployments, 0, g_object_ref (self->staged_deployment));
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* Determine whether we're "physical" or not, the first time we load deployments */
|
|
if (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_LOADED)
|
|
{
|
|
/* If we have a booted deployment, the sysroot is / and we're definitely
|
|
* not physical.
|
|
*/
|
|
if (self->booted_deployment)
|
|
self->is_physical = FALSE; /* (the default, but explicit for clarity) */
|
|
/* Otherwise - check for /sysroot which should only exist in a deployment,
|
|
* not in ${sysroot} (a metavariable for the real physical root).
|
|
*/
|
|
else
|
|
{
|
|
if (!glnx_fstatat_allow_noent (self->sysroot_fd, "sysroot", &stbuf, 0, error))
|
|
return FALSE;
|
|
if (errno == ENOENT)
|
|
self->is_physical = TRUE;
|
|
}
|
|
/* Otherwise, the default is FALSE */
|
|
|
|
self->loadstate = OSTREE_SYSROOT_LOAD_STATE_LOADED;
|
|
}
|
|
|
|
self->bootversion = bootversion;
|
|
self->subbootversion = subbootversion;
|
|
self->deployments = g_steal_pointer (&deployments);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_load_if_changed:
|
|
* @self: #OstreeSysroot
|
|
* @out_changed: (out caller-allocates):
|
|
* @cancellable: Cancellable
|
|
* @error: Error
|
|
*
|
|
* Since: 2016.4
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_load_if_changed (OstreeSysroot *self,
|
|
gboolean *out_changed,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
GLNX_AUTO_PREFIX_ERROR ("loading sysroot", error);
|
|
|
|
if (!ostree_sysroot_initialize (self, error))
|
|
return FALSE;
|
|
|
|
/* Here we also lazily initialize the repository. We didn't do this
|
|
* previous to v2017.6, but we do now to support the error-free
|
|
* ostree_sysroot_repo() API.
|
|
*/
|
|
if (!ensure_repo (self, error))
|
|
return FALSE;
|
|
|
|
struct stat stbuf;
|
|
if (!glnx_fstatat (self->sysroot_fd, "ostree/deploy", &stbuf, 0, error))
|
|
return FALSE;
|
|
|
|
if (self->loaded_ts.tv_sec == stbuf.st_mtim.tv_sec &&
|
|
self->loaded_ts.tv_nsec == stbuf.st_mtim.tv_nsec)
|
|
{
|
|
if (out_changed)
|
|
*out_changed = FALSE;
|
|
/* Note early return */
|
|
return TRUE;
|
|
}
|
|
|
|
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;
|
|
|
|
if (!sysroot_load_from_bootloader_configs (self, cancellable, error))
|
|
return FALSE;
|
|
|
|
self->loaded_ts = stbuf.st_mtim;
|
|
|
|
if (out_changed)
|
|
*out_changed = TRUE;
|
|
return TRUE;
|
|
}
|
|
|
|
int
|
|
ostree_sysroot_get_bootversion (OstreeSysroot *self)
|
|
{
|
|
return self->bootversion;
|
|
}
|
|
|
|
int
|
|
ostree_sysroot_get_subbootversion (OstreeSysroot *self)
|
|
{
|
|
return self->subbootversion;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_get_booted_deployment:
|
|
* @self: Sysroot
|
|
*
|
|
* This function may only be called if the sysroot is loaded.
|
|
*
|
|
* Returns: (transfer none) (nullable): The currently booted deployment, or %NULL if none
|
|
*/
|
|
OstreeDeployment *
|
|
ostree_sysroot_get_booted_deployment (OstreeSysroot *self)
|
|
{
|
|
g_assert (self);
|
|
g_assert (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED);
|
|
|
|
return self->booted_deployment;
|
|
}
|
|
|
|
|
|
/**
|
|
* ostree_sysroot_require_booted_deployment:
|
|
* @self: Sysroot
|
|
*
|
|
* Find the booted deployment, or return an error if not booted via OSTree.
|
|
*
|
|
* Returns: (transfer none) (not nullable): The currently booted deployment, or an error
|
|
* Since: 2021.1
|
|
*/
|
|
OstreeDeployment *
|
|
ostree_sysroot_require_booted_deployment (OstreeSysroot *self, GError **error)
|
|
{
|
|
g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL);
|
|
|
|
if (!self->booted_deployment)
|
|
return glnx_null_throw (error, "Not currently booted into an OSTree system");
|
|
return self->booted_deployment;
|
|
}
|
|
|
|
|
|
/**
|
|
* ostree_sysroot_get_staged_deployment:
|
|
* @self: Sysroot
|
|
*
|
|
* Returns: (transfer none) (nullable): The currently staged deployment, or %NULL if none
|
|
*
|
|
* Since: 2018.5
|
|
*/
|
|
OstreeDeployment *
|
|
ostree_sysroot_get_staged_deployment (OstreeSysroot *self)
|
|
{
|
|
g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL);
|
|
|
|
return self->staged_deployment;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_get_deployments:
|
|
* @self: Sysroot
|
|
*
|
|
* Returns: (element-type OstreeDeployment) (transfer container): Ordered list of deployments
|
|
*/
|
|
GPtrArray *
|
|
ostree_sysroot_get_deployments (OstreeSysroot *self)
|
|
{
|
|
g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL);
|
|
|
|
GPtrArray *copy = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
|
|
for (guint i = 0; i < self->deployments->len; i++)
|
|
g_ptr_array_add (copy, g_object_ref (self->deployments->pdata[i]));
|
|
return copy;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_get_deployment_dirpath:
|
|
* @self: Repo
|
|
* @deployment: A deployment
|
|
*
|
|
* Note this function only returns a *relative* path - if you want
|
|
* to access, it, you must either use fd-relative api such as openat(),
|
|
* or concatenate it with the full ostree_sysroot_get_path().
|
|
*
|
|
* Returns: (transfer full) (not nullable): Path to deployment root directory, relative to sysroot
|
|
*/
|
|
char *
|
|
ostree_sysroot_get_deployment_dirpath (OstreeSysroot *self,
|
|
OstreeDeployment *deployment)
|
|
{
|
|
return g_strdup_printf ("ostree/deploy/%s/deploy/%s.%d",
|
|
ostree_deployment_get_osname (deployment),
|
|
ostree_deployment_get_csum (deployment),
|
|
ostree_deployment_get_deployserial (deployment));
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_get_deployment_directory:
|
|
* @self: Sysroot
|
|
* @deployment: A deployment
|
|
*
|
|
* Returns: (transfer full): Path to deployment root directory
|
|
*/
|
|
GFile *
|
|
ostree_sysroot_get_deployment_directory (OstreeSysroot *self,
|
|
OstreeDeployment *deployment)
|
|
{
|
|
g_autofree char *dirpath = ostree_sysroot_get_deployment_dirpath (self, deployment);
|
|
return g_file_resolve_relative_path (self->path, dirpath);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_get_deployment_origin_path:
|
|
* @deployment_path: A deployment path
|
|
*
|
|
* Returns: (transfer full): Path to deployment origin file
|
|
*/
|
|
GFile *
|
|
ostree_sysroot_get_deployment_origin_path (GFile *deployment_path)
|
|
{
|
|
g_autoptr(GFile) deployment_parent = g_file_get_parent (deployment_path);
|
|
return ot_gfile_resolve_path_printf (deployment_parent,
|
|
"%s.origin",
|
|
gs_file_get_path_cached (deployment_path));
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_get_repo:
|
|
* @self: Sysroot
|
|
* @out_repo: (out) (transfer full) (optional): Repository in sysroot @self
|
|
* @cancellable: Cancellable
|
|
* @error: Error
|
|
*
|
|
* Retrieve the OSTree repository in sysroot @self. The repo is guaranteed to be open
|
|
* (see ostree_repo_open()).
|
|
*
|
|
* Returns: %TRUE on success, %FALSE otherwise
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_get_repo (OstreeSysroot *self,
|
|
OstreeRepo **out_repo,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
if (!ensure_repo (self, error))
|
|
return FALSE;
|
|
if (out_repo != NULL)
|
|
*out_repo = g_object_ref (self->repo);
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_repo:
|
|
* @self: Sysroot
|
|
*
|
|
* This function is a variant of ostree_sysroot_get_repo() that cannot fail, and
|
|
* returns a cached repository. Can only be called after ostree_sysroot_initialize()
|
|
* or ostree_sysroot_load() has been invoked successfully.
|
|
*
|
|
* Returns: (transfer none) (not nullable): The OSTree repository in sysroot @self.
|
|
*
|
|
* Since: 2017.7
|
|
*/
|
|
OstreeRepo *
|
|
ostree_sysroot_repo (OstreeSysroot *self)
|
|
{
|
|
g_assert (self);
|
|
g_assert (self->loadstate >= OSTREE_SYSROOT_LOAD_STATE_LOADED);
|
|
g_assert (self->repo);
|
|
return self->repo;
|
|
}
|
|
|
|
static OstreeBootloader*
|
|
_ostree_sysroot_new_bootloader_by_type (
|
|
OstreeSysroot *sysroot,
|
|
OstreeCfgSysrootBootloaderOpt bl_type)
|
|
{
|
|
switch (bl_type)
|
|
{
|
|
case CFG_SYSROOT_BOOTLOADER_OPT_NONE:
|
|
/* No bootloader specified; do not query bootloaders to run. */
|
|
return NULL;
|
|
case CFG_SYSROOT_BOOTLOADER_OPT_GRUB2:
|
|
return (OstreeBootloader*) _ostree_bootloader_grub2_new (sysroot);
|
|
case CFG_SYSROOT_BOOTLOADER_OPT_SYSLINUX:
|
|
return (OstreeBootloader*) _ostree_bootloader_syslinux_new (sysroot);
|
|
case CFG_SYSROOT_BOOTLOADER_OPT_UBOOT:
|
|
return (OstreeBootloader*) _ostree_bootloader_uboot_new (sysroot);
|
|
case CFG_SYSROOT_BOOTLOADER_OPT_ZIPL:
|
|
/* We never consider zipl as active by default, so it can only be created
|
|
* if it's explicitly requested in the config */
|
|
return (OstreeBootloader*) _ostree_bootloader_zipl_new (sysroot);
|
|
case CFG_SYSROOT_BOOTLOADER_OPT_AUTO:
|
|
/* "auto" is handled by ostree_sysroot_query_bootloader so we should
|
|
* never get here: Fallthrough */
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_query_bootloader:
|
|
* @sysroot: Sysroot
|
|
* @out_bootloader: (out) (transfer full) (optional) (nullable): Return location for bootloader, may be %NULL
|
|
* @cancellable: Cancellable
|
|
* @error: Error
|
|
*/
|
|
gboolean
|
|
_ostree_sysroot_query_bootloader (OstreeSysroot *sysroot,
|
|
OstreeBootloader **out_bootloader,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
OstreeRepo *repo = ostree_sysroot_repo (sysroot);
|
|
OstreeCfgSysrootBootloaderOpt bootloader_config = repo->bootloader;
|
|
|
|
g_debug ("Using bootloader configuration: %s",
|
|
CFG_SYSROOT_BOOTLOADER_OPTS_STR[bootloader_config]);
|
|
|
|
g_autoptr(OstreeBootloader) ret_loader = NULL;
|
|
if (bootloader_config == CFG_SYSROOT_BOOTLOADER_OPT_AUTO)
|
|
{
|
|
OstreeCfgSysrootBootloaderOpt probe[] = {
|
|
CFG_SYSROOT_BOOTLOADER_OPT_SYSLINUX,
|
|
CFG_SYSROOT_BOOTLOADER_OPT_GRUB2,
|
|
CFG_SYSROOT_BOOTLOADER_OPT_UBOOT,
|
|
};
|
|
for (int i = 0; i < G_N_ELEMENTS (probe); i++)
|
|
{
|
|
g_autoptr(OstreeBootloader) bl = _ostree_sysroot_new_bootloader_by_type (
|
|
sysroot, probe[i]);
|
|
gboolean is_active = FALSE;
|
|
if (!_ostree_bootloader_query (bl, &is_active, cancellable, error))
|
|
return FALSE;
|
|
if (is_active)
|
|
{
|
|
ret_loader = g_steal_pointer (&bl);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
ret_loader = _ostree_sysroot_new_bootloader_by_type (sysroot, bootloader_config);
|
|
|
|
ot_transfer_out_value (out_bootloader, &ret_loader)
|
|
return TRUE;
|
|
}
|
|
|
|
char *
|
|
_ostree_sysroot_join_lines (GPtrArray *lines)
|
|
{
|
|
GString *buf = g_string_new ("");
|
|
gboolean prev_was_empty = FALSE;
|
|
|
|
for (guint i = 0; i < lines->len; i++)
|
|
{
|
|
const char *line = lines->pdata[i];
|
|
/* Special bit to remove extraneous empty lines */
|
|
if (*line == '\0')
|
|
{
|
|
if (prev_was_empty || i == 0)
|
|
continue;
|
|
else
|
|
prev_was_empty = TRUE;
|
|
}
|
|
g_string_append (buf, line);
|
|
g_string_append_c (buf, '\n');
|
|
}
|
|
return g_string_free (buf, FALSE);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_query_deployments_for:
|
|
* @self: Sysroot
|
|
* @osname: (allow-none): "stateroot" name
|
|
* @out_pending: (out) (nullable) (optional) (transfer full): The pending deployment
|
|
* @out_rollback: (out) (nullable) (optional) (transfer full): The rollback deployment
|
|
*
|
|
* Find the pending and rollback deployments for @osname. Pass %NULL for @osname
|
|
* to use the booted deployment's osname. By default, pending deployment is the
|
|
* first deployment in the order that matches @osname, and @rollback will be the
|
|
* next one after the booted deployment, or the deployment after the pending if
|
|
* we're not looking at the booted deployment.
|
|
*
|
|
* Since: 2017.7
|
|
*/
|
|
void
|
|
ostree_sysroot_query_deployments_for (OstreeSysroot *self,
|
|
const char *osname,
|
|
OstreeDeployment **out_pending,
|
|
OstreeDeployment **out_rollback)
|
|
{
|
|
g_return_if_fail (osname != NULL || self->booted_deployment != NULL);
|
|
g_autoptr(OstreeDeployment) ret_pending = NULL;
|
|
g_autoptr(OstreeDeployment) ret_rollback = NULL;
|
|
|
|
if (osname == NULL)
|
|
osname = ostree_deployment_get_osname (self->booted_deployment);
|
|
|
|
gboolean found_booted = FALSE;
|
|
for (guint i = 0; i < self->deployments->len; i++)
|
|
{
|
|
OstreeDeployment *deployment = self->deployments->pdata[i];
|
|
|
|
/* Ignore deployments not for this osname */
|
|
if (strcmp (ostree_deployment_get_osname (deployment), osname) != 0)
|
|
continue;
|
|
|
|
/* Is this deployment booted? If so, note we're past the booted */
|
|
if (self->booted_deployment != NULL &&
|
|
ostree_deployment_equal (deployment, self->booted_deployment))
|
|
{
|
|
found_booted = TRUE;
|
|
continue;
|
|
}
|
|
|
|
if (!found_booted && !ret_pending)
|
|
ret_pending = g_object_ref (deployment);
|
|
else if (found_booted && !ret_rollback)
|
|
ret_rollback = g_object_ref (deployment);
|
|
}
|
|
if (out_pending)
|
|
*out_pending = g_steal_pointer (&ret_pending);
|
|
if (out_rollback)
|
|
*out_rollback = g_steal_pointer (&ret_rollback);
|
|
}
|
|
|
|
|
|
/**
|
|
* ostree_sysroot_get_merge_deployment:
|
|
* @self: Sysroot
|
|
* @osname: (allow-none): Operating system group
|
|
*
|
|
* Find the deployment to use as a configuration merge source; this is
|
|
* the first one in the current deployment list which matches osname.
|
|
*
|
|
* Returns: (transfer full) (nullable): Configuration merge deployment
|
|
*/
|
|
OstreeDeployment *
|
|
ostree_sysroot_get_merge_deployment (OstreeSysroot *self,
|
|
const char *osname)
|
|
{
|
|
g_return_val_if_fail (osname != NULL || self->booted_deployment != NULL, NULL);
|
|
|
|
if (osname == NULL)
|
|
osname = ostree_deployment_get_osname (self->booted_deployment);
|
|
|
|
/* If we're booted into the OS into which we're deploying, then
|
|
* merge the currently *booted* configuration, rather than the most
|
|
* recently deployed.
|
|
*/
|
|
if (self->booted_deployment &&
|
|
g_strcmp0 (ostree_deployment_get_osname (self->booted_deployment), osname) == 0)
|
|
return g_object_ref (self->booted_deployment);
|
|
else
|
|
{
|
|
g_autoptr(OstreeDeployment) pending = NULL;
|
|
ostree_sysroot_query_deployments_for (self, osname, &pending, NULL);
|
|
return g_steal_pointer (&pending);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_origin_new_from_refspec:
|
|
* @self: Sysroot
|
|
* @refspec: A refspec
|
|
*
|
|
* Returns: (transfer full) (not nullable): A new config file which sets @refspec as an origin
|
|
*/
|
|
GKeyFile *
|
|
ostree_sysroot_origin_new_from_refspec (OstreeSysroot *self,
|
|
const char *refspec)
|
|
{
|
|
GKeyFile *ret = g_key_file_new ();
|
|
g_key_file_set_string (ret, "origin", "refspec", refspec);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_lock:
|
|
* @self: Self
|
|
* @error: Error
|
|
*
|
|
* Acquire an exclusive multi-process write lock for @self. This call
|
|
* blocks until the lock has been acquired. The lock is not
|
|
* reentrant.
|
|
*
|
|
* Release the lock with ostree_sysroot_unlock(). The lock will also
|
|
* be released if @self is deallocated.
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_lock (OstreeSysroot *self,
|
|
GError **error)
|
|
{
|
|
if (!ensure_sysroot_fd (self, error))
|
|
return FALSE;
|
|
|
|
if (!_ostree_sysroot_ensure_writable (self, error))
|
|
return FALSE;
|
|
|
|
return glnx_make_lock_file (self->sysroot_fd, OSTREE_SYSROOT_LOCKFILE,
|
|
LOCK_EX, &self->lock, error);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_try_lock:
|
|
* @self: Self
|
|
* @out_acquired: (out): Whether or not the lock has been acquired
|
|
* @error: Error
|
|
*
|
|
* Try to acquire an exclusive multi-process write lock for @self. If
|
|
* another process holds the lock, this function will return
|
|
* immediately, setting @out_acquired to %FALSE, and returning %TRUE
|
|
* (and no error).
|
|
*
|
|
* Release the lock with ostree_sysroot_unlock(). The lock will also
|
|
* be released if @self is deallocated.
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_try_lock (OstreeSysroot *self,
|
|
gboolean *out_acquired,
|
|
GError **error)
|
|
{
|
|
if (!ensure_sysroot_fd (self, error))
|
|
return FALSE;
|
|
|
|
if (!_ostree_sysroot_ensure_writable (self, error))
|
|
return FALSE;
|
|
|
|
/* Note use of LOCK_NB */
|
|
g_autoptr(GError) local_error = NULL;
|
|
if (!glnx_make_lock_file (self->sysroot_fd, OSTREE_SYSROOT_LOCKFILE,
|
|
LOCK_EX | LOCK_NB, &self->lock, &local_error))
|
|
{
|
|
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
|
|
{
|
|
*out_acquired = FALSE;
|
|
}
|
|
else
|
|
{
|
|
g_propagate_error (error, g_steal_pointer (&local_error));
|
|
return FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
*out_acquired = TRUE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_unlock:
|
|
* @self: Self
|
|
*
|
|
* Clear the lock previously acquired with ostree_sysroot_lock(). It
|
|
* is safe to call this function if the lock has not been previously
|
|
* acquired.
|
|
*/
|
|
void
|
|
ostree_sysroot_unlock (OstreeSysroot *self)
|
|
{
|
|
glnx_release_lock_file (&self->lock);
|
|
}
|
|
|
|
static void
|
|
lock_in_thread (GTask *task,
|
|
gpointer source,
|
|
gpointer task_data,
|
|
GCancellable *cancellable)
|
|
{
|
|
GError *local_error = NULL;
|
|
OstreeSysroot *self = source;
|
|
|
|
if (!ostree_sysroot_lock (self, &local_error))
|
|
goto out;
|
|
|
|
if (g_cancellable_set_error_if_cancelled (cancellable, &local_error))
|
|
ostree_sysroot_unlock (self);
|
|
|
|
out:
|
|
if (local_error)
|
|
g_task_return_error (task, local_error);
|
|
else
|
|
g_task_return_boolean (task, TRUE);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_lock_async:
|
|
* @self: Self
|
|
* @cancellable: Cancellable
|
|
* @callback: Callback
|
|
* @user_data: User data
|
|
*
|
|
* An asynchronous version of ostree_sysroot_lock().
|
|
*/
|
|
void
|
|
ostree_sysroot_lock_async (OstreeSysroot *self,
|
|
GCancellable *cancellable,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
g_autoptr(GTask) task = g_task_new (self, cancellable, callback, user_data);
|
|
g_task_run_in_thread (task, lock_in_thread);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_lock_finish:
|
|
* @self: Self
|
|
* @result: Result
|
|
* @error: Error
|
|
*
|
|
* Call when ostree_sysroot_lock_async() is ready.
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_lock_finish (OstreeSysroot *self,
|
|
GAsyncResult *result,
|
|
GError **error)
|
|
{
|
|
g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
|
|
return g_task_propagate_boolean ((GTask*)result, error);
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_init_osname:
|
|
* @self: Sysroot
|
|
* @osname: Name group of operating system checkouts
|
|
* @cancellable: Cancellable
|
|
* @error: Error
|
|
*
|
|
* Initialize the directory structure for an "osname", which is a
|
|
* group of operating system deployments, with a shared `/var`. One
|
|
* is required for generating a deployment.
|
|
*
|
|
* Since: 2016.4
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_init_osname (OstreeSysroot *self,
|
|
const char *osname,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
if (!_ostree_sysroot_ensure_writable (self, error))
|
|
return FALSE;
|
|
|
|
const char *deploydir = glnx_strjoina ("ostree/deploy/", osname);
|
|
if (mkdirat (self->sysroot_fd, deploydir, 0777) < 0)
|
|
return glnx_throw_errno_prefix (error, "Creating %s", deploydir);
|
|
|
|
glnx_autofd int dfd = -1;
|
|
if (!glnx_opendirat (self->sysroot_fd, deploydir, TRUE, &dfd, error))
|
|
return FALSE;
|
|
|
|
if (mkdirat (dfd, "var", 0777) < 0)
|
|
return glnx_throw_errno_prefix (error, "Creating %s", "var");
|
|
|
|
/* This is a bit of a legacy hack...but we have to keep it around
|
|
* now. We're ensuring core subdirectories of /var exist.
|
|
*/
|
|
if (mkdirat (dfd, "var/tmp", 0777) < 0)
|
|
return glnx_throw_errno_prefix (error, "Creating %s", "var/tmp");
|
|
|
|
if (fchmodat (dfd, "var/tmp", 01777, 0) < 0)
|
|
return glnx_throw_errno_prefix (error, "fchmod %s", "var/tmp");
|
|
|
|
if (mkdirat (dfd, "var/lib", 0777) < 0)
|
|
return glnx_throw_errno_prefix (error, "Creating %s", "var/lib");
|
|
|
|
/* This needs to be available and properly labeled early during the boot
|
|
* process (before tmpfiles.d kicks in), so that journald can flush logs from
|
|
* the first boot there. https://bugzilla.redhat.com/show_bug.cgi?id=1265295
|
|
* */
|
|
if (mkdirat (dfd, "var/log", 0755) < 0)
|
|
return glnx_throw_errno_prefix (error, "Creating %s", "var/log");
|
|
|
|
if (symlinkat ("../run", dfd, "var/run") < 0)
|
|
return glnx_throw_errno_prefix (error, "Symlinking %s", "var/run");
|
|
|
|
if (symlinkat ("../run/lock", dfd, "var/lock") < 0)
|
|
return glnx_throw_errno_prefix (error, "Symlinking %s", "var/lock");
|
|
|
|
if (!_ostree_sysroot_bump_mtime (self, error))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_simple_write_deployment:
|
|
* @sysroot: Sysroot
|
|
* @osname: (allow-none): OS name
|
|
* @new_deployment: Prepend this deployment to the list
|
|
* @merge_deployment: (allow-none): Use this deployment for configuration merge
|
|
* @flags: Flags controlling behavior
|
|
* @cancellable: Cancellable
|
|
* @error: Error
|
|
*
|
|
* Prepend @new_deployment to the list of deployments, commit, and
|
|
* cleanup. By default, all other deployments for the given @osname
|
|
* except the merge deployment and the booted deployment will be
|
|
* garbage collected.
|
|
*
|
|
* If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN is
|
|
* specified, then all current deployments will be kept.
|
|
*
|
|
* If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_PENDING is
|
|
* specified, then pending deployments will be kept.
|
|
*
|
|
* If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_ROLLBACK is
|
|
* specified, then rollback 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.
|
|
*
|
|
* If %OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NO_CLEAN is
|
|
* specified, then no cleanup will be performed after adding the
|
|
* deployment. Make sure to call ostree_sysroot_cleanup() sometime
|
|
* later, instead.
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot,
|
|
const char *osname,
|
|
OstreeDeployment *new_deployment,
|
|
OstreeDeployment *merge_deployment,
|
|
OstreeSysrootSimpleWriteDeploymentFlags flags,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
const gboolean postclean =
|
|
(flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NO_CLEAN) == 0;
|
|
const gboolean make_default =
|
|
!((flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT) > 0);
|
|
const gboolean retain_pending =
|
|
(flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_PENDING) > 0;
|
|
const gboolean retain_rollback =
|
|
(flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_ROLLBACK) > 0;
|
|
gboolean retain =
|
|
(flags & OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN) > 0;
|
|
|
|
g_autoptr(GPtrArray) deployments = ostree_sysroot_get_deployments (sysroot);
|
|
OstreeDeployment *booted_deployment = ostree_sysroot_get_booted_deployment (sysroot);
|
|
|
|
if (osname == NULL && booted_deployment)
|
|
osname = ostree_deployment_get_osname (booted_deployment);
|
|
|
|
gboolean added_new = FALSE;
|
|
g_autoptr(GPtrArray) new_deployments = g_ptr_array_new_with_free_func (g_object_unref);
|
|
if (make_default)
|
|
{
|
|
g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
|
|
added_new = TRUE;
|
|
}
|
|
|
|
/* without a booted and a merge deployment, retain_pending/rollback become meaningless;
|
|
* let's just retain all deployments in that case */
|
|
if (!booted_deployment && !merge_deployment && (retain_pending || retain_rollback))
|
|
retain = TRUE;
|
|
|
|
/* tracks when we come across the booted deployment */
|
|
gboolean before_booted = TRUE;
|
|
gboolean before_merge = TRUE;
|
|
for (guint i = 0; i < deployments->len; i++)
|
|
{
|
|
OstreeDeployment *deployment = deployments->pdata[i];
|
|
const gboolean osname_matches =
|
|
(osname == NULL || g_str_equal (ostree_deployment_get_osname (deployment), osname));
|
|
const gboolean is_booted = ostree_deployment_equal (deployment, booted_deployment);
|
|
const gboolean is_merge = ostree_deployment_equal (deployment, merge_deployment);
|
|
|
|
if (is_booted)
|
|
before_booted = FALSE;
|
|
if (is_merge)
|
|
before_merge = FALSE;
|
|
|
|
/* use the booted deployment as the "crossover" point between pending and rollback
|
|
* deployments, fall back on merge deployment */
|
|
const gboolean passed_crossover = booted_deployment ? !before_booted : !before_merge;
|
|
|
|
/* Retain deployment if:
|
|
* - we're explicitly asked to, or
|
|
* - it's pinned
|
|
* - the deployment is for another osname, or
|
|
* - we're keeping pending deployments and this is a pending deployment, or
|
|
* - this is the merge or boot deployment, or
|
|
* - we're keeping rollback deployments and this is a rollback deployment
|
|
*/
|
|
if (retain
|
|
|| ostree_deployment_is_pinned (deployment)
|
|
|| !osname_matches
|
|
|| (retain_pending && !passed_crossover)
|
|
|| (is_booted || is_merge)
|
|
|| (retain_rollback && passed_crossover))
|
|
g_ptr_array_add (new_deployments, g_object_ref (deployment));
|
|
|
|
/* add right after booted/merge deployment */
|
|
if (!added_new && passed_crossover)
|
|
{
|
|
g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
|
|
added_new = TRUE;
|
|
}
|
|
}
|
|
|
|
/* add it last if no crossover defined (or it's the first deployment in the sysroot) */
|
|
if (!added_new)
|
|
g_ptr_array_add (new_deployments, g_object_ref (new_deployment));
|
|
|
|
OstreeSysrootWriteDeploymentsOpts write_opts = { .do_postclean = postclean };
|
|
if (!ostree_sysroot_write_deployments_with_options (sysroot, new_deployments, &write_opts,
|
|
cancellable, error))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Deploy a copy of @target_deployment */
|
|
static gboolean
|
|
clone_deployment (OstreeSysroot *sysroot,
|
|
OstreeDeployment *target_deployment,
|
|
OstreeDeployment *merge_deployment,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
/* Ensure we have a clean slate */
|
|
if (!ostree_sysroot_prepare_cleanup (sysroot, cancellable, error))
|
|
return glnx_prefix_error (error, "Performing initial cleanup");
|
|
|
|
/* Copy the bootloader config options */
|
|
OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (merge_deployment);
|
|
g_auto(GStrv) previous_args = g_strsplit (ostree_bootconfig_parser_get (bootconfig, "options"), " ", -1);
|
|
g_autoptr(OstreeKernelArgs) kargs = ostree_kernel_args_new ();
|
|
ostree_kernel_args_append_argv (kargs, previous_args);
|
|
|
|
/* Deploy the copy */
|
|
g_autoptr(OstreeDeployment) new_deployment = NULL;
|
|
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))
|
|
return FALSE;
|
|
|
|
/* 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))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Do `mkdir()` followed by `chmod()` immediately afterwards to ensure `umask()` isn't
|
|
* masking permissions where we don't want it to. Thus we avoid calling `umask()`, which
|
|
* would affect the whole process. */
|
|
static gboolean mkdir_unmasked (int dfd,
|
|
const char *path,
|
|
int mode,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
if (!glnx_shutil_mkdir_p_at (dfd, path, mode, cancellable, error))
|
|
return FALSE;
|
|
if (fchmodat (dfd, path, mode, 0) < 0)
|
|
return glnx_throw_errno_prefix (error, "chmod(%s)", path);
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* Since: 2016.4
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_deployment_unlock (OstreeSysroot *self,
|
|
OstreeDeployment *deployment,
|
|
OstreeDeploymentUnlockedState unlocked_state,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
/* This function cannot re-lock */
|
|
g_return_val_if_fail (unlocked_state != OSTREE_DEPLOYMENT_UNLOCKED_NONE, FALSE);
|
|
|
|
OstreeDeploymentUnlockedState current_unlocked = ostree_deployment_get_unlocked (deployment);
|
|
if (current_unlocked != OSTREE_DEPLOYMENT_UNLOCKED_NONE)
|
|
return glnx_throw (error, "Deployment is already in unlocked state: %s",
|
|
ostree_deployment_unlocked_state_to_string (current_unlocked));
|
|
|
|
g_autoptr(OstreeDeployment) merge_deployment =
|
|
ostree_sysroot_get_merge_deployment (self, ostree_deployment_get_osname (deployment));
|
|
if (!merge_deployment)
|
|
return glnx_throw (error, "No previous deployment to duplicate");
|
|
|
|
/* For hotfixes, we push a rollback target */
|
|
if (unlocked_state == OSTREE_DEPLOYMENT_UNLOCKED_HOTFIX)
|
|
{
|
|
if (!clone_deployment (self, deployment, merge_deployment, cancellable, error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* Crack it open */
|
|
if (!ostree_sysroot_deployment_set_mutable (self, deployment, TRUE,
|
|
cancellable, error))
|
|
return FALSE;
|
|
|
|
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;
|
|
|
|
g_autoptr(OstreeSePolicy) sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error);
|
|
if (!sepolicy)
|
|
return FALSE;
|
|
|
|
/* we want our /usr overlay to have the same permission bits as the one we'll shadow */
|
|
mode_t usr_mode;
|
|
{ struct stat stbuf;
|
|
if (!glnx_fstatat (deployment_dfd, "usr", &stbuf, 0, error))
|
|
return FALSE;
|
|
usr_mode = stbuf.st_mode;
|
|
}
|
|
|
|
const char *ovl_options = NULL;
|
|
static const char hotfix_ovl_options[] = "lowerdir=usr,upperdir=.usr-ovl-upper,workdir=.usr-ovl-work";
|
|
g_autofree char *unlock_ovldir = NULL;
|
|
|
|
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 (!mkdir_unmasked (deployment_dfd, ".usr-ovl-upper", usr_mode, cancellable, error))
|
|
return FALSE;
|
|
if (!mkdir_unmasked (deployment_dfd, ".usr-ovl-work", usr_mode, cancellable, error))
|
|
return FALSE;
|
|
ovl_options = hotfix_ovl_options;
|
|
}
|
|
break;
|
|
case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
|
|
case OSTREE_DEPLOYMENT_UNLOCKED_TRANSIENT:
|
|
{
|
|
unlock_ovldir = g_strdup ("/var/tmp/ostree-unlock-ovl.XXXXXX");
|
|
/* We're just doing transient development/hacking? Okay,
|
|
* stick the overlayfs bits in /var/tmp.
|
|
*/
|
|
const char *development_ovl_upper;
|
|
const char *development_ovl_work;
|
|
|
|
/* Ensure that the directory is created with the same label as `/usr` */
|
|
{ g_auto(OstreeSepolicyFsCreatecon) con = { 0, };
|
|
|
|
if (!_ostree_sepolicy_preparefscreatecon (&con, sepolicy,
|
|
"/usr", usr_mode, error))
|
|
return FALSE;
|
|
|
|
if (g_mkdtemp_full (unlock_ovldir, 0755) == NULL)
|
|
return glnx_throw_errno_prefix (error, "mkdtemp");
|
|
}
|
|
|
|
development_ovl_upper = glnx_strjoina (unlock_ovldir, "/upper");
|
|
if (!mkdir_unmasked (AT_FDCWD, development_ovl_upper, usr_mode, cancellable, error))
|
|
return FALSE;
|
|
development_ovl_work = glnx_strjoina (unlock_ovldir, "/work");
|
|
if (!mkdir_unmasked (AT_FDCWD, development_ovl_work, usr_mode, cancellable, error))
|
|
return FALSE;
|
|
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.
|
|
*/
|
|
{
|
|
pid_t mount_child = fork ();
|
|
if (mount_child < 0)
|
|
return glnx_throw_errno_prefix (error, "fork");
|
|
else if (mount_child == 0)
|
|
{
|
|
int mountflags = 0;
|
|
if (unlocked_state == OSTREE_DEPLOYMENT_UNLOCKED_TRANSIENT)
|
|
mountflags |= MS_RDONLY;
|
|
/* Child process. Do NOT use any GLib API here; it's not generally fork() safe.
|
|
*
|
|
* TODO: report errors across a pipe (or use the journal?) rather than
|
|
* spewing to stderr.
|
|
*/
|
|
if (fchdir (deployment_dfd) < 0)
|
|
err (1, "fchdir");
|
|
if (mount ("overlay", "/usr", "overlay", mountflags, ovl_options) < 0)
|
|
err (1, "mount");
|
|
exit (EXIT_SUCCESS);
|
|
}
|
|
else
|
|
{
|
|
/* Parent */
|
|
int estatus;
|
|
|
|
if (TEMP_FAILURE_RETRY (waitpid (mount_child, &estatus, 0)) < 0)
|
|
return glnx_throw_errno_prefix (error, "waitpid() on mount helper");
|
|
if (!g_spawn_check_exit_status (estatus, error))
|
|
return glnx_prefix_error (error, "Failed overlayfs mount");
|
|
}
|
|
}
|
|
|
|
g_autoptr(OstreeDeployment) deployment_clone = ostree_deployment_clone (deployment);
|
|
GKeyFile *origin_clone = ostree_deployment_get_origin (deployment_clone);
|
|
|
|
/* 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))
|
|
return FALSE;
|
|
break;
|
|
case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
|
|
case OSTREE_DEPLOYMENT_UNLOCKED_TRANSIENT:
|
|
{
|
|
g_autofree char *devpath =
|
|
unlocked_state == OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT ?
|
|
_ostree_sysroot_get_runstate_path (deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT)
|
|
:
|
|
_ostree_sysroot_get_runstate_path (deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_TRANSIENT);
|
|
g_autofree char *devpath_parent = dirname (g_strdup (devpath));
|
|
|
|
if (!glnx_shutil_mkdir_p_at (AT_FDCWD, devpath_parent, 0755, cancellable, error))
|
|
return FALSE;
|
|
|
|
if (!g_file_set_contents (devpath, unlock_ovldir, -1, error))
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* 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))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ostree_sysroot_deployment_set_pinned:
|
|
* @self: Sysroot
|
|
* @deployment: A deployment
|
|
* @is_pinned: Whether or not deployment will be automatically GC'd
|
|
* @error: Error
|
|
*
|
|
* By default, deployments may be subject to garbage collection. Typical uses of
|
|
* libostree only retain at most 2 deployments. If @is_pinned is `TRUE`, a
|
|
* metadata bit will be set causing libostree to avoid automatic GC of the
|
|
* deployment. However, this is really an "advisory" note; it's still possible
|
|
* for e.g. older versions of libostree unaware of pinning to GC the deployment.
|
|
*
|
|
* This function does nothing and returns successfully if the deployment
|
|
* is already in the desired pinning state. It is an error to try to pin
|
|
* the staged deployment (as it's not in the bootloader entries).
|
|
*
|
|
* Since: 2018.3
|
|
*/
|
|
gboolean
|
|
ostree_sysroot_deployment_set_pinned (OstreeSysroot *self,
|
|
OstreeDeployment *deployment,
|
|
gboolean is_pinned,
|
|
GError **error)
|
|
{
|
|
const gboolean current_pin = ostree_deployment_is_pinned (deployment);
|
|
if (is_pinned == current_pin)
|
|
return TRUE;
|
|
|
|
if (ostree_deployment_is_staged (deployment))
|
|
return glnx_throw (error, "Cannot pin staged deployment");
|
|
|
|
g_autoptr(OstreeDeployment) deployment_clone = ostree_deployment_clone (deployment);
|
|
GKeyFile *origin_clone = ostree_deployment_get_origin (deployment_clone);
|
|
|
|
if (is_pinned)
|
|
g_key_file_set_boolean (origin_clone, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", TRUE);
|
|
else
|
|
g_key_file_remove_key (origin_clone, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", NULL);
|
|
|
|
if (!ostree_sysroot_write_origin_file (self, deployment, origin_clone, NULL, error))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|