chiark / gitweb /
firstboot: add new component to query basic system settings on first boot, or when...
authorLennart Poettering <lennart@poettering.net>
Mon, 7 Jul 2014 13:05:37 +0000 (15:05 +0200)
committerLennart Poettering <lennart@poettering.net>
Mon, 7 Jul 2014 13:25:55 +0000 (15:25 +0200)
A new tool "systemd-firstboot" can be used either interactively on boot,
where it will query basic locale, timezone, hostname, root password
information and set it. Or it can be used non-interactively from the
command line when prepareing disk images for booting. When used
non-inertactively the tool can either copy settings from the host, or
take settings on the command line.

$ systemd-firstboot --root=/path/to/my/new/root --copy-locale --copy-root-password --hostname=waldi

The tool will be automatically invoked (interactively) now on first boot
if /etc is found unpopulated.

This also creates the infrastructure for generators to be notified via
an environment variable whether they are running on the first boot, or
not.

16 files changed:
.gitignore
Makefile.am
configure.ac
src/core/execute.c
src/core/main.c
src/core/manager.c
src/core/manager.h
src/core/shutdown.c
src/firstboot/Makefile [new symlink]
src/firstboot/firstboot-generator.c [new file with mode: 0644]
src/firstboot/firstboot.c [new file with mode: 0644]
src/shared/util.c
src/shared/util.h
src/sleep/sleep.c
units/.gitignore
units/systemd-firstboot.service.in [new file with mode: 0644]

index 822f5a6..1ba3d06 100644 (file)
@@ -64,6 +64,8 @@
 /systemd-delta
 /systemd-detect-virt
 /systemd-efi-boot-generator
+/systemd-firstboot
+/systemd-firstboot-generator
 /systemd-fsck
 /systemd-fstab-generator
 /systemd-getty-generator
index 0bf803a..321379c 100644 (file)
@@ -1873,6 +1873,40 @@ INSTALL_DIRS += \
 endif
 
 # ------------------------------------------------------------------------------
+if ENABLE_FIRSTBOOT
+systemd_firstboot_SOURCES = \
+       src/firstboot/firstboot.c
+
+systemd_firstboot_LDADD = \
+       libsystemd-units.la \
+       libsystemd-label.la \
+       libsystemd-capability.la \
+       libsystemd-internal.la \
+       libsystemd-shared.la \
+       -lcrypt
+
+rootbin_PROGRAMS += \
+       systemd-firstboot
+
+nodist_systemunit_DATA += \
+       units/systemd-firstboot.service
+
+EXTRA_DIST += \
+       units/systemd-firstboot.service.in
+
+systemgenerator_PROGRAMS += \
+       systemd-firstboot-generator
+
+systemd_firstboot_generator_SOURCES = \
+       src/firstboot/firstboot-generator.c
+
+systemd_firstboot_generator_LDADD = \
+       libsystemd-label.la \
+       libsystemd-shared.la
+
+endif
+
+# ------------------------------------------------------------------------------
 systemd_machine_id_setup_SOURCES = \
        src/machine-id-setup/machine-id-setup-main.c \
        src/core/machine-id-setup.c \
index 2ee73ca..19af217 100644 (file)
@@ -842,6 +842,14 @@ fi
 AM_CONDITIONAL(ENABLE_SYSUSERS, [test "$have_sysusers" = "yes"])
 
 # ------------------------------------------------------------------------------
+have_firstboot=no
+AC_ARG_ENABLE(firstboot, AS_HELP_STRING([--disable-firstboot], [disable firstboot support]))
+if test "x$enable_firstboot" != "xno"; then
+        have_firstboot=yes
+fi
+AM_CONDITIONAL(ENABLE_FIRSTBOOT, [test "$have_firstboot" = "yes"])
+
+# ------------------------------------------------------------------------------
 have_randomseed=no
 AC_ARG_ENABLE(randomseed, AS_HELP_STRING([--disable-randomseed], [disable randomseed tools]))
 if test "x$enable_randomseed" != "xno"; then
@@ -1291,6 +1299,7 @@ AC_MSG_RESULT([
         quotacheck:              ${have_quotacheck}
         tmpfiles:                ${have_tmpfiles}
         sysusers:                ${have_sysusers}
+        firstboot:               ${have_firstboot}
         randomseed:              ${have_randomseed}
         backlight:               ${have_backlight}
         rfkill:                  ${have_rfkill}
index 88d094e..6e76bd5 100644 (file)
@@ -571,7 +571,7 @@ static int ask_for_confirmation(char *response, char **argv) {
         if (!line)
                 return -ENOMEM;
 
-        r = ask(response, "yns", "Execute %s? [Yes, No, Skip] ", line);
+        r = ask_char(response, "yns", "Execute %s? [Yes, No, Skip] ", line);
 
         restore_confirm_stdio(&saved_stdin, &saved_stdout);
 
index a732c69..e1fc3f3 100644 (file)
@@ -1648,6 +1648,7 @@ int main(int argc, char *argv[]) {
         m->initrd_timestamp = initrd_timestamp;
         m->security_start_timestamp = security_start_timestamp;
         m->security_finish_timestamp = security_finish_timestamp;
+        m->is_first_boot = empty_etc;
 
         manager_set_default_rlimits(m, arg_default_rlimit);
         manager_environment_add(m, NULL, arg_default_environment);
index 0cb2044..9d078c0 100644 (file)
@@ -2476,6 +2476,9 @@ void manager_check_finished(Manager *m) {
         /* Turn off confirm spawn now */
         m->confirm_spawn = false;
 
+        /* This is no longer the first boot */
+        m->is_first_boot = false;
+
         if (dual_timestamp_is_set(&m->finish_timestamp))
                 return;
 
@@ -2628,6 +2631,7 @@ void manager_run_generators(Manager *m) {
         _cleanup_closedir_ DIR *d = NULL;
         const char *generator_path;
         const char *argv[5];
+        const char *env[2];
         int r;
 
         assert(m);
@@ -2661,8 +2665,14 @@ void manager_run_generators(Manager *m) {
         argv[3] = m->generator_unit_path_late;
         argv[4] = NULL;
 
+        if (m->is_first_boot) {
+                env[0] = (char*) "SYSTEMD_FIRST_BOOT=1";
+                env[1] = NULL;
+        } else
+                env[0] = NULL;
+
         RUN_WITH_UMASK(0022)
-                execute_directory(generator_path, d, DEFAULT_TIMEOUT_USEC, (char**) argv);
+                execute_directory(generator_path, d, DEFAULT_TIMEOUT_USEC, (char**) argv, (char**) env);
 
 finish:
         trim_generator_dir(m, &m->generator_unit_path);
index f2c1b0d..eff639d 100644 (file)
@@ -243,6 +243,7 @@ struct Manager {
         bool default_cpu_accounting;
         bool default_memory_accounting;
         bool default_blockio_accounting;
+        bool is_first_boot;
 
         usec_t default_timer_accuracy_usec;
 
index fde3ce9..e7771c9 100644 (file)
@@ -375,7 +375,7 @@ int main(int argc, char *argv[]) {
         arguments[0] = NULL;
         arguments[1] = arg_verb;
         arguments[2] = NULL;
-        execute_directory(SYSTEM_SHUTDOWN_PATH, NULL, DEFAULT_TIMEOUT_USEC, arguments);
+        execute_directory(SYSTEM_SHUTDOWN_PATH, NULL, DEFAULT_TIMEOUT_USEC, arguments, NULL);
 
         if (!in_container && !in_initrd() &&
             access("/run/initramfs/shutdown", X_OK) == 0) {
diff --git a/src/firstboot/Makefile b/src/firstboot/Makefile
new file mode 120000 (symlink)
index 0000000..d0b0e8e
--- /dev/null
@@ -0,0 +1 @@
+../Makefile
\ No newline at end of file
diff --git a/src/firstboot/firstboot-generator.c b/src/firstboot/firstboot-generator.c
new file mode 100644 (file)
index 0000000..6d23f40
--- /dev/null
@@ -0,0 +1,71 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright 2014 Lennart Poettering
+
+  systemd 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.1 of the License, or
+  (at your option) any later version.
+
+  systemd 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 systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include "util.h"
+#include "mkdir.h"
+
+static const char *arg_dest = "/tmp";
+
+static bool is_first_boot(void) {
+        const char *e;
+
+        e = getenv("SYSTEMD_FIRST_BOOT");
+        if (!e)
+                return false;
+
+        return parse_boolean(e) > 0;
+}
+
+int main(int argc, char *argv[]) {
+        int r;
+
+        if (argc > 1 && argc != 4) {
+                log_error("This program takes three or no arguments.");
+                return EXIT_FAILURE;
+        }
+
+        if (argc > 1)
+                arg_dest = argv[2];
+
+        log_set_target(LOG_TARGET_SAFE);
+        log_parse_environment();
+        log_open();
+
+        umask(0022);
+
+        if (is_first_boot()) {
+                const char *t;
+
+                t = strappenda(arg_dest, "/default.target.wants/systemd-firstboot.service");
+
+                mkdir_parents(t, 0755);
+                if (symlink(SYSTEM_DATA_UNIT_PATH "/systemd-firstboot.service", t) < 0 && errno != EEXIST) {
+                        log_error("Failed to create firstboot service symlinks %s: %m", t);
+                        r = -errno;
+                        goto finish;
+                }
+        }
+
+        r = 0;
+
+finish:
+        return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c
new file mode 100644 (file)
index 0000000..56893d0
--- /dev/null
@@ -0,0 +1,930 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright 2014 Lennart Poettering
+
+  systemd 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.1 of the License, or
+  (at your option) any later version.
+
+  systemd 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 systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <shadow.h>
+
+#include "strv.h"
+#include "fileio.h"
+#include "copy.h"
+#include "build.h"
+#include "mkdir.h"
+#include "time-util.h"
+#include "path-util.h"
+#include "locale-util.h"
+#include "ask-password-api.h"
+
+static char *arg_root = NULL;
+static char *arg_locale = NULL;  /* $LANG */
+static char *arg_locale_messages = NULL; /* $LC_MESSAGES */
+static char *arg_timezone = NULL;
+static char *arg_hostname = NULL;
+static sd_id128_t arg_machine_id = {};
+static char *arg_root_password = NULL;
+static bool arg_prompt_locale = false;
+static bool arg_prompt_timezone = false;
+static bool arg_prompt_hostname = false;
+static bool arg_prompt_root_password = false;
+static bool arg_copy_locale = false;
+static bool arg_copy_timezone = false;
+static bool arg_copy_root_password = false;
+
+#define prefix_roota(p) (arg_root ? (const char*) strappenda(arg_root, p) : (const char*) p)
+
+static void clear_string(char *x) {
+
+        if (!x)
+                return;
+
+        /* A delicious drop of snake-oil! */
+        memset(x, 'x', strlen(x));
+}
+
+static bool press_any_key(void) {
+        char k = 0;
+        bool need_nl = true;
+
+        printf("-- Press any key to proceed --");
+        fflush(stdout);
+
+        read_one_char(stdin, &k, (usec_t) -1, &need_nl);
+
+        if (need_nl)
+                putchar('\n');
+
+        return k != 'q';
+}
+
+static void print_welcome(void) {
+        _cleanup_free_ char *pretty_name = NULL;
+        const char *os_release = NULL;
+        static bool done = false;
+        int r;
+
+        if (done)
+                return;
+
+        os_release = prefix_roota("/etc/os-release");
+        r = parse_env_file(os_release, NEWLINE,
+                           "PRETTY_NAME", &pretty_name,
+                           NULL);
+        if (r == -ENOENT) {
+
+                os_release = prefix_roota("/usr/lib/os-release");
+                r = parse_env_file(os_release, NEWLINE,
+                                   "PRETTY_NAME", &pretty_name,
+                                   NULL);
+        }
+
+        if (r < 0 && r != -ENOENT)
+                log_warning("Failed to read os-release file: %s", strerror(-r));
+
+        printf("\nWelcome to your new installation of %s!\nPlease configure a few basic system settings:\n\n",
+               isempty(pretty_name) ? "Linux" : pretty_name);
+
+        press_any_key();
+
+        done = true;
+}
+
+static int show_menu(char **x, unsigned n_columns, unsigned width, unsigned percentage) {
+        unsigned n, per_column, i, j;
+        unsigned break_lines, break_modulo;
+
+        assert(n_columns > 0);
+
+        n = strv_length(x);
+        per_column = (n + n_columns - 1) / n_columns;
+
+        break_lines = lines();
+        if (break_lines > 2)
+                break_lines--;
+
+        /* The first page gets two extra lines, since we want to show
+         * a title */
+        break_modulo = break_lines;
+        if (break_modulo > 3)
+                break_modulo -= 3;
+
+        for (i = 0; i < per_column; i++) {
+
+                for (j = 0; j < n_columns; j ++) {
+                        _cleanup_free_ char *e = NULL;
+
+                        if (j * per_column + i >= n)
+                                break;
+
+                        e = ellipsize(x[j * per_column + i], width, percentage);
+                        if (!e)
+                                return log_oom();
+
+                        printf("%4u) %-*s", j * per_column + i + 1, width, e);
+                }
+
+                putchar('\n');
+
+                /* on the first screen we reserve 2 extra lines for the title */
+                if (i % break_lines == break_modulo) {
+                        if (!press_any_key())
+                                return 0;
+                }
+        }
+
+        return 0;
+}
+
+static int prompt_loop(const char *text, char **l, bool (*is_valid)(const char *name), char **ret) {
+        int r;
+
+        assert(text);
+        assert(is_valid);
+        assert(ret);
+
+        for (;;) {
+                _cleanup_free_ char *p = NULL;
+                unsigned u;
+
+                r = ask_string(&p, "%s %s (empty to skip): ", draw_special_char(DRAW_TRIANGULAR_BULLET), text);
+                if (r < 0) {
+                        log_error("Failed to query user: %s", strerror(-r));
+                        return r;
+                }
+
+                if (isempty(p)) {
+                        log_warning("No data entered, skipping.");
+                        return 0;
+                }
+
+                r = safe_atou(p, &u);
+                if (r >= 0) {
+                        char *c;
+
+                        if (u <= 0 || u > strv_length(l)) {
+                                log_error("Specified entry number out of range.");
+                                continue;
+                        }
+
+                        log_info("Selected '%s'.", l[u-1]);
+
+                        c = strdup(l[u-1]);
+                        if (!c)
+                                return log_oom();
+
+                        free(*ret);
+                        *ret = c;
+                        return 0;
+                }
+
+                if (!is_valid(p)) {
+                        log_error("Entered data invalid.");
+                        continue;
+                }
+
+                free(*ret);
+                *ret = p;
+                p = 0;
+                return 0;
+        }
+}
+
+static int prompt_locale(void) {
+        _cleanup_strv_free_ char **locales = NULL;
+        int r;
+
+        if (arg_locale || arg_locale_messages)
+                return 0;
+
+        if (!arg_prompt_locale)
+                return 0;
+
+        r = get_locales(&locales);
+        if (r < 0) {
+                log_error("Cannot query locales list: %s", strerror(-r));
+                return r;
+        }
+
+        print_welcome();
+
+        printf("\nAvailable Locales:\n\n");
+        r = show_menu(locales, 3, 22, 60);
+        if (r < 0)
+                return r;
+
+        putchar('\n');
+
+        r = prompt_loop("Please enter system locale name or number", locales, locale_is_valid, &arg_locale);
+        if (r < 0)
+                return r;
+
+        if (isempty(arg_locale))
+                return 0;
+
+        r = prompt_loop("Please enter system message locale name or number", locales, locale_is_valid, &arg_locale_messages);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int process_locale(void) {
+        const char *etc_localeconf;
+        char* locales[3];
+        unsigned i = 0;
+        int r;
+
+        etc_localeconf = prefix_roota("/etc/locale.conf");
+        if (faccessat(AT_FDCWD, etc_localeconf, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)
+                return 0;
+
+        if (arg_copy_locale && arg_root) {
+
+                mkdir_parents(etc_localeconf, 0755);
+                r = copy_file("/etc/locale.conf", etc_localeconf, 0, 0644);
+                if (r != -ENOENT) {
+                        if (r < 0) {
+                                log_error("Failed to copy %s: %s", etc_localeconf, strerror(-r));
+                                return r;
+                        }
+
+                        log_info("%s copied.", etc_localeconf);
+                        return 0;
+                }
+        }
+
+        r = prompt_locale();
+        if (r < 0)
+                return r;
+
+        if (!isempty(arg_locale))
+                locales[i++] = strappenda("LANG=", arg_locale);
+        if (!isempty(arg_locale_messages) && !streq(arg_locale_messages, arg_locale))
+                locales[i++] = strappenda("LC_MESSAGES=", arg_locale_messages);
+
+        if (i == 0)
+                return 0;
+
+        locales[i] = NULL;
+
+        mkdir_parents(etc_localeconf, 0755);
+        r = write_env_file(etc_localeconf, locales);
+        if (r < 0) {
+                log_error("Failed to write %s: %s", etc_localeconf, strerror(-r));
+                return r;
+        }
+
+        log_info("%s written.", etc_localeconf);
+        return 0;
+}
+
+static int prompt_timezone(void) {
+        _cleanup_strv_free_ char **zones = NULL;
+        int r;
+
+        if (arg_timezone)
+                return 0;
+
+        if (!arg_prompt_timezone)
+                return 0;
+
+        r = get_timezones(&zones);
+        if (r < 0) {
+                log_error("Cannot query timezone list: %s", strerror(-r));
+                return r;
+        }
+
+        print_welcome();
+
+        printf("\nAvailable Time Zones:\n\n");
+        r = show_menu(zones, 3, 22, 30);
+        if (r < 0)
+                return r;
+
+        putchar('\n');
+
+        r = prompt_loop("Please enter timezone name or number", zones, timezone_is_valid, &arg_timezone);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int process_timezone(void) {
+        const char *etc_localtime, *e;
+        int r;
+
+        etc_localtime = prefix_roota("/etc/localtime");
+        if (faccessat(AT_FDCWD, etc_localtime, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)
+                return 0;
+
+        if (arg_copy_timezone && arg_root) {
+                _cleanup_free_ char *p = NULL;
+
+                r = readlink_malloc("/etc/localtime", &p);
+                if (r != -ENOENT) {
+                        if (r < 0) {
+                                log_error("Failed to read host timezone: %s", strerror(-r));
+                                return r;
+                        }
+
+                        mkdir_parents(etc_localtime, 0755);
+                        if (symlink(p, etc_localtime) < 0) {
+                                log_error("Failed to create %s symlink: %m", etc_localtime);
+                                return -errno;
+                        }
+
+                        log_info("%s copied.", etc_localtime);
+                        return 0;
+                }
+        }
+
+        r = prompt_timezone();
+        if (r < 0)
+                return r;
+
+        if (isempty(arg_timezone))
+                return 0;
+
+        e = strappenda("../usr/share/zoneinfo/", arg_timezone);
+
+        mkdir_parents(etc_localtime, 0755);
+        if (symlink(e, etc_localtime) < 0) {
+                log_error("Failed to create %s symlink: %m", etc_localtime);
+                return -errno;
+        }
+
+        log_info("%s written", etc_localtime);
+        return 0;
+}
+
+static int prompt_hostname(void) {
+        int r;
+
+        if (arg_hostname)
+                return 0;
+
+        if (!arg_prompt_hostname)
+                return 0;
+
+        print_welcome();
+        putchar('\n');
+
+        for (;;) {
+                _cleanup_free_ char *h = NULL;
+
+                r = ask_string(&h, "%s Please enter hostname for new system (empty to skip): ", draw_special_char(DRAW_TRIANGULAR_BULLET));
+                if (r < 0) {
+                        log_error("Failed to query hostname: %s", strerror(-r));
+                        return r;
+                }
+
+                if (isempty(h)) {
+                        log_warning("No hostname entered, skipping.");
+                        break;
+                }
+
+                if (!hostname_is_valid(h)) {
+                        log_error("Specified hostname invalid.");
+                        continue;
+                }
+
+                arg_hostname = h;
+                h = NULL;
+                break;
+        }
+
+        return 0;
+}
+
+static int process_hostname(void) {
+        const char *etc_hostname;
+        int r;
+
+        etc_hostname = prefix_roota("/etc/hostname");
+        if (faccessat(AT_FDCWD, etc_hostname, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)
+                return 0;
+
+        r = prompt_hostname();
+        if (r < 0)
+                return r;
+
+        if (isempty(arg_hostname))
+                return 0;
+
+        mkdir_parents(etc_hostname, 0755);
+        r = write_string_file(etc_hostname, arg_hostname);
+        if (r < 0) {
+                log_error("Failed to write %s: %s", etc_hostname, strerror(-r));
+                return r;
+        }
+
+        log_info("%s written.", etc_hostname);
+        return 0;
+}
+
+static int process_machine_id(void) {
+        const char *etc_machine_id;
+        char id[SD_ID128_STRING_MAX];
+        int r;
+
+        etc_machine_id = prefix_roota("/etc/machine-id");
+        if (faccessat(AT_FDCWD, etc_machine_id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)
+                return 0;
+
+        if (!arg_root)
+                return 0;
+
+        if (sd_id128_equal(arg_machine_id, SD_ID128_NULL))
+                return 0;
+
+        mkdir_parents(etc_machine_id, 0755);
+        r = write_string_file(etc_machine_id, sd_id128_to_string(arg_machine_id, id));
+        if (r < 0) {
+                log_error("Failed to write machine id: %s", strerror(-r));
+                return r;
+        }
+
+        log_info("%s written.", etc_machine_id);
+        return 0;
+}
+
+static int prompt_root_password(void) {
+        const char *msg1, *msg2, *etc_shadow;
+        int r;
+
+        if (arg_root_password)
+                return 0;
+
+        if (!arg_prompt_root_password)
+                return 0;
+
+        etc_shadow = prefix_roota("/etc/shadow");
+        if (faccessat(AT_FDCWD, etc_shadow, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)
+                return 0;
+
+        print_welcome();
+        putchar('\n');
+
+        msg1 = strappenda(draw_special_char(DRAW_TRIANGULAR_BULLET), " Please enter a new root password (empty to skip): ");
+        msg2 = strappenda(draw_special_char(DRAW_TRIANGULAR_BULLET), " Please enter new root password again: ");
+
+        for (;;) {
+                _cleanup_free_ char *a = NULL, *b = NULL;
+
+                r = ask_password_tty(msg1, 0, NULL, &a);
+                if (r < 0) {
+                        log_error("Failed to query root password: %s", strerror(-r));
+                        return r;
+                }
+
+                if (isempty(a)) {
+                        log_warning("No password entered, skipping.");
+                        break;
+                }
+
+                r = ask_password_tty(msg2, 0, NULL, &b);
+                if (r < 0) {
+                        log_error("Failed to query root password: %s", strerror(-r));
+                        clear_string(a);
+                        return r;
+                }
+
+                if (!streq(a, b)) {
+                        log_error("Entered passwords did not match, please try again.");
+                        clear_string(a);
+                        clear_string(b);
+                        continue;
+                }
+
+                clear_string(b);
+                arg_root_password = a;
+                a = NULL;
+                break;
+        }
+
+        return 0;
+}
+
+static int write_root_shadow(const char *path, const struct spwd *p) {
+        _cleanup_fclose_ FILE *f = NULL;
+        assert(path);
+        assert(p);
+
+        mkdir_parents(path, 0755);
+        f = fopen(path, "wex");
+        if (!f)
+                return -errno;
+
+        errno = 0;
+        if (putspent(p, f) != 0)
+                return errno ? -errno : -EIO;
+
+        return fflush_and_check(f);
+}
+
+static int process_root_password(void) {
+
+        static const char table[] =
+                "abcdefghijklmnopqrstuvwxyz"
+                "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                "0123456789"
+                "./";
+
+        struct spwd item = {
+                .sp_namp = (char*) "root",
+                .sp_min = 0,
+                .sp_max = 99999,
+                .sp_warn = 7,
+                .sp_inact = -1,
+                .sp_expire = -1,
+                .sp_flag = (unsigned long) -1, /* this appears to be what everybody does ... */
+        };
+        char salt[3+16+1+1];
+        uint8_t raw[16];
+        unsigned i;
+        char *j;
+
+        const char *etc_shadow;
+        int r;
+
+        etc_shadow = prefix_roota("/etc/shadow");
+        if (faccessat(AT_FDCWD, etc_shadow, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)
+                return 0;
+
+        if (arg_copy_root_password && arg_root) {
+                struct spwd *p;
+
+                errno = 0;
+                p = getspnam("root");
+                if (p || errno != ENOENT) {
+                        if (!p) {
+                                if (!errno)
+                                        errno = EIO;
+
+                                log_error("Failed to find shadow entry for root: %m");
+                                return -errno;
+                        }
+
+                        r = write_root_shadow(etc_shadow, p);
+                        if (r < 0) {
+                                log_error("Failed to write %s: %s", etc_shadow, strerror(-r));
+                                return r;
+                        }
+
+                        log_info("%s copied.", etc_shadow);
+                        return 0;
+                }
+        }
+
+        r = prompt_root_password();
+        if (r < 0)
+                return r;
+
+        if (!arg_root_password)
+                return 0;
+
+        r = dev_urandom(raw, 16);
+        if (r < 0) {
+                log_error("Failed to get salt: %s", strerror(-r));
+                return r;
+        }
+
+        /* We only bother with SHA512 hashed passwords, the rest is legacy, and we don't do legacy. */
+        assert_cc(sizeof(table) == 64 + 1);
+        j = stpcpy(salt, "$6$");
+        for (i = 0; i < 16; i++)
+                j[i] = table[raw[i] & 63];
+        j[i++] = '$';
+        j[i] = 0;
+
+        errno = 0;
+        item.sp_pwdp = crypt(arg_root_password, salt);
+        if (!item.sp_pwdp) {
+                if (!errno)
+                        errno = -EINVAL;
+
+                log_error("Failed to encrypt password: %m");
+                return -errno;
+        }
+
+        item.sp_lstchg = (long) (now(CLOCK_REALTIME) / USEC_PER_DAY);
+
+        r = write_root_shadow(etc_shadow, &item);
+        if (r < 0) {
+                log_error("Failed to write %s: %s", etc_shadow, strerror(-r));
+                return r;
+        }
+
+        log_info("%s written.", etc_shadow);
+        return 0;
+}
+
+static int help(void) {
+
+        printf("%s [OPTIONS...]\n\n"
+               "Configures basic settings of the system.\n\n"
+               "  -h --help                    Show this help\n"
+               "     --version                 Show package version\n"
+               "     --root=PATH               Operate on an alternate filesystem root\n"
+               "     --locale=LOCALE           Set primary locale (LANG=)\n"
+               "     --locale-messages=LOCALE  Set message locale (LC_MESSAGES=)\n"
+               "     --timezone=TIMEZONE       Set timezone\n"
+               "     --hostname=NAME           Set host name\n"
+               "     --machine-ID=ID           Set machine ID\n"
+               "     --root-password=PASSWORD  Set root password\n"
+               "     --root-password-file=FILE Set root password from file\n"
+               "     --prompt-locale           Prompt the user for locale settings\n"
+               "     --prompt-timezone         Prompt the user for timezone\n"
+               "     --prompt-hostname         Prompt the user for hostname\n"
+               "     --prompt-root-password    Prompt the user for root password\n"
+               "     --prompt                  Prompt for locale, timezone, hostname, root password\n"
+               "     --copy-locale             Copy locale from host\n"
+               "     --copy-timezone           Copy timezone from host\n"
+               "     --copy-root-password      Copy root password from host\n"
+               "     --copy                    Copy locale, timezone, root password\n"
+               "     --setup-machine-id        Generate a new random machine ID\n",
+               program_invocation_short_name);
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+        enum {
+                ARG_VERSION = 0x100,
+                ARG_ROOT,
+                ARG_LOCALE,
+                ARG_LOCALE_MESSAGES,
+                ARG_TIMEZONE,
+                ARG_HOSTNAME,
+                ARG_MACHINE_ID,
+                ARG_ROOT_PASSWORD,
+                ARG_ROOT_PASSWORD_FILE,
+                ARG_PROMPT,
+                ARG_PROMPT_LOCALE,
+                ARG_PROMPT_TIMEZONE,
+                ARG_PROMPT_HOSTNAME,
+                ARG_PROMPT_ROOT_PASSWORD,
+                ARG_COPY,
+                ARG_COPY_LOCALE,
+                ARG_COPY_TIMEZONE,
+                ARG_COPY_ROOT_PASSWORD,
+                ARG_SETUP_MACHINE_ID,
+        };
+
+        static const struct option options[] = {
+                { "help",                 no_argument,       NULL, 'h'                      },
+                { "version",              no_argument,       NULL, ARG_VERSION              },
+                { "root",                 required_argument, NULL, ARG_ROOT                 },
+                { "locale",               required_argument, NULL, ARG_LOCALE               },
+                { "locale-messages",      required_argument, NULL, ARG_LOCALE_MESSAGES      },
+                { "timezone",             required_argument, NULL, ARG_TIMEZONE             },
+                { "hostname",             required_argument, NULL, ARG_HOSTNAME             },
+                { "machine-id",           required_argument, NULL, ARG_MACHINE_ID           },
+                { "root-password",        required_argument, NULL, ARG_ROOT_PASSWORD        },
+                { "root-password-file",   required_argument, NULL, ARG_ROOT_PASSWORD_FILE   },
+                { "prompt",               no_argument,       NULL, ARG_PROMPT               },
+                { "prompt-locale",        no_argument,       NULL, ARG_PROMPT_LOCALE        },
+                { "prompt-timezone",      no_argument,       NULL, ARG_PROMPT_TIMEZONE      },
+                { "prompt-hostname",      no_argument,       NULL, ARG_PROMPT_HOSTNAME      },
+                { "prompt-root-password", no_argument,       NULL, ARG_PROMPT_ROOT_PASSWORD },
+                { "copy",                 no_argument,       NULL, ARG_COPY                 },
+                { "copy-locale",          no_argument,       NULL, ARG_COPY_LOCALE          },
+                { "copy-timezone",        no_argument,       NULL, ARG_COPY_TIMEZONE        },
+                { "copy-root-password",   no_argument,       NULL, ARG_COPY_ROOT_PASSWORD   },
+                { "setup-machine-id",     no_argument,       NULL, ARG_SETUP_MACHINE_ID     },
+                {}
+        };
+
+        int r, c;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) {
+
+                switch (c) {
+
+                case 'h':
+                        return help();
+
+                case ARG_VERSION:
+                        puts(PACKAGE_STRING);
+                        puts(SYSTEMD_FEATURES);
+                        return 0;
+
+                case ARG_ROOT:
+                        free(arg_root);
+                        arg_root = path_make_absolute_cwd(optarg);
+                        if (!arg_root)
+                                return log_oom();
+
+                        path_kill_slashes(arg_root);
+
+                        if (path_equal(arg_root, "/")) {
+                                free(arg_root);
+                                arg_root = NULL;
+                        }
+
+                        break;
+
+                case ARG_LOCALE:
+                        if (!locale_is_valid(optarg)) {
+                                log_error("Locale %s is not valid.", optarg);
+                                return -EINVAL;
+                        }
+
+                        free(arg_locale);
+                        arg_locale = strdup(optarg);
+                        if (!arg_locale)
+                                return log_oom();
+
+                        break;
+
+                case ARG_LOCALE_MESSAGES:
+                        if (!locale_is_valid(optarg)) {
+                                log_error("Locale %s is not valid.", optarg);
+                                return -EINVAL;
+                        }
+
+                        free(arg_locale_messages);
+                        arg_locale_messages = strdup(optarg);
+                        if (!arg_locale_messages)
+                                return log_oom();
+
+                        break;
+
+                case ARG_TIMEZONE:
+                        if (!timezone_is_valid(optarg)) {
+                                log_error("Timezone %s is not valid.", optarg);
+                                return -EINVAL;
+                        }
+
+                        free(arg_timezone);
+                        arg_timezone = strdup(optarg);
+                        if (!arg_timezone)
+                                return log_oom();
+
+                        break;
+
+                case ARG_ROOT_PASSWORD:
+                        free(arg_root_password);
+                        arg_root_password = strdup(optarg);
+                        if (!arg_root_password)
+                                return log_oom();
+
+                        break;
+
+                case ARG_ROOT_PASSWORD_FILE:
+                        free(arg_root_password);
+                        arg_root_password  = NULL;
+
+                        r = read_one_line_file(optarg, &arg_root_password);
+                        if (r < 0) {
+                                log_error("Failed to read %s: %s", optarg, strerror(-r));
+                                return r;
+                        }
+
+                        break;
+
+                case ARG_HOSTNAME:
+                        if (!hostname_is_valid(optarg)) {
+                                log_error("Host name %s is not valid.", optarg);
+                                return -EINVAL;
+                        }
+
+                        free(arg_hostname);
+                        arg_hostname = strdup(optarg);
+                        if (!arg_hostname)
+                                return log_oom();
+
+                        break;
+
+                case ARG_MACHINE_ID:
+                        if (sd_id128_from_string(optarg, &arg_machine_id) < 0) {
+                                log_error("Failed to parse machine id %s.", optarg);
+                                return -EINVAL;
+                        }
+
+                        break;
+
+                case ARG_PROMPT:
+                        arg_prompt_locale = arg_prompt_timezone = arg_prompt_hostname = arg_prompt_root_password = true;
+                        break;
+
+                case ARG_PROMPT_LOCALE:
+                        arg_prompt_locale = true;
+                        break;
+
+                case ARG_PROMPT_TIMEZONE:
+                        arg_prompt_timezone = true;
+                        break;
+
+                case ARG_PROMPT_HOSTNAME:
+                        arg_prompt_hostname = true;
+                        break;
+
+                case ARG_PROMPT_ROOT_PASSWORD:
+                        arg_prompt_root_password = true;
+                        break;
+
+                case ARG_COPY:
+                        arg_copy_locale = arg_copy_timezone = arg_copy_root_password = true;
+
+                case ARG_COPY_LOCALE:
+                        arg_copy_locale = true;
+                        break;
+
+                case ARG_COPY_TIMEZONE:
+                        arg_copy_timezone = true;
+                        break;
+
+                case ARG_COPY_ROOT_PASSWORD:
+                        arg_copy_root_password = true;
+                        break;
+
+                case ARG_SETUP_MACHINE_ID:
+
+                        r = sd_id128_randomize(&arg_machine_id);
+                        if (r < 0) {
+                                log_error("Failed to generate randomized machine ID: %s", strerror(-r));
+                                return r;
+                        }
+
+                        break;
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached("Unhandled option");
+                }
+        }
+
+        return 1;
+}
+
+int main(int argc, char *argv[]) {
+        int r;
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                goto finish;
+
+        log_set_target(LOG_TARGET_AUTO);
+        log_parse_environment();
+        log_open();
+
+        umask(0022);
+
+        r = process_locale();
+        if (r < 0)
+                goto finish;
+
+        r = process_timezone();
+        if (r < 0)
+                goto finish;
+
+        r = process_hostname();
+        if (r < 0)
+                goto finish;
+
+        r = process_machine_id();
+        if (r < 0)
+                goto finish;
+
+        r = process_root_password();
+        if (r < 0)
+                goto finish;
+
+finish:
+        free(arg_root);
+        free(arg_locale);
+        free(arg_locale_messages);
+        free(arg_timezone);
+        free(arg_hostname);
+        clear_string(arg_root_password);
+        free(arg_root_password);
+
+        return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}
index d223ecf..bef8730 100644 (file)
@@ -1624,7 +1624,7 @@ int read_one_char(FILE *f, char *ret, usec_t t, bool *need_nl) {
         return 0;
 }
 
-int ask(char *ret, const char *replies, const char *text, ...) {
+int ask_char(char *ret, const char *replies, const char *text, ...) {
         int r;
 
         assert(ret);
@@ -1672,6 +1672,49 @@ int ask(char *ret, const char *replies, const char *text, ...) {
         }
 }
 
+int ask_string(char **ret, const char *text, ...) {
+        assert(ret);
+        assert(text);
+
+        for (;;) {
+                char line[LINE_MAX];
+                va_list ap;
+
+                if (on_tty())
+                        fputs(ANSI_HIGHLIGHT_ON, stdout);
+
+                va_start(ap, text);
+                vprintf(text, ap);
+                va_end(ap);
+
+                if (on_tty())
+                        fputs(ANSI_HIGHLIGHT_OFF, stdout);
+
+                fflush(stdout);
+
+                errno = 0;
+                if (!fgets(line, sizeof(line), stdin))
+                        return errno ? -errno : -EIO;
+
+                if (!endswith(line, "\n"))
+                        putchar('\n');
+                else {
+                        char *s;
+
+                        if (isempty(line))
+                                continue;
+
+                        truncate_nl(line);
+                        s = strdup(line);
+                        if (!s)
+                                return -ENOMEM;
+
+                        *ret = s;
+                        return 0;
+                }
+        }
+}
+
 int reset_terminal_fd(int fd, bool switch_to_text) {
         struct termios termios;
         int r = 0;
@@ -3752,7 +3795,7 @@ bool dirent_is_file_with_suffix(const struct dirent *de, const char *suffix) {
         return endswith(de->d_name, suffix);
 }
 
-void execute_directory(const char *directory, DIR *d, usec_t timeout, char *argv[]) {
+void execute_directory(const char *directory, DIR *d, usec_t timeout, char *argv[], char *env[]) {
         pid_t executor_pid;
         int r;
 
@@ -3783,6 +3826,14 @@ void execute_directory(const char *directory, DIR *d, usec_t timeout, char *argv
 
                 assert_se(prctl(PR_SET_PDEATHSIG, SIGTERM) == 0);
 
+                if (!strv_isempty(env)) {
+                        char **i;
+
+                        STRV_FOREACH(i, env)
+                                putenv(*i);
+                }
+
+
                 if (!d) {
                         d = _d = opendir(directory);
                         if (!d) {
@@ -3807,7 +3858,8 @@ void execute_directory(const char *directory, DIR *d, usec_t timeout, char *argv
                         if (!dirent_is_file(de))
                                 continue;
 
-                        if (asprintf(&path, "%s/%s", directory, de->d_name) < 0) {
+                        path = strjoin(directory, "/", de->d_name, NULL);
+                        if (!path) {
                                 log_oom();
                                 _exit(EXIT_FAILURE);
                         }
index 8544940..7c9842b 100644 (file)
@@ -505,7 +505,7 @@ bool tty_is_console(const char *tty) _pure_;
 int vtnr_from_tty(const char *tty);
 const char *default_term_for_tty(const char *tty);
 
-void execute_directory(const char *directory, DIR *_d, usec_t timeout, char *argv[]);
+void execute_directory(const char *directory, DIR *_d, usec_t timeout, char *argv[], char *env[]);
 
 int kill_and_sigcont(pid_t pid, int sig);
 
index 5adbea5..3b0e927 100644 (file)
@@ -110,7 +110,7 @@ static int execute(char **modes, char **states) {
         arguments[1] = (char*) "pre";
         arguments[2] = arg_verb;
         arguments[3] = NULL;
-        execute_directory(SYSTEM_SLEEP_PATH, NULL, DEFAULT_TIMEOUT_USEC, arguments);
+        execute_directory(SYSTEM_SLEEP_PATH, NULL, DEFAULT_TIMEOUT_USEC, arguments, NULL);
 
         log_struct(LOG_INFO,
                    MESSAGE_ID(SD_MESSAGE_SLEEP_START),
@@ -129,7 +129,7 @@ static int execute(char **modes, char **states) {
                    NULL);
 
         arguments[1] = (char*) "post";
-        execute_directory(SYSTEM_SLEEP_PATH, NULL, DEFAULT_TIMEOUT_USEC, arguments);
+        execute_directory(SYSTEM_SLEEP_PATH, NULL, DEFAULT_TIMEOUT_USEC, arguments, NULL);
 
         return r;
 }
index 8ae6ca8..5d68927 100644 (file)
@@ -23,6 +23,7 @@
 /systemd-backlight@.service
 /systemd-binfmt.service
 /systemd-bus-proxyd@.service
+/systemd-firstboot.service
 /systemd-fsck-root.service
 /systemd-fsck@.service
 /systemd-halt.service
diff --git a/units/systemd-firstboot.service.in b/units/systemd-firstboot.service.in
new file mode 100644 (file)
index 0000000..e7ae745
--- /dev/null
@@ -0,0 +1,23 @@
+#  This file is part of systemd.
+#
+#  systemd 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.1 of the License, or
+#  (at your option) any later version.
+
+[Unit]
+Description=First Boot Wizard
+Documentation=man:systemd-firstboot(8)
+DefaultDependencies=no
+Conflicts=shutdown.target
+After=systemd-readahead-collect.service systemd-readahead-replay.service systemd-remount-fs.service systemd-sysusers.service
+Before=sysinit.target shutdown.target
+ConditionPathIsReadWrite=/etc
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=@rootbindir@/systemd-firstboot --prompt-locale --prompt-timezone --prompt-root-password
+StandardOutput=tty
+StandardInput=tty
+StandardError=tty