]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Use paths specified from environment variables for /etc configuration files
authorValentin David <valentin.david@canonical.com>
Mon, 10 Mar 2025 09:53:41 +0000 (10:53 +0100)
committerLennart Poettering <lennart@poettering.net>
Mon, 23 Jun 2025 13:32:11 +0000 (15:32 +0200)
Some configuration files that need updates are directly under in /etc. To
update them atomically, we need write access to /etc. For Ubuntu Core this is
an issue as /etc is not writable. Only a selection of subdirectories can be
writable. The general solution is symlinks or bind mounts to writable places.
But for atomic writes in /etc, that does not work. So Ubuntu has had a patch
for that that did not age well.

Instead we would like to introduce some environment variables for alternate
paths.

 * SYSTEMD_ETC_HOSTNAME: /etc/hostname
 * SYSTEMD_ETC_MACHINE_INFO: /etc/machine-info
 * SYSTEMD_ETC_LOCALTIME: /etc/localtime
 * SYSTEMD_ETC_LOCALE_CONF: /etc/locale.conf
 * SYSTEMD_ETC_VCONSOLE_CONF: /etc/vconsole.conf
 * SYSTEMD_ETC_ADJTIME: /etc/adjtime

While it is for now expected that there is a symlink from the standard, we
still try to read them from that alternate path. This is important for
`/etc/localtime`, which is a symlink, so we cannot have an indirect symlink or
bind mount for it.

Since machine-id is typically written only once and not updated. This commit
does not cover it. An initrd can properly create it and bind mount it.

19 files changed:
docs/ENVIRONMENT.md
src/basic/hostname-util.c
src/basic/hostname-util.h
src/basic/time-util.c
src/basic/time-util.h
src/core/manager.c
src/firstboot/firstboot.c
src/hostname/hostnamed.c
src/locale/localed-util.c
src/shared/env-file-label.c [new file with mode: 0644]
src/shared/hostname-setup.c
src/shared/locale-setup.c
src/shared/locale-setup.h
src/timedate/timedated.c
test/units/TEST-30-ONCLOCKCHANGE.sh
test/units/TEST-45-TIMEDATE.sh
test/units/TEST-71-HOSTNAME.sh
test/units/TEST-73-LOCALE.sh
test/units/TEST-74-AUX-UTILS.firstboot.sh

index 87fb67502a8bffd470bd6c1ccddf7c0cddc6ed84..ab3ac4895b1f89a52e9d4890183a4c78461dfa9d 100644 (file)
@@ -294,6 +294,9 @@ All tools:
   first existing unit listed in the environment variable, and
   `timedatectl set-ntp off` disables and stops all listed units.
 
+* `$SYSTEMD_ETC_ADJTIME` - override the path to the hardware clock settings
+  file. The default is `/etc/adjtime`.
+
 `systemd-sulogin-shell`:
 
 * `$SYSTEMD_SULOGIN_FORCE=1` — This skips asking for the root password if the
@@ -787,3 +790,25 @@ Tools using the Varlink protocol (such as `varlinkctl`) or sd-bus (such as
   `systemd.factory_reset=` kernel command line option: if set to false,
   requesting a TPM clearing is skipped, and the command immediately exits
   successfully.
+
+`systemd-timedated`, `systemd-firstboot`, `systemd`:
+
+* `$SYSTEMD_ETC_LOCALTIME` - override the path to the timezone symlink. The
+  default is `/etc/localtime`. The directory of the path should exist and not
+  be removed.
+
+`systemd-hostnamed`, `systemd-firstboot`:
+
+* `$SYSTEMD_ETC_HOSTNAME` - override the path to local system name
+  configuration file. The default is `/etc/hostname`.
+
+* `$SYSTEMD_ETC_MACHINE_INFO` - override the path to the machine metadata file. The
+  default is `/etc/machine-info`.
+
+`systemd-localed`, `systemd-firstboot`:
+
+* `$SYSTEMD_ETC_LOCALE_CONF` - override the path to the system-wide locale
+  configuration file. The default is `/etc/locale.conf`.
+
+* `$SYSTEMD_ETC_VCONSOLE_CONF` - override the path to the virtual console
+  configuration file. The default is `/etc/vconsole.conf`.
index a3f820e3c9e7e5227fd7f92e9f5c05b1cf42311d..2238e86d76694c86525466bbf6cb466e809b601c 100644 (file)
@@ -157,13 +157,31 @@ bool is_localhost(const char *hostname) {
                 endswith_no_case(hostname, ".localhost.localdomain.");
 }
 
+const char* etc_hostname(void) {
+        static const char *cached = NULL;
+
+        if (!cached)
+                cached = secure_getenv("SYSTEMD_ETC_HOSTNAME") ?: "/etc/hostname";
+
+        return cached;
+}
+
+const char* etc_machine_info(void) {
+        static const char *cached = NULL;
+
+        if (!cached)
+                cached = secure_getenv("SYSTEMD_ETC_MACHINE_INFO") ?: "/etc/machine-info";
+
+        return cached;
+}
+
 int get_pretty_hostname(char **ret) {
         _cleanup_free_ char *n = NULL;
         int r;
 
         assert(ret);
 
-        r = parse_env_file(NULL, "/etc/machine-info", "PRETTY_HOSTNAME", &n);
+        r = parse_env_file(NULL, etc_machine_info(), "PRETTY_HOSTNAME", &n);
         if (r < 0)
                 return r;
 
index 5f3930756d047c60e3658a1aa44b314e65f3fd3c..3cbf0966ea1b84edc5ed38af26e677475ba467c2 100644 (file)
@@ -37,6 +37,9 @@ static inline bool is_dns_proxy_stub_hostname(const char *hostname) {
         return STRCASE_IN_SET(hostname, "_localdnsproxy", "_localdnsproxy.");
 }
 
+const char* etc_hostname(void);
+const char* etc_machine_info(void);
+
 int get_pretty_hostname(char **ret);
 
 int machine_spec_valid(const char *s);
index 1a86f7fdee5b4ffc70626217a9fef90938802c05..55931a254615741a3ddb275d02fa5e5d97b3edda 100644 (file)
@@ -1607,7 +1607,7 @@ int get_timezone(char **ret) {
 
         assert(ret);
 
-        r = readlink_malloc("/etc/localtime", &t);
+        r = readlink_malloc(etc_localtime(), &t);
         if (r == -ENOENT)
                 /* If the symlink does not exist, assume "UTC", like glibc does */
                 return strdup_to(ret, "UTC");
@@ -1623,6 +1623,15 @@ int get_timezone(char **ret) {
         return strdup_to(ret, e);
 }
 
+const char* etc_localtime(void) {
+        static const char *cached = NULL;
+
+        if (!cached)
+                cached = secure_getenv("SYSTEMD_ETC_LOCALTIME") ?: "/etc/localtime";
+
+        return cached;
+}
+
 int mktime_or_timegm_usec(
                 struct tm *tm, /* input + normalized output */
                 bool utc,
index 90c17c39b1772f83866f91742bdd9b980ab3a895..d31e62d18ae6088f6d40f061c6d1610e60923f2a 100644 (file)
@@ -168,6 +168,7 @@ bool clock_supported(clockid_t clock);
 usec_t usec_shift_clock(usec_t, clockid_t from, clockid_t to);
 
 int get_timezone(char **ret);
+const char* etc_localtime(void);
 
 int mktime_or_timegm_usec(struct tm *tm, bool utc, usec_t *ret);
 int localtime_or_gmtime_usec(usec_t t, bool utc, struct tm *ret);
index 53c62afaae640e8efdbe638b2b018f893d628a20..000f19d6f68e600fbef4e8fc1c5e34f23374cd01 100644 (file)
@@ -420,7 +420,7 @@ static int manager_read_timezone_stat(Manager *m) {
         assert(m);
 
         /* Read the current stat() data of /etc/localtime so that we detect changes */
-        if (lstat("/etc/localtime", &st) < 0) {
+        if (lstat(etc_localtime(), &st) < 0) {
                 log_debug_errno(errno, "Failed to stat /etc/localtime, ignoring: %m");
                 changed = m->etc_localtime_accessible;
                 m->etc_localtime_accessible = false;
@@ -457,14 +457,20 @@ static int manager_setup_timezone_change(Manager *m) {
          * Note that we create the new event source first here, before releasing the old one. This should optimize
          * behaviour as this way sd-event can reuse the old watch in case the inode didn't change. */
 
-        r = sd_event_add_inotify(m->event, &new_event, "/etc/localtime",
+        r = sd_event_add_inotify(m->event, &new_event, etc_localtime(),
                                  IN_ATTRIB|IN_MOVE_SELF|IN_CLOSE_WRITE|IN_DONT_FOLLOW, manager_dispatch_timezone_change, m);
         if (r == -ENOENT) {
                 /* If the file doesn't exist yet, subscribe to /etc instead, and wait until it is created either by
                  * O_CREATE or by rename() */
+                _cleanup_free_ char *localtime_dir = NULL;
 
-                log_debug_errno(r, "/etc/localtime doesn't exist yet, watching /etc instead.");
-                r = sd_event_add_inotify(m->event, &new_event, "/etc",
+                int dir_r = path_extract_directory(etc_localtime(), &localtime_dir);
+                if (dir_r < 0)
+                        return log_error_errno(dir_r, "Failed to extract directory from path '%s': %m", etc_localtime());
+
+                log_debug_errno(r, "%s doesn't exist yet, watching %s instead.", etc_localtime(), localtime_dir);
+
+                r = sd_event_add_inotify(m->event, &new_event, localtime_dir,
                                          IN_CREATE|IN_MOVED_TO|IN_ONLYDIR, manager_dispatch_timezone_change, m);
         }
         if (r < 0)
index a7654ca6159324c8ecdf584b8bf9e0d364bf0633..4462086e782036d536d6fadf6858e76e0e0900ce 100644 (file)
@@ -31,6 +31,7 @@
 #include "label.h"
 #include "label-util.h"
 #include "libcrypt-util.h"
+#include "locale-setup.h"
 #include "locale-util.h"
 #include "lock-util.h"
 #include "loop-util.h"
@@ -408,7 +409,7 @@ static int process_locale(int rfd) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, "/etc/locale.conf",
+        pfd = chase_and_open_parent_at(rfd, etc_locale_conf(),
                                        CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
                                        &f);
         if (pfd < 0)
@@ -425,7 +426,7 @@ static int process_locale(int rfd) {
                 return log_error_errno(r, "Failed to check if directory file descriptor is root: %m");
 
         if (arg_copy_locale && r == 0) {
-                r = copy_file_atomic_at(AT_FDCWD, "/etc/locale.conf", pfd, f, 0644, COPY_REFLINK);
+                r = copy_file_atomic_at(AT_FDCWD, etc_locale_conf(), pfd, f, 0644, COPY_REFLINK);
                 if (r != -ENOENT) {
                         if (r < 0)
                                 return log_error_errno(r, "Failed to copy host's /etc/locale.conf: %m");
@@ -520,7 +521,7 @@ static int process_keymap(int rfd) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, "/etc/vconsole.conf",
+        pfd = chase_and_open_parent_at(rfd, etc_vconsole_conf(),
                                        CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
                                        &f);
         if (pfd < 0)
@@ -537,7 +538,7 @@ static int process_keymap(int rfd) {
                 return log_error_errno(r, "Failed to check if directory file descriptor is root: %m");
 
         if (arg_copy_keymap && r == 0) {
-                r = copy_file_atomic_at(AT_FDCWD, "/etc/vconsole.conf", pfd, f, 0644, COPY_REFLINK);
+                r = copy_file_atomic_at(AT_FDCWD, etc_vconsole_conf(), pfd, f, 0644, COPY_REFLINK);
                 if (r != -ENOENT) {
                         if (r < 0)
                                 return log_error_errno(r, "Failed to copy host's /etc/vconsole.conf: %m");
@@ -617,13 +618,13 @@ static int prompt_timezone(int rfd) {
 
 static int process_timezone(int rfd) {
         _cleanup_close_ int pfd = -EBADF;
-        _cleanup_free_ char *f = NULL;
+        _cleanup_free_ char *f = NULL, *relpath = NULL;
         const char *e;
         int r;
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, "/etc/localtime",
+        pfd = chase_and_open_parent_at(rfd, etc_localtime(),
                                        CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
                                        &f);
         if (pfd < 0)
@@ -642,7 +643,7 @@ static int process_timezone(int rfd) {
         if (arg_copy_timezone && r == 0) {
                 _cleanup_free_ char *s = NULL;
 
-                r = readlink_malloc("/etc/localtime", &s);
+                r = readlink_malloc(etc_localtime(), &s);
                 if (r != -ENOENT) {
                         if (r < 0)
                                 return log_error_errno(r, "Failed to read host's /etc/localtime: %m");
@@ -663,9 +664,12 @@ static int process_timezone(int rfd) {
         if (isempty(arg_timezone))
                 return 0;
 
-        e = strjoina("../usr/share/zoneinfo/", arg_timezone);
+        e = strjoina("/usr/share/zoneinfo/", arg_timezone);
+        r = path_make_relative_parent(etc_localtime(), e, &relpath);
+        if (r < 0)
+                return r;
 
-        r = symlinkat_atomic_full(e, pfd, f, SYMLINK_LABEL);
+        r = symlinkat_atomic_full(relpath, pfd, f, SYMLINK_LABEL);
         if (r < 0)
                 return log_error_errno(r, "Failed to create /etc/localtime symlink: %m");
 
@@ -712,7 +716,7 @@ static int process_hostname(int rfd) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, "/etc/hostname",
+        pfd = chase_and_open_parent_at(rfd, etc_hostname(),
                                        CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN,
                                        &f);
         if (pfd < 0)
@@ -1228,12 +1232,12 @@ static int process_reset(int rfd) {
                 return 0;
 
         FOREACH_STRING(p,
-                       "/etc/locale.conf",
-                       "/etc/vconsole.conf",
-                       "/etc/hostname",
+                       etc_locale_conf(),
+                       etc_vconsole_conf(),
+                       etc_hostname(),
                        "/etc/machine-id",
                        "/etc/kernel/cmdline",
-                       "/etc/localtime") {
+                       etc_localtime()) {
                 r = reset_one(rfd, p);
                 if (r < 0)
                         return r;
index 789ab2be78a0059321b9dcae8986be20f46ca226..d0e4ebeeda02db1b2bac355085b618ac792f2f1b 100644 (file)
@@ -127,7 +127,7 @@ static void context_read_etc_hostname(Context *c) {
 
         assert(c);
 
-        if (stat("/etc/hostname", &current_stat) >= 0 &&
+        if (stat(etc_hostname(), &current_stat) >= 0 &&
             stat_inode_unmodified(&c->etc_hostname_stat, &current_stat))
                 return;
 
@@ -160,7 +160,7 @@ static void context_read_machine_info(Context *c) {
 
         assert(c);
 
-        if (stat("/etc/machine-info", &current_stat) >= 0 &&
+        if (stat(etc_machine_info(), &current_stat) >= 0 &&
             stat_inode_unmodified(&c->etc_machine_info_stat, &current_stat))
                 return;
 
@@ -175,7 +175,7 @@ static void context_read_machine_info(Context *c) {
                       (UINT64_C(1) << PROP_HARDWARE_SKU) |
                       (UINT64_C(1) << PROP_HARDWARE_VERSION));
 
-        r = parse_env_file(NULL, "/etc/machine-info",
+        r = parse_env_file(NULL, etc_machine_info(),
                            "PRETTY_HOSTNAME", &c->data[PROP_PRETTY_HOSTNAME],
                            "ICON_NAME", &c->data[PROP_ICON_NAME],
                            "CHASSIS", &c->data[PROP_CHASSIS],
@@ -806,14 +806,14 @@ static int context_write_data_static_hostname(Context *c) {
         s = &c->etc_hostname_stat;
 
         if (isempty(c->data[PROP_STATIC_HOSTNAME])) {
-                if (unlink("/etc/hostname") < 0 && errno != ENOENT)
+                if (unlink(etc_hostname()) < 0 && errno != ENOENT)
                         return -errno;
 
                 TAKE_PTR(s);
                 return 0;
         }
 
-        r = write_string_file("/etc/hostname", c->data[PROP_STATIC_HOSTNAME], WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_LABEL);
+        r = write_string_file(etc_hostname(), c->data[PROP_STATIC_HOSTNAME], WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_LABEL);
         if (r < 0)
                 return r;
 
@@ -839,7 +839,7 @@ static int context_write_data_machine_info(Context *c) {
          * already, even if we can't make it hit the disk. */
         s = &c->etc_machine_info_stat;
 
-        r = load_env_file(NULL, "/etc/machine-info", &l);
+        r = load_env_file(NULL, etc_machine_info(), &l);
         if (r < 0 && r != -ENOENT)
                 return r;
 
@@ -852,7 +852,7 @@ static int context_write_data_machine_info(Context *c) {
         }
 
         if (strv_isempty(l)) {
-                if (unlink("/etc/machine-info") < 0 && errno != ENOENT)
+                if (unlink(etc_machine_info()) < 0 && errno != ENOENT)
                         return -errno;
 
                 TAKE_PTR(s);
@@ -861,8 +861,8 @@ static int context_write_data_machine_info(Context *c) {
 
         r = write_env_file(
                         AT_FDCWD,
-                        "/etc/machine-info",
-                        /* headers= */ NULL,
+                        etc_machine_info(),
+                        /* headers= */NULL,
                         l,
                         WRITE_ENV_FILE_LABEL);
         if (r < 0)
@@ -1687,7 +1687,7 @@ static int build_describe_response(Context *c, bool privileged, sd_json_variant
         (void) vsock_get_local_cid(&local_cid);
 
         (void) load_os_release_pairs(/* root= */ NULL, &os_release_pairs);
-        (void) load_env_file_pairs(/* f=*/ NULL, "/etc/machine-info", &machine_info_pairs);
+        (void) load_env_file_pairs(/* f=*/ NULL, etc_machine_info(), &machine_info_pairs);
 
         r = sd_json_buildo(
                         &v,
index 0a96e7a3c9a23436f78a1491ef37c50feaed11c7..72f317f0b0c7eb86d7802072cea6973378655691 100644 (file)
@@ -152,7 +152,7 @@ int vconsole_read_data(Context *c, sd_bus_message *m) {
                 c->vc_cache = sd_bus_message_ref(m);
         }
 
-        fd = RET_NERRNO(open("/etc/vconsole.conf", O_CLOEXEC | O_PATH));
+        fd = RET_NERRNO(open(etc_vconsole_conf(), O_CLOEXEC | O_PATH));
         if (fd == -ENOENT) {
                 c->vc_stat = (struct stat) {};
                 vc_context_clear(&c->vc);
@@ -174,7 +174,7 @@ int vconsole_read_data(Context *c, sd_bus_message *m) {
         x11_context_clear(&c->x11_from_vc);
 
         r = parse_env_file_fd(
-                        fd, "/etc/vconsole.conf",
+                        fd, etc_vconsole_conf(),
                         "KEYMAP",        &c->vc.keymap,
                         "KEYMAP_TOGGLE", &c->vc.toggle,
                         "XKBLAYOUT",     &c->x11_from_vc.layout,
@@ -298,7 +298,7 @@ int vconsole_write_data(Context *c) {
 
         xc = context_get_x11_context(c);
 
-        r = load_env_file(NULL, "/etc/vconsole.conf", &l);
+        r = load_env_file(NULL, etc_vconsole_conf(), &l);
         if (r < 0 && r != -ENOENT)
                 return r;
 
@@ -307,7 +307,7 @@ int vconsole_write_data(Context *c) {
                 return r;
 
         if (strv_isempty(l)) {
-                if (unlink("/etc/vconsole.conf") < 0)
+                if (unlink(etc_vconsole_conf()) < 0)
                         return errno == ENOENT ? 0 : -errno;
 
                 c->vc_stat = (struct stat) {};
@@ -318,7 +318,7 @@ int vconsole_write_data(Context *c) {
         if (r < 0)
                 return r;
 
-        if (stat("/etc/vconsole.conf", &c->vc_stat) < 0)
+        if (stat(etc_vconsole_conf(), &c->vc_stat) < 0)
                 return -errno;
 
         return 0;
diff --git a/src/shared/env-file-label.c b/src/shared/env-file-label.c
new file mode 100644 (file)
index 0000000..c0c9668
--- /dev/null
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/stat.h>
+
+#include "env-file.h"
+#include "env-file-label.h"
+#include "locale-setup.h"
+#include "selinux-util.h"
+
+int write_env_file_label(int dir_fd, const char *fname, char **headers, char **l) {
+        int r;
+
+        r = mac_selinux_create_file_prepare(fname, S_IFREG);
+        if (r < 0)
+                return r;
+
+        r = write_env_file(dir_fd, fname, headers, l);
+
+        mac_selinux_create_file_clear();
+
+        return r;
+}
+
+int write_vconsole_conf_label(char **l) {
+        int r;
+
+        r = mac_selinux_create_file_prepare(etc_vconsole_conf(), S_IFREG);
+        if (r < 0)
+                return r;
+
+        r = write_vconsole_conf(AT_FDCWD, etc_vconsole_conf(), l);
+
+        mac_selinux_create_file_clear();
+
+        return r;
+}
index bb942838b7eacaed8117fca547fc72689926012b..0d21e0482d16d95b564393320ca62c51534b56b9 100644 (file)
@@ -139,7 +139,7 @@ int read_etc_hostname(const char *path, bool substitute_wildcards, char **ret) {
         assert(ret);
 
         if (!path)
-                path = "/etc/hostname";
+                path = etc_hostname();
 
         f = fopen(path, "re");
         if (!f)
index c59bbe2abcef13210d501e032b1fed9887c7197c..6b66c1544366c6ca0a625af7801d05c9c9493dd9 100644 (file)
@@ -67,7 +67,7 @@ static int locale_context_load_conf(LocaleContext *c, LocaleLoadFlag flag) {
         if (!FLAGS_SET(flag, LOCALE_LOAD_LOCALE_CONF))
                 return 0;
 
-        fd = RET_NERRNO(open("/etc/locale.conf", O_CLOEXEC | O_PATH));
+        fd = RET_NERRNO(open(etc_locale_conf(), O_CLOEXEC | O_PATH));
         if (fd == -ENOENT)
                 return 0;
         if (fd < 0)
@@ -83,7 +83,7 @@ static int locale_context_load_conf(LocaleContext *c, LocaleLoadFlag flag) {
         c->st = st;
         locale_context_clear(c);
 
-        r = parse_env_file_fd(fd, "/etc/locale.conf",
+        r = parse_env_file_fd(fd, etc_locale_conf(),
                               "LANG",              &c->locale[VARIABLE_LANG],
                               "LANGUAGE",          &c->locale[VARIABLE_LANGUAGE],
                               "LC_CTYPE",          &c->locale[VARIABLE_LC_CTYPE],
@@ -199,7 +199,7 @@ int locale_context_save(LocaleContext *c, char ***ret_set, char ***ret_unset) {
                 return r;
 
         if (strv_isempty(set)) {
-                if (unlink("/etc/locale.conf") < 0)
+                if (unlink(etc_locale_conf()) < 0)
                         return errno == ENOENT ? 0 : -errno;
 
                 c->st = (struct stat) {};
@@ -213,14 +213,14 @@ int locale_context_save(LocaleContext *c, char ***ret_set, char ***ret_unset) {
 
         r = write_env_file(
                         AT_FDCWD,
-                        "/etc/locale.conf",
+                        etc_locale_conf(),
                         /* headers= */ NULL,
                         set,
                         WRITE_ENV_FILE_LABEL);
         if (r < 0)
                 return r;
 
-        if (stat("/etc/locale.conf", &c->st) < 0)
+        if (stat(etc_locale_conf(), &c->st) < 0)
                 return -errno;
 
         if (ret_set)
@@ -300,3 +300,21 @@ int locale_setup(char ***environment) {
 
         return 0;
 }
+
+const char* etc_locale_conf(void) {
+        static const char *cached = NULL;
+
+        if (!cached)
+                cached = secure_getenv("SYSTEMD_ETC_LOCALE_CONF") ?: "/etc/locale.conf";
+
+        return cached;
+}
+
+const char* etc_vconsole_conf(void) {
+        static const char *cached = NULL;
+
+        if (!cached)
+                cached = secure_getenv("SYSTEMD_ETC_VCONSOLE_CONF") ?: "/etc/vconsole.conf";
+
+        return cached;
+}
index ae32219aa4baceebd4652fc6618f8fd73e9eb37a..fec9046bb7b8fb258c0f4798598be4e94eafce36 100644 (file)
@@ -28,3 +28,6 @@ void locale_context_take(LocaleContext *c, char *l[_VARIABLE_LC_MAX]);
 bool locale_context_equal(const LocaleContext *c, char *l[_VARIABLE_LC_MAX]);
 
 int locale_setup(char ***environment);
+
+const char* etc_locale_conf(void);
+const char* etc_vconsole_conf(void);
index 3a75e6a33edaed8eb478709857af2e49d4728f25..cafd3494cb9b893cf56e71c874f187996a2387b9 100644 (file)
@@ -292,22 +292,32 @@ static int context_write_data_timezone(Context *c) {
 
                 if (access("/usr/share/zoneinfo/UTC", F_OK) < 0) {
 
-                        if (unlink("/etc/localtime") < 0 && errno != ENOENT)
+                        if (unlink(etc_localtime()) < 0 && errno != ENOENT)
                                 return -errno;
 
                         return 0;
                 }
 
-                source = "../usr/share/zoneinfo/UTC";
+                source = "/usr/share/zoneinfo/UTC";
         } else {
-                p = path_join("../usr/share/zoneinfo", c->zone);
+                p = path_join("/usr/share/zoneinfo", c->zone);
                 if (!p)
                         return -ENOMEM;
 
                 source = p;
         }
 
-        return symlink_atomic(source, "/etc/localtime");
+        return symlinkat_atomic_full(source, AT_FDCWD, etc_localtime(),
+                                     !secure_getenv("SYSTEMD_ETC_LOCALTIME"));
+}
+
+static const char* etc_adjtime(void) {
+        static const char *cached = NULL;
+
+        if (!cached)
+                cached = secure_getenv("SYSTEMD_ETC_ADJTIME") ?: "/etc/adjtime";
+
+        return cached;
 }
 
 static int context_write_data_local_rtc(Context *c) {
@@ -316,7 +326,7 @@ static int context_write_data_local_rtc(Context *c) {
 
         assert(c);
 
-        r = read_full_file("/etc/adjtime", &s, NULL);
+        r = read_full_file(etc_adjtime(), &s, NULL);
         if (r < 0) {
                 if (r != -ENOENT)
                         return r;
@@ -368,7 +378,7 @@ static int context_write_data_local_rtc(Context *c) {
                 *mempcpy_typesafe(stpcpy(stpcpy(mempcpy(w, s, a), prepend), c->local_rtc ? "LOCAL" : "UTC"), e, b) = 0;
 
                 if (streq(w, NULL_ADJTIME_UTC)) {
-                        if (unlink("/etc/adjtime") < 0)
+                        if (unlink(etc_adjtime()) < 0)
                                 if (errno != ENOENT)
                                         return -errno;
 
@@ -380,7 +390,7 @@ static int context_write_data_local_rtc(Context *c) {
         if (r < 0)
                 return r;
 
-        return write_string_file("/etc/adjtime", w, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_LABEL);
+        return write_string_file(etc_adjtime(), w, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_LABEL);
 }
 
 static int context_update_ntp_status(Context *c, sd_bus *bus, sd_bus_message *m) {
index 235b278f60101f01b637abea47d7d5e6ea3340dd..bf796a2c0023bd50761fe5f7b0dc506f64a4e4ee 100755 (executable)
@@ -33,6 +33,34 @@ timedatectl set-time "$future_time"
 
 while test ! -f /tmp/clock-changed ; do sleep .5 ; done
 
+mkdir -p /etc/alternate-path
+rm -f /etc/alternate-path/localtime
+
+cat <<EOF >/run/systemd/system.conf
+[Manager]
+ManagerEnvironment=SYSTEMD_ETC_LOCALTIME=/etc/alternate-path/localtime
+EOF
+mkdir -p /run/systemd/system/systemd-timedated.service.d
+cat >/run/systemd/system/systemd-timedated.service.d/override.conf <<EOF
+[Service]
+Environment=SYSTEMD_ETC_LOCALTIME=/run/alternate-path/mylocaltime
+Environment=SYSTEMD_ETC_ADJTIME=/run/alternate-path/myadjtime
+EOF
+systemctl daemon-reload
+
+systemd-run --on-timezone-change touch /tmp/timezone-changed-alternate-path-1
+timedatectl set-timezone Europe/Berlin
+
+while test ! -f /tmp/timezone-changed-alternate-path-1 ; do sleep .5 ; done
+
+systemd-run --on-timezone-change touch /tmp/timezone-changed-alternate-path-2
+timedatectl set-timezone Europe/Kyiv
+
+while test ! -f /tmp/timezone-changed-alternate-path-2 ; do sleep .5 ; done
+
+rm /run/systemd/system.conf /run/systemd/system/systemd-timedated.service.d/override.conf
+systemctl daemon-reload
+
 systemd-analyze log-level info
 
 touch /testok
index 420ebefd017d9b423e49c6102f1baa4ac62b84cd..c1fa30e206d5cc6e1fd1e88c7bd6601693c17d45 100755 (executable)
@@ -403,6 +403,50 @@ EOF
     rm -f /run/systemd/network/ntp99.*
 }
 
+teardown_timedated_alternate_paths() {
+    set +eu
+
+    rm -rf /run/systemd/system/systemd-timedated.service.d
+    systemctl daemon-reload
+    systemctl restart systemd-timedated
+}
+
+testcase_timedated_alternate_paths() {
+    trap teardown_timedated_alternate_paths RETURN
+
+    mkdir -p /run/alternate-path
+    mkdir -p /run/systemd/system/systemd-timedated.service.d
+    cat >/run/systemd/system/systemd-timedated.service.d/override.conf <<EOF
+[Service]
+Environment=SYSTEMD_ETC_LOCALTIME=/run/alternate-path/mylocaltime
+Environment=SYSTEMD_ETC_ADJTIME=/run/alternate-path/myadjtime
+EOF
+    systemctl daemon-reload
+    systemctl restart systemd-timedated
+
+    assert_in "Local time:" "$(timedatectl --no-pager)"
+
+    assert_eq "$(timedatectl --no-pager set-timezone Europe/Kyiv 2>&1)" ""
+    assert_eq "$(readlink /run/alternate-path/mylocaltime | sed 's#^.*zoneinfo/##')" "Europe/Kyiv"
+    assert_in "Time zone: Europe/Kyiv \(EES*T, \+0[0-9]00\)" "$(timedatectl)"
+
+    # Restart to force using get_timezine
+    systemctl restart systemd-timedated
+    assert_in "Time zone: Europe/Kyiv \(EES*T, \+0[0-9]00\)" "$(timedatectl)"
+
+    assert_in "RTC in local TZ: no" "$(timedatectl --no-pager)"
+    assert_rc 0 timedatectl set-local-rtc 1
+    assert_in "RTC in local TZ: yes" "$(timedatectl --no-pager)"
+    assert_eq "$(cat /run/alternate-path/myadjtime)" "0.0 0 0
+0
+LOCAL"
+    assert_rc 0 timedatectl set-local-rtc 0
+    if [[ -e /run/alternate-path/myadjtime ]]; then
+        echo "/run/alternate-path/myadjtime still exists" >&2
+        exit 1
+    fi
+}
+
 run_testcases
 
 touch /testok
index f844ccfcdb02365cebe1ebab550469bb28c3265d..d1eaa8106e388fbc789e2957e7a0258527b9596a 100755 (executable)
@@ -278,6 +278,45 @@ test_wildcard() {
     hostnamectl set-hostname "$SAVED"
 }
 
+teardown_hostnamed_alternate_paths() {
+    set +eu
+
+    rm -rf /run/systemd/system/systemd-hostnamed.service.d
+    systemctl daemon-reload
+    systemctl restart systemd-hostnamed
+    if [[ -f /etc/hostname ]]; then
+        orig=$(cat /etc/hostname)
+        if [[ -n "${orig}" ]]; then
+            hostnamectl hostname "${orig}"
+        fi
+    fi
+}
+
+testcase_hostnamed_alternate_paths() {
+    trap teardown_hostnamed_alternate_paths RETURN
+
+    mkdir -p /run/alternate-path
+
+    mkdir -p /run/systemd/system/systemd-hostnamed.service.d
+    cat >/run/systemd/system/systemd-hostnamed.service.d/override.conf <<EOF
+[Service]
+Environment=SYSTEMD_ETC_HOSTNAME=/run/alternate-path/myhostname
+Environment=SYSTEMD_ETC_MACHINE_INFO=/run/alternate-path/mymachine-info
+EOF
+    systemctl daemon-reload
+    systemctl restart systemd-hostnamed
+
+    assert_rc 0 hostnamectl set-hostname heisenberg
+    assert_rc 0 hostnamectl chassis watch
+
+    output=$(hostnamectl)
+    assert_in "Static hostname: heisenberg" "$output"
+    assert_in "Chassis: watch" "$output"
+    assert_in "heisenberg" "$(cat /run/alternate-path/myhostname)"
+    assert_in "CHASSIS=watch" "$(cat /run/alternate-path/mymachine-info)"
+}
+
+
 run_testcases
 
 touch /testok
index b070e99af16683ad7697a6e0afd694469ba3fc96..b1f19f07b321988fd5440e160a8ac4d5c8aad06c 100755 (executable)
@@ -651,6 +651,53 @@ testcase_locale_gen_leading_space() {
     localectl set-locale en_US.UTF-8
 }
 
+teardown_localed_alternate_paths() {
+    set +eu
+
+    rm -rf /run/systemd/system/systemd-localed.service.d
+    systemctl daemon-reload
+    systemctl restart systemd-localed
+}
+
+testcase_localed_alternate_paths() {
+    trap teardown_localed_alternate_paths RETURN
+
+    mkdir -p /run/alternate-path
+
+    mkdir -p /run/systemd/system/systemd-localed.service.d
+    cat >/run/systemd/system/systemd-localed.service.d/override.conf <<EOF
+[Service]
+Environment=SYSTEMD_ETC_LOCALE_CONF=/run/alternate-path/mylocale.conf
+Environment=SYSTEMD_ETC_VCONSOLE_CONF=/run/alternate-path/myvconsole.conf
+EOF
+    systemctl daemon-reload
+    systemctl restart systemd-localed
+
+    if localectl list-locales | grep "^de_DE.UTF-8$"; then
+        assert_rc 0 localectl set-locale "LANG=de_DE.UTF-8" "LC_CTYPE=C"
+    else
+        skip_locale=1
+    fi
+
+    if localectl list-keymaps | grep -F "^no$"; then
+        assert_rc 0 localectl set-keymap "no"
+    else
+        skip_keymap=1
+    fi
+
+    output=$(localectl)
+
+    if [[ -z "${skip_locale-}" ]]; then
+        assert_in "System Locale: LANG=de_DE.UTF-8" "$output"
+        assert_in "LANG=de_DE.UTF-8" "$(cat /run/alternate-path/mylocale.conf)"
+    fi
+
+    if [[ -z "${skip_keymap-}" ]]; then
+        assert_in "VC Keymap: no" "$output"
+        assert_in "KEYMAP=no" "$(cat /run/alternate-path/myvconsole.conf)"
+    fi
+}
+
 # Make sure the content of kbd-model-map is the one that the tests expect
 # regardless of the version installed on the distro where the testsuite is
 # running on.
index 79a9a77f8b1ff1c42e85e72b2c4d9af13be92d74..3c1ee5ed16382f577ee83b84860a85258036da17 100755 (executable)
@@ -18,6 +18,10 @@ at_exit() {
         rm -fr "$ROOT"
     fi
 
+    if [[ -d /etc/otherpath ]]; then
+        rm -rf /etc/otherpath
+    fi
+
     restore_locale
 }
 
@@ -282,3 +286,36 @@ rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow"
 (! systemd-firstboot --root="$ROOT" --root-shell=/bin/nonexistentshell)
 (! systemd-firstboot --root="$ROOT" --machine-id=invalidmachineid)
 (! systemd-firstboot --root="$ROOT" --timezone=Foo/Bar)
+
+mkdir -p "${ROOT}/etc/otherpath"
+mkdir -p /etc/otherpath
+echo "KEYMAP=us" >/etc/otherpath/vconsole.conf
+echo "LANG=en_US.UTF-8" >/etc/otherpath/locale.conf
+ln -s "../$(readlink /etc/localtime)" /etc/otherpath/localtime
+
+SYSTEMD_ETC_LOCALE_CONF=/etc/otherpath/locale.conf \
+SYSTEMD_ETC_VCONSOLE_CONF=/etc/otherpath/vconsole.conf \
+SYSTEMD_ETC_LOCALTIME=/etc/otherpath/localtime \
+SYSTEMD_ETC_HOSTNAME=/etc/otherpath/hostname \
+systemd-firstboot --root="$ROOT" --copy-locale --copy-keymap --copy-timezone --hostname="weirdpaths"
+
+diff "${ROOT}/etc/otherpath/locale.conf" "/etc/otherpath/locale.conf"
+diff "${ROOT}/etc/otherpath/vconsole.conf" "/etc/otherpath/vconsole.conf"
+grep -q "weirdpaths" "${ROOT}/etc/otherpath/hostname"
+
+[[ "$(readlink /etc/otherpath/localtime)" = "$(readlink "${ROOT}/etc/otherpath/localtime")" ]]
+
+SYSTEMD_ETC_LOCALE_CONF=/etc/otherpath/locale.conf \
+SYSTEMD_ETC_VCONSOLE_CONF=/etc/otherpath/vconsole.conf \
+SYSTEMD_ETC_LOCALTIME=/etc/otherpath/localtime \
+SYSTEMD_ETC_HOSTNAME=/etc/otherpath/hostname \
+systemd-firstboot --root="$ROOT" --force \
+                  --hostname="weirdpaths2" \
+                  --locale=no_NO.UTF-8 \
+                  --keymap=no \
+                  --timezone=Europe/Oslo
+
+grep -q "LANG=no_NO.UTF-8" "${ROOT}/etc/otherpath/locale.conf"
+grep -q "KEYMAP=no" "${ROOT}/etc/otherpath/vconsole.conf"
+grep -q "weirdpaths2" "${ROOT}/etc/otherpath/hostname"
+[[ "$(readlink "${ROOT}/etc/otherpath/localtime")" = "../../usr/share/zoneinfo/Europe/Oslo" ]]