diff --git a/Makefile-ostree.am b/Makefile-ostree.am
index fb377075eb..f78f7d7571 100644
--- a/Makefile-ostree.am
+++ b/Makefile-ostree.am
@@ -82,6 +82,7 @@ ostree_SOURCES += \
src/ostree/ot-admin-builtin-pin.c \
src/ostree/ot-admin-builtin-upgrade.c \
src/ostree/ot-admin-builtin-unlock.c \
+ src/ostree/ot-admin-builtin-create-toplevel-user-links.c \
src/ostree/ot-admin-builtins.h \
src/ostree/ot-admin-instutil-builtin-selinux-ensure-labeled.c \
src/ostree/ot-admin-instutil-builtin-set-kargs.c \
diff --git a/src/libostree/ostree-cmd-private.c b/src/libostree/ostree-cmd-private.c
index ad820fdeac..e1a9290473 100644
--- a/src/libostree/ostree-cmd-private.c
+++ b/src/libostree/ostree-cmd-private.c
@@ -52,6 +52,7 @@ ostree_cmd__private__ (void)
_ostree_repo_verify_bindings,
_ostree_sysroot_finalize_staged,
_ostree_sysroot_boot_complete,
+ _ostree_sysroot_create_toplevel_user_links,
};
return &table;
diff --git a/src/libostree/ostree-cmd-private.h b/src/libostree/ostree-cmd-private.h
index 17f943c8b2..1c9c2f873d 100644
--- a/src/libostree/ostree-cmd-private.h
+++ b/src/libostree/ostree-cmd-private.h
@@ -34,6 +34,7 @@ typedef struct {
gboolean (* ostree_repo_verify_bindings) (const char *collection_id, const char *ref_name, GVariant *commit, GError **error);
gboolean (* ostree_finalize_staged) (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error);
gboolean (* ostree_boot_complete) (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error);
+ gboolean (* ostree_create_toplevel_user_links) (OstreeSysroot *sysroot, int deployment_dfd, GCancellable *cancellable, GError **error);
} OstreeCmdPrivateVTable;
/* Note this not really "public", we just export the symbol, but not the header */
diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h
index 0d33f7c2d0..132b34968d 100644
--- a/src/libostree/ostree-repo-private.h
+++ b/src/libostree/ostree-repo-private.h
@@ -211,6 +211,7 @@ struct OstreeRepo {
guint test_error_flags; /* OstreeRepoTestErrorFlags */
GKeyFile *config;
+ GKeyFile *user_config;
GHashTable *remotes;
GMutex remotes_lock;
OstreeRepoMode mode;
diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c
index 90cde65139..d275a3d934 100644
--- a/src/libostree/ostree-repo.c
+++ b/src/libostree/ostree-repo.c
@@ -3195,6 +3195,97 @@ min_free_space_size_validate_and_convert (OstreeRepo *self,
return TRUE;
}
+static int
+compare_strings (gconstpointer a, gconstpointer b)
+{
+ const char **sa = (const char **)a;
+ const char **sb = (const char **)b;
+
+ return strcmp (*sa, *sb);
+}
+
+#define SYSCONF_CONFIGS SHORTENED_SYSCONFDIR "/ostree/config.d"
+
+
+static gboolean
+load_user_configs (OstreeRepo *self,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_clear_pointer (&self->user_config, g_key_file_unref);
+ g_autoptr(OstreeSysroot) sysroot_ref = (OstreeSysroot*)g_weak_ref_get (&self->sysroot);
+
+ /* Only read from /etc/ostree/config.d if we are pointed at a deployment */
+ if (sysroot_ref == NULL || sysroot_ref->is_physical)
+ return TRUE;
+
+ g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
+ gboolean exists;
+ if (!ot_dfd_iter_init_allow_noent (sysroot_ref->sysroot_fd, SYSCONF_CONFIGS, &dfd_iter, &exists, error))
+ return FALSE;
+ /* Note early return */
+ if (!exists)
+ return TRUE;
+
+ g_autoptr(GPtrArray) configs = g_ptr_array_new_with_free_func (g_free);
+
+ while (TRUE)
+ {
+ struct dirent *dent;
+ if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error))
+ return FALSE;
+ if (dent == NULL)
+ break;
+
+ /* match remotes.d semantics */
+ if (dent->d_type != DT_REG || !g_str_has_suffix (dent->d_name, ".conf"))
+ continue;
+
+ /* parse it just to validate it */
+ g_autoptr(GKeyFile) k = g_key_file_new ();
+ g_autofree char *path = g_strdup_printf ("/proc/self/fd/%d/%s", dfd_iter.fd, dent->d_name);
+ if (!g_key_file_load_from_file (k, path, G_KEY_FILE_NONE, error))
+ return glnx_prefix_error (error, "parsing config file %s", path);
+
+ g_ptr_array_add (configs, g_strdup (dent->d_name));
+ }
+
+ if (configs->len == 0)
+ return TRUE; /* Note early return; no user configs. */
+
+ g_ptr_array_sort (configs, compare_strings);
+
+ g_auto(GLnxTmpfile) user_keyfile = { 0, };
+ if (!glnx_open_anonymous_tmpfile (O_RDWR | O_CLOEXEC, &user_keyfile, error))
+ return glnx_prefix_error (error, "allocating tmpfile for user config");
+
+ for (guint i = 0; i < configs->len; i++)
+ {
+ const char *config = configs->pdata[i];
+
+ glnx_autofd int fd = -1;
+ if (!glnx_openat_rdonly (dfd_iter.fd, config, FALSE, &fd, error))
+ return glnx_prefix_error (error, "opening %s/%s", SYSCONF_CONFIGS, config);
+
+ if (glnx_regfile_copy_bytes (fd, user_keyfile.fd, -1) < 0)
+ return glnx_throw_errno_prefix (error, "copying %s/%s", SYSCONF_CONFIGS, config);
+
+ /* make sure there's a newline */
+ if (glnx_loop_write (user_keyfile.fd, "\n", 1) < 0)
+ return glnx_throw_errno_prefix (error, "writing newline to tmpfile");
+ }
+
+ self->user_config = g_key_file_new ();
+
+ /* we want g_key_file_load_from_fd() but that's private */
+ g_autofree char *tmpfile_path = g_strdup_printf ("/proc/self/fd/%d", user_keyfile.fd);
+ if (!g_key_file_load_from_file (self->user_config, tmpfile_path, G_KEY_FILE_NONE, error))
+ return glnx_prefix_error (error, "parsing final config file");
+
+ return TRUE;
+}
+
+
static gboolean
reload_core_config (OstreeRepo *self,
GCancellable *cancellable,
@@ -3442,6 +3533,12 @@ reload_core_config (OstreeRepo *self,
self->repo_finders = g_steal_pointer (&configured_finders);
}
+ if (ostree_repo_is_system (self))
+ {
+ if (!load_user_configs (self, cancellable, error))
+ return FALSE;
+ }
+
return TRUE;
}
diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c
index 456b0c041d..41aef83d61 100644
--- a/src/libostree/ostree-sysroot-deploy.c
+++ b/src/libostree/ostree-sysroot-deploy.c
@@ -2742,6 +2742,35 @@ lint_deployment_fs (OstreeSysroot *self,
return TRUE;
}
+/* This is called when deployments are created, but also
+ * via `ostree admin create-toplevel-user-links`. */
+gboolean
+_ostree_sysroot_create_toplevel_user_links (OstreeSysroot *self,
+ int deployment_dfd,
+ GCancellable *cancellable,
+ GError **error)
+{
+ OstreeRepo *repo = ostree_sysroot_repo (self);
+ g_autoptr(GHashTable) toplevel_links = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ ot_keyfile_get_keys_in_hashtable (repo->config, toplevel_links, "toplevel-links");
+ /* toplevel-links is supported in user configs; read after to support overriding */
+ if (repo->user_config)
+ ot_keyfile_get_keys_in_hashtable (repo->user_config, toplevel_links, "toplevel-links");
+
+ GLNX_HASH_TABLE_FOREACH_KV (toplevel_links, const char *, dir, const char *, symlink)
+ {
+ /* sanity-check the user isn't trying to do silly things */
+ if (g_str_equal (dir, ".") || g_str_equal (dir, "..") || strchr (dir, '/') != NULL)
+ return glnx_throw (error, "Invalid top-level dir: '%s'", dir);
+
+ if (TEMP_FAILURE_RETRY (symlinkat (symlink, deployment_dfd, dir)) < 0)
+ return glnx_throw_errno_prefix (error, "symlinkat(/%s)", dir);
+ }
+
+ return TRUE;
+}
+
/* The first part of writing a deployment. This primarily means doing the
* hardlink farm checkout, but we also compute some initial state.
*/
@@ -2793,6 +2822,9 @@ sysroot_initialize_deployment (OstreeSysroot *self,
if (!lint_deployment_fs (self, new_deployment, deployment_dfd, cancellable, error))
return FALSE;
+ if (!_ostree_sysroot_create_toplevel_user_links (self, deployment_dfd, cancellable, error))
+ return FALSE;
+
ot_transfer_out_value (out_new_deployment, &new_deployment);
return TRUE;
}
diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h
index a49a406cf3..1b95d3ac64 100644
--- a/src/libostree/ostree-sysroot-private.h
+++ b/src/libostree/ostree-sysroot-private.h
@@ -187,4 +187,10 @@ gboolean _ostree_sysroot_cleanup_internal (OstreeSysroot *sysroot,
GCancellable *cancellable,
GError **error);
+gboolean
+_ostree_sysroot_create_toplevel_user_links (OstreeSysroot *self,
+ int deployment_dfd,
+ GCancellable *cancellable,
+ GError **error);
+
G_END_DECLS
diff --git a/src/libotutil/ot-keyfile-utils.c b/src/libotutil/ot-keyfile-utils.c
index de8abd2bea..b0ab2e8bc1 100644
--- a/src/libotutil/ot-keyfile-utils.c
+++ b/src/libotutil/ot-keyfile-utils.c
@@ -258,3 +258,25 @@ ot_keyfile_copy_group (GKeyFile *source_keyfile,
out:
return ret;
}
+
+void
+ot_keyfile_get_keys_in_hashtable (GKeyFile *keyfile,
+ GHashTable *table,
+ const char *group_name)
+{
+ g_assert (keyfile != NULL);
+ g_assert (table != NULL);
+ g_assert (group_name != NULL);
+
+ gsize length;
+ g_auto(GStrv) keys = g_key_file_get_keys (keyfile, group_name, &length, NULL);
+ if (keys == NULL)
+ return;
+
+ for (gsize i = 0; i < length; i++)
+ {
+ const char *key = keys[i];
+ g_autofree char *val = g_key_file_get_value (keyfile, group_name, key, NULL);
+ g_hash_table_insert (table, g_strdup (key), g_strdup (val));
+ }
+}
diff --git a/src/libotutil/ot-keyfile-utils.h b/src/libotutil/ot-keyfile-utils.h
index 3b4f6560f7..713b74fb6a 100644
--- a/src/libotutil/ot-keyfile-utils.h
+++ b/src/libotutil/ot-keyfile-utils.h
@@ -72,4 +72,9 @@ ot_keyfile_copy_group (GKeyFile *source_keyfile,
GKeyFile *target_keyfile,
const char *group_name);
+void
+ot_keyfile_get_keys_in_hashtable (GKeyFile *keyfile,
+ GHashTable *table,
+ const char *group_name);
+
G_END_DECLS
diff --git a/src/ostree/ot-admin-builtin-create-toplevel-user-links.c b/src/ostree/ot-admin-builtin-create-toplevel-user-links.c
new file mode 100644
index 0000000000..143ff11bd8
--- /dev/null
+++ b/src/ostree/ot-admin-builtin-create-toplevel-user-links.c
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * This program 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 licence 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 .
+ */
+
+#include "config.h"
+
+#include
+#include
+
+#include "ot-main.h"
+#include "ot-admin-builtins.h"
+#include "ostree-cmd-private.h"
+
+static GOptionEntry options[] = {
+ { NULL }
+};
+
+gboolean
+ot_admin_builtin_create_toplevel_user_links (int argc, char **argv,
+ OstreeCommandInvocation *invocation,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(OstreeSysroot) sysroot = NULL;
+ g_autoptr(GOptionContext) context = g_option_context_new ("");
+ if (!ostree_admin_option_context_parse (context, options, &argc, &argv,
+ OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER,
+ invocation, &sysroot, cancellable, error))
+ return glnx_prefix_error (error, "parsing options");
+
+ OstreeDeployment *deployment = ostree_sysroot_get_booted_deployment (sysroot);
+ g_autofree char *deployment_path = ostree_sysroot_get_deployment_dirpath (sysroot, deployment);
+
+ glnx_autofd int deployment_dfd = -1;
+ if (!glnx_opendirat (ostree_sysroot_get_fd (sysroot), deployment_path, TRUE, &deployment_dfd, error))
+ return glnx_prefix_error (error, "open(%s)", deployment_path);
+
+ if (!ostree_sysroot_deployment_set_mutable (sysroot, deployment, TRUE, cancellable, error))
+ return glnx_prefix_error (error, "setting deployment mutable");
+
+ if (!ostree_cmd__private__()->ostree_create_toplevel_user_links (sysroot, deployment_dfd, cancellable, error))
+ return glnx_prefix_error (error, "creating toplevel user links");
+
+ if (!ostree_sysroot_deployment_set_mutable (sysroot, deployment, FALSE, cancellable, error))
+ return glnx_prefix_error (error, "setting deployment immutable");
+
+ return TRUE;
+}
diff --git a/src/ostree/ot-admin-builtins.h b/src/ostree/ot-admin-builtins.h
index 8bac1c5625..a180521360 100644
--- a/src/ostree/ot-admin-builtins.h
+++ b/src/ostree/ot-admin-builtins.h
@@ -47,6 +47,7 @@ BUILTINPROTO(diff);
BUILTINPROTO(switch);
BUILTINPROTO(upgrade);
BUILTINPROTO(kargs);
+BUILTINPROTO(create_toplevel_user_links);
#undef BUILTINPROTO
diff --git a/src/ostree/ot-builtin-admin.c b/src/ostree/ot-builtin-admin.c
index 503fb9a7be..29fecdd249 100644
--- a/src/ostree/ot-builtin-admin.c
+++ b/src/ostree/ot-builtin-admin.c
@@ -79,6 +79,9 @@ static OstreeCommand admin_subcommands[] = {
{ "kargs", OSTREE_BUILTIN_FLAG_NO_REPO,
ot_admin_builtin_kargs,
"Change kernel arguments" },
+ { "create-toplevel-user-links", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
+ ot_admin_builtin_create_toplevel_user_links,
+ "Create user-configured top-level symlinks" },
{ NULL, 0, NULL, NULL }
};