ostree/src/ostree/ot-admin-functions.c

838 lines
26 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
*
* Copyright (C) 2012 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.
*
* Author: Colin Walters <walters@verbum.org>
*/
#include "config.h"
#include "ot-admin-functions.h"
#include "ot-deployment.h"
#include "ot-config-parser.h"
#include "ot-bootloader-syslinux.h"
#include "ot-bootloader-uboot.h"
#include "otutil.h"
#include "ostree.h"
#include "libgsystem.h"
OtOrderedHash *
ot_admin_parse_kernel_args (const char *options)
{
OtOrderedHash *ret;
char **args;
char **iter;
ret = ot_ordered_hash_new ();
if (!options)
return ret;
args = g_strsplit (options, " ", -1);
for (iter = args; *iter; iter++)
{
char *arg = *iter;
char *val;
val = ot_admin_util_split_keyeq (arg);
g_ptr_array_add (ret->order, arg);
g_hash_table_insert (ret->table, arg, val);
}
return ret;
}
char *
ot_admin_kernel_arg_string_serialize (OtOrderedHash *ohash)
{
guint i;
GString *buf = g_string_new ("");
gboolean first = TRUE;
for (i = 0; i < ohash->order->len; i++)
{
const char *key = ohash->order->pdata[i];
const char *val = g_hash_table_lookup (ohash->table, key);
g_assert (val != NULL);
if (first)
first = FALSE;
else
g_string_append_c (buf, ' ');
if (*val)
g_string_append_printf (buf, "%s=%s", key, val);
else
g_string_append (buf, key);
}
return g_string_free (buf, FALSE);
}
static void
match_info_cleanup (void *loc)
{
GMatchInfo **match = (GMatchInfo**)loc;
if (*match) g_match_info_unref (*match);
}
gboolean
ot_admin_ensure_initialized (GFile *sysroot,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_object GFile *dir = NULL;
gs_unref_object GFile *ostree_dir = NULL;
ostree_dir = g_file_get_child (sysroot, "ostree");
g_clear_object (&dir);
dir = g_file_get_child (ostree_dir, "repo");
if (!gs_file_ensure_directory (dir, TRUE, cancellable, error))
goto out;
g_clear_object (&dir);
dir = g_file_get_child (ostree_dir, "deploy");
if (!gs_file_ensure_directory (dir, TRUE, cancellable, error))
goto out;
g_clear_object (&dir);
dir = ot_gfile_get_child_build_path (ostree_dir, "repo", "objects", NULL);
if (!g_file_query_exists (dir, NULL))
{
gs_free char *opt_repo_arg = g_strdup_printf ("--repo=%s/repo",
gs_file_get_path_cached (ostree_dir));
if (!gs_subprocess_simple_run_sync (NULL, GS_SUBPROCESS_STREAM_DISPOSITION_NULL,
cancellable, error,
"ostree", opt_repo_arg, "init", NULL))
{
g_prefix_error (error, "Failed to initialize repository: ");
goto out;
}
}
ret = TRUE;
out:
return ret;
}
gboolean
ot_admin_check_os (GFile *sysroot,
const char *osname,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_object GFile *osdir = NULL;
osdir = ot_gfile_resolve_path_printf (sysroot, "ostree/deploy/%s/var", osname);
if (!g_file_query_exists (osdir, NULL))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"No such OS '%s', use os-init to create it", osname);
goto out;
}
ret = TRUE;
out:
return ret;
}
static gboolean
parse_bootlink (const char *bootlink,
int *out_entry_bootversion,
char **out_osname,
char **out_bootcsum,
int *out_treebootserial,
GError **error)
{
gboolean ret = FALSE;
__attribute__((cleanup(match_info_cleanup))) GMatchInfo *match = NULL;
gs_free char *bootversion_str = NULL;
gs_free char *treebootserial_str = NULL;
static gsize regex_initialized;
static GRegex *regex;
if (g_once_init_enter (&regex_initialized))
{
regex = g_regex_new ("^/ostree/boot.([01])/([^/]+)/([^/]+)/([0-9]+)$", 0, 0, NULL);
g_assert (regex);
g_once_init_leave (&regex_initialized, 1);
}
if (!g_regex_match (regex, bootlink, 0, &match))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Invalid ostree= argument '%s', expected ostree=/ostree/boot.BOOTVERSION/OSNAME/BOOTCSUM/TREESERIAL", bootlink);
goto out;
}
bootversion_str = g_match_info_fetch (match, 1);
*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);
treebootserial_str = g_match_info_fetch (match, 4);
*out_treebootserial = (int)g_ascii_strtoll (treebootserial_str, NULL, 10);
ret = TRUE;
out:
return ret;
}
gboolean
ot_admin_parse_deploy_path_name (const char *name,
char **out_csum,
int *out_serial,
GError **error)
{
gboolean ret = FALSE;
__attribute__((cleanup(match_info_cleanup))) GMatchInfo *match = NULL;
gs_free char *serial_str = NULL;
static gsize regex_initialized;
static GRegex *regex;
if (g_once_init_enter (&regex_initialized))
{
regex = g_regex_new ("^([0-9a-f]+)\\.([0-9]+)$", 0, 0, NULL);
g_assert (regex);
g_once_init_leave (&regex_initialized, 1);
}
if (!g_regex_match (regex, name, 0, &match))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Invalid deploy name '%s', expected CHECKSUM.TREESERIAL", name);
goto out;
}
*out_csum = g_match_info_fetch (match, 1);
serial_str = g_match_info_fetch (match, 2);
*out_serial = (int)g_ascii_strtoll (serial_str, NULL, 10);
ret = TRUE;
out:
return ret;
}
GFile *
ot_admin_get_deployment_origin_path (GFile *deployment_path)
{
gs_unref_object 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));
}
static gboolean
parse_origin (GFile *sysroot,
GFile *deployment_path,
GKeyFile **out_origin,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
GKeyFile *ret_origin = NULL;
gs_unref_object GFile *origin_path = ot_admin_get_deployment_origin_path (deployment_path);
gs_free char *origin_contents = NULL;
if (!ot_gfile_load_contents_utf8_allow_noent (origin_path, &origin_contents,
cancellable, error))
goto out;
if (origin_contents)
{
ret_origin = g_key_file_new ();
if (!g_key_file_load_from_data (ret_origin, origin_contents, -1, 0, error))
goto out;
}
ret = TRUE;
ot_transfer_out_value (out_origin, &ret_origin);
out:
if (error)
g_prefix_error (error, "Parsing %s: ", gs_file_get_path_cached (origin_path));
if (ret_origin)
g_key_file_unref (ret_origin);
return ret;
}
static gboolean
parse_deployment (GFile *sysroot,
const char *boot_link,
OtDeployment **out_deployment,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
const char *relative_boot_link;
gs_unref_object OtDeployment *ret_deployment = NULL;
int entry_boot_version;
int treebootserial = -1;
int deployserial = -1;
gs_free char *osname = NULL;
gs_free char *bootcsum = NULL;
gs_free char *treecsum = NULL;
gs_unref_object GFile *treebootserial_link = NULL;
gs_unref_object GFileInfo *treebootserial_info = NULL;
gs_unref_object GFile *treebootserial_target = NULL;
GKeyFile *origin = NULL;
if (!parse_bootlink (boot_link, &entry_boot_version,
&osname, &bootcsum, &treebootserial,
error))
goto out;
relative_boot_link = boot_link;
if (*relative_boot_link == '/')
relative_boot_link++;
treebootserial_link = g_file_resolve_relative_path (sysroot, relative_boot_link);
treebootserial_info = g_file_query_info (treebootserial_link, OSTREE_GIO_FAST_QUERYINFO,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
cancellable, error);
if (!treebootserial_info)
goto out;
if (!ot_gfile_get_symlink_target_from_info (treebootserial_link, treebootserial_info,
&treebootserial_target, cancellable, error))
goto out;
if (!ot_admin_parse_deploy_path_name (gs_file_get_basename_cached (treebootserial_target),
&treecsum, &deployserial, error))
goto out;
if (!parse_origin (sysroot, treebootserial_target, &origin,
cancellable, error))
goto out;
ret_deployment = ot_deployment_new (-1, osname, treecsum, deployserial,
bootcsum, treebootserial);
if (origin)
ot_deployment_set_origin (ret_deployment, origin);
ret = TRUE;
ot_transfer_out_value (out_deployment, &ret_deployment);
out:
if (origin)
g_key_file_unref (origin);
return ret;
}
static gboolean
parse_kernel_commandline (OtOrderedHash **out_args,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_object GFile *proc_cmdline = g_file_new_for_path ("/proc/cmdline");
gs_free char *contents = NULL;
gsize len;
if (!g_file_load_contents (proc_cmdline, cancellable, &contents, &len, NULL,
error))
goto out;
ret = TRUE;
*out_args = ot_admin_parse_kernel_args (contents);;
out:
return ret;
}
/**
* ot_admin_find_booted_deployment:
* @target_sysroot: Root directory
* @deployments: (element-type OtDeployment): Loaded deployments
* @out_deployment: (out): The currently booted deployment
* @cancellable:
* @error:
*
* If the system is currently booted into a deployment in
* @deployments, set @out_deployment. Note that if @target_sysroot is
* not equal to "/", @out_deployment will always be set to %NULL.
*/
gboolean
ot_admin_find_booted_deployment (GFile *target_sysroot,
GPtrArray *deployments,
OtDeployment **out_deployment,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_object GFile *active_root = g_file_new_for_path ("/");
gs_unref_object OtDeployment *ret_deployment = NULL;
if (g_file_equal (active_root, target_sysroot))
{
guint i;
const char *bootlink_arg;
__attribute__((cleanup(ot_ordered_hash_cleanup))) OtOrderedHash *kernel_args = NULL;
guint32 root_device;
guint64 root_inode;
if (!ot_admin_util_get_devino (active_root, &root_device, &root_inode,
cancellable, error))
goto out;
if (!parse_kernel_commandline (&kernel_args, cancellable, error))
goto out;
bootlink_arg = g_hash_table_lookup (kernel_args->table, "ostree");
if (bootlink_arg)
{
for (i = 0; i < deployments->len; i++)
{
OtDeployment *deployment = deployments->pdata[i];
gs_unref_object GFile *deployment_path = ot_admin_get_deployment_directory (active_root, deployment);
guint32 device;
guint64 inode;
if (!ot_admin_util_get_devino (deployment_path, &device, &inode,
cancellable, error))
goto out;
if (device == root_device && inode == root_inode)
{
ret_deployment = g_object_ref (deployment);
break;
}
}
if (ret_deployment == NULL)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Unexpected state: ostree= kernel argument found, but / is not a deployment root");
goto out;
}
}
else
{
/* Not an ostree system */
}
}
ret = TRUE;
ot_transfer_out_value (out_deployment, &ret_deployment);
out:
return ret;
}
gboolean
ot_admin_require_deployment_or_osname (GFile *sysroot,
GPtrArray *deployments,
const char *osname,
OtDeployment **out_deployment,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_object OtDeployment *ret_deployment = NULL;
if (!ot_admin_find_booted_deployment (sysroot, deployments, &ret_deployment,
cancellable, error))
goto out;
if (ret_deployment == NULL && osname == NULL)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Not currently booted into an OSTree system and no --os= argument given");
goto out;
}
ret = TRUE;
ot_transfer_out_value (out_deployment, &ret_deployment);
out:
return ret;
}
OtDeployment *
ot_admin_get_merge_deployment (GPtrArray *deployments,
const char *osname,
OtDeployment *booted_deployment)
{
g_return_val_if_fail (osname != NULL || booted_deployment != NULL, NULL);
if (osname == NULL)
osname = ot_deployment_get_osname (booted_deployment);
if (booted_deployment &&
g_strcmp0 (ot_deployment_get_osname (booted_deployment), osname) == 0)
{
return g_object_ref (booted_deployment);
}
else
{
guint i;
for (i = 0; i < deployments->len; i++)
{
OtDeployment *deployment = deployments->pdata[i];
if (strcmp (ot_deployment_get_osname (deployment), osname) != 0)
continue;
return g_object_ref (deployment);
}
}
return NULL;
}
static gboolean
read_current_bootversion (GFile *sysroot,
int *out_bootversion,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_object GFile *boot_loader_path = g_file_resolve_relative_path (sysroot, "boot/loader");
gs_unref_object GFileInfo *info = NULL;
const char *target;
int ret_bootversion;
if (!ot_gfile_query_info_allow_noent (boot_loader_path, OSTREE_GIO_FAST_QUERYINFO,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
&info,
cancellable, error))
goto out;
if (info == NULL)
ret_bootversion = 0;
else
{
if (g_file_info_get_file_type (info) != G_FILE_TYPE_SYMBOLIC_LINK)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Not a symbolic link: %s", gs_file_get_path_cached (boot_loader_path));
goto out;
}
target = g_file_info_get_symlink_target (info);
if (g_strcmp0 (target, "loader.0") == 0)
ret_bootversion = 0;
else if (g_strcmp0 (target, "loader.1") == 0)
ret_bootversion = 1;
else
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Invalid target '%s' in %s", target, gs_file_get_path_cached (boot_loader_path));
goto out;
}
}
ret = TRUE;
*out_bootversion = ret_bootversion;
out:
return ret;
}
gboolean
ot_admin_read_current_subbootversion (GFile *sysroot,
int bootversion,
int *out_subbootversion,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_object GFile *ostree_dir = g_file_get_child (sysroot, "ostree");
gs_free char *ostree_bootdir_name = g_strdup_printf ("boot.%d", bootversion);
gs_unref_object GFile *ostree_bootdir = g_file_resolve_relative_path (ostree_dir, ostree_bootdir_name);
gs_free char *ostree_subbootdir_name = NULL;
gs_unref_object GFile *ostree_subbootdir = NULL;
gs_unref_ptrarray GPtrArray *deployments_to_swap = NULL;
if (!ot_gfile_query_symlink_target_allow_noent (ostree_bootdir, &ostree_subbootdir,
cancellable, error))
goto out;
if (ostree_subbootdir == NULL)
{
*out_subbootversion = 0;
}
else
{
const char *current_subbootdir_name = gs_file_get_basename_cached (ostree_subbootdir);
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
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Invalid target '%s' in %s",
gs_file_get_path_cached (ostree_subbootdir),
gs_file_get_path_cached (ostree_bootdir));
goto out;
}
}
ret = TRUE;
out:
return ret;
}
gboolean
ot_admin_read_boot_loader_configs (GFile *sysroot,
int bootversion,
GPtrArray **out_loader_configs,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_object GFileEnumerator *dir_enum = NULL;
gs_unref_object GFile *loader_entries_dir = NULL;
gs_unref_ptrarray GPtrArray *ret_loader_configs = NULL;
GError *temp_error = NULL;
loader_entries_dir = ot_gfile_resolve_path_printf (sysroot, "boot/loader.%d/entries",
bootversion);
ret_loader_configs = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
dir_enum = g_file_enumerate_children (loader_entries_dir, OSTREE_GIO_FAST_QUERYINFO,
0, NULL, &temp_error);
if (!dir_enum)
{
if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
{
g_clear_error (&temp_error);
goto done;
}
else
{
g_propagate_error (error, temp_error);
goto out;
}
}
while (TRUE)
{
GFileInfo *file_info;
GFile *child;
const char *name;
if (!gs_file_enumerator_iterate (dir_enum, &file_info, &child,
cancellable, error))
goto out;
if (file_info == NULL)
break;
name = g_file_info_get_name (file_info);
if (g_str_has_prefix (name, "ostree-") &&
g_str_has_suffix (name, ".conf") &&
g_file_info_get_file_type (file_info) == G_FILE_TYPE_REGULAR)
{
gs_unref_object OtConfigParser *config = ot_config_parser_new (" \t");
if (!ot_config_parser_parse (config, child, cancellable, error))
{
g_prefix_error (error, "Parsing %s: ", gs_file_get_path_cached (child));
goto out;
}
g_ptr_array_add (ret_loader_configs, g_object_ref (config));
}
}
done:
ot_transfer_out_value (out_loader_configs, &ret_loader_configs);
ret = TRUE;
out:
return ret;
}
static char *
get_ostree_kernel_arg_from_config (OtConfigParser *config)
{
const char *options;
char *ret = NULL;
char **opts, **iter;
options = ot_config_parser_get (config, "options");
if (!options)
return NULL;
opts = g_strsplit (options, " ", -1);
for (iter = opts; *iter; iter++)
{
const char *opt = *iter;
if (g_str_has_prefix (opt, "ostree="))
{
ret = g_strdup (opt + strlen ("ostree="));
break;
}
}
g_strfreev (opts);
return ret;
}
static gboolean
list_deployments_process_one_boot_entry (GFile *sysroot,
OtConfigParser *config,
GPtrArray *inout_deployments,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_free char *ostree_arg = NULL;
gs_unref_object OtDeployment *deployment = NULL;
ostree_arg = get_ostree_kernel_arg_from_config (config);
if (ostree_arg == NULL)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"No ostree= kernel argument found");
goto out;
}
if (!parse_deployment (sysroot, ostree_arg, &deployment,
cancellable, error))
goto out;
ot_deployment_set_bootconfig (deployment, config);
g_ptr_array_add (inout_deployments, g_object_ref (deployment));
ret = TRUE;
out:
return ret;
}
static gint
compare_deployments_by_boot_loader_version (gconstpointer a_pp,
gconstpointer b_pp)
{
OtDeployment *a = *((OtDeployment**)a_pp);
OtDeployment *b = *((OtDeployment**)b_pp);
OtConfigParser *a_bootconfig = ot_deployment_get_bootconfig (a);
OtConfigParser *b_bootconfig = ot_deployment_get_bootconfig (b);
const char *a_version = ot_config_parser_get (a_bootconfig, "version");
const char *b_version = ot_config_parser_get (b_bootconfig, "version");
if (a_version && b_version)
return strverscmp (a_version, b_version);
else if (a_version)
return 1;
else
return -1;
}
gboolean
ot_admin_list_deployments (GFile *sysroot,
int *out_current_bootversion,
GPtrArray **out_deployments,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_ptrarray GPtrArray *boot_loader_configs = NULL;
gs_unref_ptrarray GPtrArray *ret_deployments = NULL;
guint i;
int bootversion;
if (!read_current_bootversion (sysroot, &bootversion, cancellable, error))
goto out;
if (!ot_admin_read_boot_loader_configs (sysroot, bootversion, &boot_loader_configs,
cancellable, error))
goto out;
ret_deployments = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
for (i = 0; i < boot_loader_configs->len; i++)
{
OtConfigParser *config = boot_loader_configs->pdata[i];
if (!list_deployments_process_one_boot_entry (sysroot, config, ret_deployments,
cancellable, error))
goto out;
}
g_ptr_array_sort (ret_deployments, compare_deployments_by_boot_loader_version);
for (i = 0; i < ret_deployments->len; i++)
{
OtDeployment *deployment = ret_deployments->pdata[i];
ot_deployment_set_index (deployment, i);
}
ret = TRUE;
*out_current_bootversion = bootversion;
ot_transfer_out_value (out_deployments, &ret_deployments);
out:
return ret;
}
GFile *
ot_admin_get_deployment_directory (GFile *sysroot,
OtDeployment *deployment)
{
gs_free char *path = g_strdup_printf ("ostree/deploy/%s/deploy/%s.%d",
ot_deployment_get_osname (deployment),
ot_deployment_get_csum (deployment),
ot_deployment_get_deployserial (deployment));
return g_file_resolve_relative_path (sysroot, path);
}
OtBootloader *
ot_admin_query_bootloader (GFile *sysroot)
{
OtBootloaderSyslinux *syslinux;
OtBootloaderUboot *uboot;
syslinux = ot_bootloader_syslinux_new (sysroot);
if (ot_bootloader_query ((OtBootloader*)syslinux))
return (OtBootloader*) (syslinux);
uboot = ot_bootloader_uboot_new (sysroot);
if (ot_bootloader_query ((OtBootloader*)uboot))
return (OtBootloader*) (uboot);
return NULL;
}
GKeyFile *
ot_origin_new_from_refspec (const char *refspec)
{
GKeyFile *ret = g_key_file_new ();
g_key_file_set_string (ret, "origin", "refspec", refspec);
return ret;
}
gboolean
ot_admin_get_repo (GFile *sysroot,
OstreeRepo **out_repo,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gs_unref_object OstreeRepo *ret_repo = NULL;
gs_unref_object GFile *repo_path = g_file_resolve_relative_path (sysroot, "ostree/repo");
ret_repo = ostree_repo_new (repo_path);
if (!ostree_repo_check (ret_repo, error))
goto out;
ret = TRUE;
ot_transfer_out_value (out_repo, &ret_repo);
out:
return ret;
}