commit 2de2a4d0f68916307e40ac7d192eea7bf95af6c2 Author: Mikaƫl Cluseau Date: Fri Jul 6 19:07:37 2018 +1100 Initial commit diff --git a/cmd/dkl-apply-config/main.go b/cmd/dkl-apply-config/main.go new file mode 100644 index 0000000..d21faec --- /dev/null +++ b/cmd/dkl-apply-config/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "os" + + "novit.nc/direktil/inits/pkg/apply" + "novit.nc/direktil/pkg/config" + dlog "novit.nc/direktil/pkg/log" +) + +var ( + log = dlog.Get("dkl-apply-config") +) + +func main() { + configPath := flag.String("config", "config.yaml", "config to load") + doFiles := flag.Bool("files", false, "apply files") + flag.Parse() + + log.SetConsole(os.Stderr) + + cfg, err := config.Load(*configPath) + if err != nil { + log.Print("failed to load config: ", err) + } + + if *doFiles { + apply.Files(cfg, log) + } +} diff --git a/cmd/dkl-initrd-init/main.go b/cmd/dkl-initrd-init/main.go new file mode 100644 index 0000000..28380ae --- /dev/null +++ b/cmd/dkl-initrd-init/main.go @@ -0,0 +1,190 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" + + yaml "gopkg.in/yaml.v2" + "novit.nc/direktil/pkg/sysfs" +) + +const ( + // VERSION is the current version of init + VERSION = "Direktil init v0.1" + + rootMountFlags = 0 + bootMountFlags = syscall.MS_NOEXEC | syscall.MS_NODEV | syscall.MS_NOSUID | syscall.MS_RDONLY + layerMountFlags = syscall.MS_RDONLY +) + +var ( + bootVersion string +) + +type config struct { + Layers []string `yaml:"layers"` +} + +func main() { + log.Print("Welcome to ", VERSION) + + // essential mounts + mount("none", "/proc", "proc", 0, "") + mount("none", "/sys", "sysfs", 0, "") + mount("none", "/dev", "devtmpfs", 0, "") + + // get the "boot version" + bootVersion = param("version", "current") + log.Printf("booting system %q", bootVersion) + + // find and mount /boot + bootMatch := param("boot", "") + if bootMatch != "" { + bootFS := param("boot.fs", "vfat") + for i := 0; ; i++ { + devNames := sysfs.DeviceByProperty("block", bootMatch) + + if len(devNames) == 0 { + if i > 30 { + fatal("boot partition not found after 30s") + } + log.Print("boot partition not found, retrying") + time.Sleep(1 * time.Second) + continue + } + + devFile := filepath.Join("/dev", devNames[0]) + + log.Print("boot partition found: ", devFile) + + mount(devFile, "/boot", bootFS, bootMountFlags, "") + break + } + } else { + log.Print("Assuming /boot is already populated.") + } + + // load config + cfgPath := param("config", "/boot/config.yaml") + + cfgBytes, err := ioutil.ReadFile(cfgPath) + if err != nil { + fatalf("failed to read %s: %v", cfgPath, err) + } + + cfg := &config{} + if err := yaml.Unmarshal(cfgBytes, cfg); err != nil { + fatal("failed to load config: ", err) + } + + // mount layers + if len(cfg.Layers) == 0 { + fatal("no layers configured!") + } + + log.Printf("wanted layers: %q", cfg.Layers) + + lowers := make([]string, len(cfg.Layers)) + for i, layer := range cfg.Layers { + path := layerPath(layer) + + info, err := os.Stat(path) + if err != nil { + fatal(err) + } + + log.Printf("layer %s found (%d bytes)", layer, info.Size()) + + dir := "/layers/" + layer + + lowers[i] = dir + + loopDev := fmt.Sprintf("/dev/loop%d", i) + losetup(loopDev, path) + + mount(loopDev, dir, "squashfs", layerMountFlags, "") + } + + // prepare system root + mount("mem", "/changes", "tmpfs", 0, "") + + mkdir("/changes/workdir", 0755) + mkdir("/changes/upperdir", 0755) + + mount("overlay", "/system", "overlay", rootMountFlags, + "lowerdir="+strings.Join(lowers, ":")+",upperdir=/changes/upperdir,workdir=/changes/workdir") + mount("/boot", "/system/boot", "", syscall.MS_BIND, "") + + // - write configuration + log.Print("writing /config.yaml") + if err := ioutil.WriteFile("/system/config.yaml", cfgBytes, 0600); err != nil { + fatal("failed: ", err) + } + + // clean zombies + cleanZombies() + + // switch root + log.Print("switching root") + err = syscall.Exec("/sbin/switch_root", []string{"switch_root", "/system", "/sbin/init"}, os.Environ()) + if err != nil { + log.Print("switch_root failed: ", err) + select {} + } +} + +func layerPath(name string) string { + return fmt.Sprintf("/boot/%s/layers/%s.fs", bootVersion, name) +} + +func fatal(v ...interface{}) { + log.Print("*** FATAL ***") + log.Print(v...) + dropToShell() +} + +func fatalf(pattern string, v ...interface{}) { + log.Print("*** FATAL ***") + log.Printf(pattern, v...) + dropToShell() +} + +func dropToShell() { + err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()) + log.Print("shell drop failed: ", err) + select {} +} + +func losetup(dev, file string) { + run("/sbin/losetup", dev, file) +} + +func run(cmd string, args ...string) { + if output, err := exec.Command(cmd, args...).CombinedOutput(); err != nil { + fatalf("command %s %q failed: %v\n%s", cmd, args, err, string(output)) + } +} + +func mkdir(dir string, mode os.FileMode) { + if err := os.MkdirAll(dir, mode); err != nil { + fatalf("mkdir %q failed: %v", dir, err) + } +} + +func mount(source, target, fstype string, flags uintptr, data string) { + if _, err := os.Stat(target); os.IsNotExist(err) { + mkdir(target, 0755) + } + + if err := syscall.Mount(source, target, fstype, flags, data); err != nil { + fatalf("mount %q %q -t %q -o %q failed: %v", source, target, fstype, data, err) + } + log.Printf("mounted %q", target) +} diff --git a/cmd/dkl-initrd-init/params.go b/cmd/dkl-initrd-init/params.go new file mode 100644 index 0000000..d46287c --- /dev/null +++ b/cmd/dkl-initrd-init/params.go @@ -0,0 +1,23 @@ +package main + +import ( + "io/ioutil" + "strings" +) + +func param(name, defaultValue string) (value string) { + ba, err := ioutil.ReadFile("/proc/cmdline") + if err != nil { + fatal("could not read /proc/cmdline: ", err) + } + + prefix := "direktil." + name + "=" + + for _, part := range strings.Split(string(ba), " ") { + if strings.HasPrefix(part, prefix) { + return strings.TrimSpace(part[len(prefix):]) + } + } + + return defaultValue +} diff --git a/cmd/dkl-initrd-init/zombies.go b/cmd/dkl-initrd-init/zombies.go new file mode 100644 index 0000000..5c23f42 --- /dev/null +++ b/cmd/dkl-initrd-init/zombies.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + "syscall" +) + +func cleanZombies() { + var wstatus syscall.WaitStatus + + for { + pid, err := syscall.Wait4(-1, &wstatus, 0, nil) + switch err { + case nil: + log.Printf("collected PID %v", pid) + + case syscall.ECHILD: + return + + default: + log.Printf("unknown error: %v", err) + } + } +} diff --git a/cmd/dkl-system-init/bootstrap.go b/cmd/dkl-system-init/bootstrap.go new file mode 100644 index 0000000..cba135a --- /dev/null +++ b/cmd/dkl-system-init/bootstrap.go @@ -0,0 +1,33 @@ +package main + +import ( + "log" + "os" + "syscall" +) + +func bootstrap() { + mount("proc", "/proc", "proc", 0, "") + mount("sys", "/sys", "sysfs", 0, "") + mount("dev", "/dev", "devtmpfs", syscall.MS_NOSUID, "mode=0755,size=10M") + mount("run", "/run", "tmpfs", 0, "") + + mount("/run", "/var/run", "", syscall.MS_BIND, "") + + mkdir("/run/lock", 0775) + log.Print("/run/lock: correcting owner") + if err := os.Chown("/run/lock", 0, 14); err != nil { + fatal(err) + } +} + +func mount(source, target, fstype string, flags uintptr, data string) { + if _, err := os.Stat(target); os.IsNotExist(err) { + mkdir(target, 0755) + } + + if err := syscall.Mount(source, target, fstype, flags, data); err != nil { + fatalf("mount %q %q -t %q -o %q failed: %v", source, target, fstype, data, err) + } + log.Printf("mounted %q", target) +} diff --git a/cmd/dkl-system-init/configure.go b/cmd/dkl-system-init/configure.go new file mode 100644 index 0000000..fc4e571 --- /dev/null +++ b/cmd/dkl-system-init/configure.go @@ -0,0 +1,267 @@ +package main + +import ( + "bytes" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "syscall" + "time" + + "github.com/sparrc/go-ping" + + "novit.nc/direktil/inits/pkg/apply" + "novit.nc/direktil/inits/pkg/vars" + "novit.nc/direktil/pkg/config" + "novit.nc/direktil/pkg/log" +) + +func init() { + services.Register(configure{}) +} + +type configure struct{} + +func (_ configure) GetName() string { + return "configure" +} + +func (_ configure) CanStart() bool { + return services.HasFlag("service:lvm", "service:udev trigger") +} + +func (_ configure) Run(_ func()) error { + // make root rshared (default in systemd, required by Kubernetes 1.10+) + // equivalent to "mount --make-rshared /" + // see kernel's Documentation/sharedsubtree.txt (search rshared) + if err := syscall.Mount("", "/", "", syscall.MS_SHARED|syscall.MS_REC, ""); err != nil { + fatalf("mount --make-rshared / failed: %v", err) + } + + // - setup root user + if passwordHash := cfg.RootUser.PasswordHash; passwordHash == "" { + run("/usr/bin/passwd", "-d", "root") + } else { + run("/bin/sh", "-c", "chpasswd --encrypted < 3 { + return + } + + time.Sleep(1 * time.Second) + initLog.Taintf(log.Warning, "network[%d] retrying (try: %d)", idx, tries) + goto retry + } +} + +func startNetwork(ifaceName string, idx int, network config.NetworkDef) { + initLog.Taintf(log.Info, "starting network[%d]", idx) + + script := vars.Substitute([]byte(network.Script), cfg) + + c := exec.Command("/bin/sh") + c.Stdin = bytes.NewBuffer(script) + c.Stdout = os.Stdout + c.Stderr = os.Stderr + + // TODO doc + c.Env = append(append(make([]string, 0), os.Environ()...), "IFNAME="+ifaceName) + + if err := c.Run(); err != nil { + links, _ := exec.Command("ip", "link", "ls").CombinedOutput() + fatalf("network setup failed (link list below): %v\n%s", err, string(links)) + } + + networkStarted[ifaceName] = true +} + +func networkPingCheck(ifName string, network config.NetworkDef) (bool, error) { + check := network.Match.Ping + + source := string(vars.Substitute([]byte(check.Source), cfg)) + + run("ip", "addr", "add", source, "dev", ifName) + run("ip", "link", "set", ifName, "up") + + defer func() { + run("ip", "link", "set", ifName, "down") + run("ip", "addr", "del", source, "dev", ifName) + }() + + pinger, err := ping.NewPinger(network.Match.Ping.Target) + if err != nil { + return false, err + } + + pinger.Count = 3 + if check.Count > 0 { + pinger.Count = check.Count + } + + pinger.Timeout = 1 * time.Second + if check.Timeout > 0 { + pinger.Timeout = time.Duration(check.Timeout) * time.Second + } + + pinger.SetPrivileged(true) + pinger.Run() + + return pinger.Statistics().PacketsRecv > 0, nil +} diff --git a/cmd/dkl-system-init/dumb-init-bridge.go b/cmd/dkl-system-init/dumb-init-bridge.go new file mode 100644 index 0000000..8739ec8 --- /dev/null +++ b/cmd/dkl-system-init/dumb-init-bridge.go @@ -0,0 +1,8 @@ +package main + +// void handleSignals(); +import "C" + +func handleChildren() { + C.handleSignals() +} diff --git a/cmd/dkl-system-init/dumb-init.c b/cmd/dkl-system-init/dumb-init.c new file mode 100644 index 0000000..810b6cd --- /dev/null +++ b/cmd/dkl-system-init/dumb-init.c @@ -0,0 +1,139 @@ +// extracted from https://raw.githubusercontent.com/Yelp/dumb-init/master/dumb-init.c + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PRINTERR(...) do { \ + fprintf(stderr, "[dumb-init] " __VA_ARGS__); \ +} while (0) + +#define DEBUG(...) do { \ + if (debug) { \ + PRINTERR(__VA_ARGS__); \ + } \ +} while (0) + +// Signals we care about are numbered from 1 to 31, inclusive. +// (32 and above are real-time signals.) +// TODO: this is likely not portable outside of Linux, or on strange architectures +#define MAXSIG 31 + +// Indices are one-indexed (signal 1 is at index 1). Index zero is unused. +int signal_rewrite[MAXSIG + 1] = {[0 ... MAXSIG] = -1}; + +pid_t child_pid = -1; +char debug = 0; +char use_setsid = 1; + +int translate_signal(int signum) { + if (signum <= 0 || signum > MAXSIG) { + return signum; + } else { + int translated = signal_rewrite[signum]; + if (translated == -1) { + return signum; + } else { + DEBUG("Translating signal %d to %d.\n", signum, translated); + return translated; + } + } +} + +void forward_signal(int signum) { + signum = translate_signal(signum); + if (signum != 0) { + kill(use_setsid ? -child_pid : child_pid, signum); + DEBUG("Forwarded signal %d to children.\n", signum); + } else { + DEBUG("Not forwarding signal %d to children (ignored).\n", signum); + } +} + +/* + * The dumb-init signal handler. + * + * The main job of this signal handler is to forward signals along to our child + * process(es). In setsid mode, this means signaling the entire process group + * rooted at our child. In non-setsid mode, this is just signaling the primary + * child. + * + * In most cases, simply proxying the received signal is sufficient. If we + * receive a job control signal, however, we should not only forward it, but + * also sleep dumb-init itself. + * + * This allows users to run foreground processes using dumb-init and to + * control them using normal shell job control features (e.g. Ctrl-Z to + * generate a SIGTSTP and suspend the process). + * + * The libc manual is useful: + * https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html + * +*/ +void handle_signal(int signum) { + DEBUG("Received signal %d.\n", signum); + if (signum == SIGCHLD) { + int status, exit_status; + pid_t killed_pid; + while ((killed_pid = waitpid(-1, &status, WNOHANG)) > 0) { + if (WIFEXITED(status)) { + exit_status = WEXITSTATUS(status); + DEBUG("A child with PID %d exited with exit status %d.\n", killed_pid, exit_status); + } else { + assert(WIFSIGNALED(status)); + exit_status = 128 + WTERMSIG(status); + DEBUG("A child with PID %d was terminated by signal %d.\n", killed_pid, exit_status - 128); + } + + if (killed_pid == child_pid) { + forward_signal(SIGTERM); // send SIGTERM to any remaining children + DEBUG("Child exited with status %d. Goodbye.\n", exit_status); + exit(exit_status); + } + } + } else { + forward_signal(signum); + if (signum == SIGTSTP || signum == SIGTTOU || signum == SIGTTIN) { + DEBUG("Suspending self due to TTY signal.\n"); + kill(getpid(), SIGSTOP); + } + } +} + +void set_rewrite_to_sigstop_if_not_defined(int signum) { + if (signal_rewrite[signum] == -1) + signal_rewrite[signum] = SIGSTOP; +} + +// A dummy signal handler used for signals we care about. +// On the FreeBSD kernel, ignored signals cannot be waited on by `sigwait` (but +// they can be on Linux). We must provide a dummy handler. +// https://lists.freebsd.org/pipermail/freebsd-ports/2009-October/057340.html +void dummy(int signum) {} + +// ------------------------------------------------------------------------ +// Go entry point +// +void handleSignals() { + sigset_t all_signals; + sigfillset(&all_signals); + sigprocmask(SIG_BLOCK, &all_signals, NULL); + + int i = 0; + for (i = 1; i <= MAXSIG; i++) + signal(i, dummy); + + for (;;) { + int signum; + sigwait(&all_signals, &signum); + handle_signal(signum); + } +} diff --git a/cmd/dkl-system-init/errors.go b/cmd/dkl-system-init/errors.go new file mode 100644 index 0000000..63bbc66 --- /dev/null +++ b/cmd/dkl-system-init/errors.go @@ -0,0 +1,36 @@ +package main + +import ( + "os" + + "novit.nc/direktil/pkg/color" + "novit.nc/direktil/pkg/log" +) + +const ( + endOfInitMessage = ` +.---- END OF INIT -----. +| init process failed. | + ---------------------- +` +) + +func fatal(v ...interface{}) { + initLog.Taint(log.Fatal, v...) + os.Stderr.Write([]byte(color.Red + endOfInitMessage + color.Reset)) + + services.SetFlag("boot-failed") + endOfProcess() +} + +func fatalf(pattern string, v ...interface{}) { + initLog.Taintf(log.Fatal, pattern, v...) + os.Stderr.Write([]byte(color.Red + endOfInitMessage + color.Reset)) + + services.SetFlag("boot-failed") + endOfProcess() +} + +func endOfProcess() { + select {} +} diff --git a/cmd/dkl-system-init/initctl.go b/cmd/dkl-system-init/initctl.go new file mode 100644 index 0000000..b0378fc --- /dev/null +++ b/cmd/dkl-system-init/initctl.go @@ -0,0 +1,49 @@ +package main + +import ( + "bufio" + "io" + "os" + "syscall" +) + +func listenInitctl() { + const f = "/run/initctl" + + if err := syscall.Mkfifo(f, 0700); err != nil { + fatal("can't create "+f+": ", err) + } + + for { + func() { + fifo, err := os.Open(f) + if err != nil { + fatal("can't open "+f+": ", err) + } + defer fifo.Close() + + r := bufio.NewReader(fifo) + + for { + s, err := r.ReadString('\n') + if err == io.EOF { + break + } + if err != nil { + initLog.Print(f+": read error: ", err) + } + + switch s { + case "prepare-shutdown\n": + prepareShutdown() + case "poweroff\n", "shutdown\n": + poweroff() + case "reboot\n": + reboot() + default: + initLog.Printf(f+": unknown command: %q", s) + } + } + }() + } +} diff --git a/cmd/dkl-system-init/lvm.go b/cmd/dkl-system-init/lvm.go new file mode 100644 index 0000000..804eb9c --- /dev/null +++ b/cmd/dkl-system-init/lvm.go @@ -0,0 +1,157 @@ +package main + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "novit.nc/direktil/pkg/config" + "novit.nc/direktil/pkg/log" +) + +const ( + pDevName = "DEVNAME=" +) + +func init() { + go services.WaitPath("/run/lvm/lvmetad.socket") + + services.Register( + &CommandService{ + Name: "lvmetad", + Restart: StdRestart, + Needs: []string{"service:devfs"}, + Command: []string{"lvmetad", "-f"}, + PreExec: func() error { + mkdir("/run/lvm", 0700) + mkdir("/run/lock/lvm", 0700) + + if !dmInProc() { + run("modprobe", "dm-mod") + } + + return nil + }, + }, + &CommandService{ + Name: "lvm", + Needs: []string{"file:/run/lvm/lvmetad.socket"}, + Command: []string{"/bin/sh", "-c", `set -ex +/sbin/lvm pvscan +/sbin/lvm vgscan --mknodes +/sbin/lvm vgchange --sysinit -a ly +`}, + }, + ) +} + +func isDir(path string) bool { + s, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return false + } + fatal("failed to query ", path, ": ", err) + } + + return s.IsDir() +} + +func dmInProc() bool { + for _, f := range []string{"devices", "misc"} { + c, err := ioutil.ReadFile("/proc/" + f) + if err != nil { + fatalf("failed to read %s: %v", f, err) + } + if !bytes.Contains(c, []byte("device-mapper")) { + return false + } + } + return true +} + +func setupVG(udevMatch string) { + dev := "" + try := 0 + +retry: + paths, err := filepath.Glob("/sys/class/block/*") + if err != nil { + fatal("failed to list block devices: ", err) + } + + for _, path := range paths { + // ignore loop devices + if strings.HasPrefix("loop", filepath.Base(path)) { + continue + } + + // fetch udev informations + out, err := exec.Command("udevadm", "info", "-q", "property", path).CombinedOutput() + if err != nil { + initLog.Taintf(log.Warning, "udev query of %q failed: %v\n%s", path, err, string(out)) + continue + } + + propertyLines := strings.Split(strings.TrimSpace(string(out)), "\n") + + devPath := "" + matches := false + + for _, line := range propertyLines { + if strings.HasPrefix(line, pDevName) { + devPath = line[len(pDevName):] + } + + if line == udevMatch { + matches = true + } + + if devPath != "" && matches { + break + } + } + + if devPath != "" && matches { + dev = devPath + break + } + } + + if dev == "" { + time.Sleep(1 * time.Second) + try++ + if try > 30 { + fatal("storage device not found after 30s, failing.") + } + goto retry + } + + initLog.Taint(log.Info, "found storage device at ", dev) + + run("pvcreate", dev) + run("vgcreate", "storage", dev) +} + +func setupLV(volume config.VolumeDef) { + if volume.Extents != "" { + run("lvcreate", "-l", volume.Extents, "-n", volume.Name, "storage") + } else { + run("lvcreate", "-L", volume.Size, "-n", volume.Name, "storage") + } + + args := make([]string, 0) + + switch volume.FS { + case "btrfs": + args = append(args, "-f") + case "ext4": + args = append(args, "-F") + } + + run("mkfs."+volume.FS, append(args, "/dev/storage/"+volume.Name)...) +} diff --git a/cmd/dkl-system-init/main.go b/cmd/dkl-system-init/main.go new file mode 100644 index 0000000..4629959 --- /dev/null +++ b/cmd/dkl-system-init/main.go @@ -0,0 +1,225 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strings" + "syscall" + "time" + + "novit.nc/direktil/pkg/color" + "novit.nc/direktil/pkg/config" + "novit.nc/direktil/pkg/log" +) + +const cfgPath = "/config.yaml" + +var ( + bootVarPrefix = []byte("direktil.var.") + cfg *config.Config + + initLog = log.Get("init") +) + +func main() { + switch filepath.Base(os.Args[0]) { + case "poweroff": + initCommand("poweroff\n") + case "reboot": + initCommand("reboot\n") + } + + if len(os.Args) > 1 { + switch os.Args[1] { + case "0": + initCommand("poweroff\n") + case "6": + initCommand("reboot\n") + default: + fmt.Fprintf(os.Stderr, "unknown args: %v\n", os.Args) + os.Exit(1) + } + } + + if os.Getpid() != 1 { + fmt.Println("not PID 1") + os.Exit(1) + } + + color.Write(os.Stderr, color.Cyan, "Direktil system starting\n") + initLog.SetConsole(os.Stderr) + + go handleChildren() + go handleSignals() + + // handle abnormal ends + defer func() { + if err := recover(); err != nil { + fatal("FATAL: panic in main: ", err) + } else { + fatal("FATAL: exited from main") + } + }() + + // set a reasonable path + os.Setenv("PATH", strings.Join([]string{ + "/usr/local/bin:/usr/local/sbin", + "/usr/bin:/usr/sbin", + "/bin:/sbin", + }, ":")) + + // load the configuration + { + c, err := config.Load(cfgPath) + if err != nil { + fatal("failed to load config: ", err) + } + + if err := os.Remove(cfgPath); err != nil { + initLog.Taint(log.Warning, "failed to remove config: ", err) + } + + cfg = c + } + + // bootstrap the basic things + bootstrap() + + go listenInitctl() + + // start the services + services.Start() + + // Wait for configuration, but timeout to always give a login + ch := make(chan int, 1) + go func() { + services.Wait(func() bool { + return services.HasFlag("configured") || + services.HasFlag("boot-failed") + }) + close(ch) + }() + + select { + case <-time.After(1 * time.Minute): + initLog.Taint(log.Warning, "configuration took too long, allowing login anyway.") + case <-ch: + } + + // Handle CAD command (ctrl+alt+del) + intCh := make(chan os.Signal, 1) + signal.Notify(intCh, syscall.SIGINT) + + syscall.Reboot(syscall.LINUX_REBOOT_CMD_CAD_ON) + go func() { + <-intCh + initLog.Taint(log.Warning, "received ctrl+alt+del, rebooting...") + reboot() + }() + + // Allow login now + go allowLogin() + + // Wait all services + services.WaitAll() + initLog.Taint(log.OK, "all services are started") + + endOfProcess() +} + +func initCommand(c string) { + err := ioutil.WriteFile("/run/initctl", []byte(c), 0600) + if err != nil { + os.Exit(1) + } + os.Exit(0) +} + +func mkdir(dir string, mode os.FileMode) { + if err := os.MkdirAll(dir, mode); err != nil { + fatalf("mkdir %q failed: %v", dir, err) + } +} + +func run(cmd string, args ...string) { + c := exec.Command(cmd, args...) + c.Stdout = os.Stdout + c.Stderr = os.Stderr + + if err := c.Run(); err != nil { + fatalf("command %s %q failed: %v", cmd, args, err) + } +} + +func touch(path string) { + run("touch", path) +} + +func poweroff() { + prepareShutdown() + + initLog.Print("final sync") + syscall.Sync() + + syscall.Reboot(syscall.LINUX_REBOOT_CMD_POWER_OFF) +} + +func reboot() { + prepareShutdown() + + initLog.Print("final sync") + syscall.Sync() + + syscall.Reboot(syscall.LINUX_REBOOT_CMD_RESTART) +} + +func prepareShutdown() { + services.Stop() + initLog.Taint(log.Info, "services stopped") + + log.DisableFiles() + + for try := 0; try < 5; try++ { + initLog.Taint(log.Info, "unmounting filesystems") + + // FIXME: filesystem list should be build from non "nodev" lines in /proc/filesystems + c := exec.Command("umount", "-a", "-t", "ext2,ext3,ext4,vfat,msdos,xfs,btrfs") + c.Stdout = initLog + c.Stderr = initLog + + if err := c.Run(); err != nil { + initLog.Taint(log.Warning, "umounting failed: ", err) + time.Sleep(time.Duration(2*try) * time.Second) + continue + } + + break + } + + initLog.Taint(log.Info, "sync'ing") + exec.Command("sync").Run() +} + +func allowLogin() { + b := make([]byte, 1) + for { + os.Stdout.Write([]byte("\n" + color.Yellow + "[press enter to login]" + color.Reset + "\n\n")) + for { + os.Stdin.Read(b) + if b[0] == '\n' { + break + } + } + + c := exec.Command("/sbin/agetty", "--noclear", "--noissue", "console", "linux") + c.Stdin = os.Stdin + c.Stdout = os.Stdout + c.Stderr = os.Stderr + + c.Run() + } +} diff --git a/cmd/dkl-system-init/runlevel-default.go b/cmd/dkl-system-init/runlevel-default.go new file mode 100644 index 0000000..c6c829b --- /dev/null +++ b/cmd/dkl-system-init/runlevel-default.go @@ -0,0 +1,28 @@ +package main + +func init() { + services.Register( + &CommandService{ + Name: "sysctl", + Needs: []string{"configured"}, + Command: []string{"/usr/sbin/sysctl", "--system"}, + }, + &CommandService{ + Name: "ssh-keygen", + Needs: []string{"files-written"}, + Command: []string{"/usr/bin/ssh-keygen", "-A"}, + }, + &CommandService{ + Name: "sshd", + Restart: StdRestart, + Needs: []string{"service:ssh-keygen"}, + Command: []string{"/usr/sbin/sshd", "-D"}, + }, + &CommandService{ + Name: "chrony", + Restart: StdRestart, + Needs: []string{"configured"}, + Command: []string{"chronyd", "-d"}, + }, + ) +} diff --git a/cmd/dkl-system-init/runlevel-sysinit.go b/cmd/dkl-system-init/runlevel-sysinit.go new file mode 100644 index 0000000..c6d497d --- /dev/null +++ b/cmd/dkl-system-init/runlevel-sysinit.go @@ -0,0 +1,111 @@ +package main + +import ( + "bufio" + "io" + "io/ioutil" + "os" + "strings" + "syscall" +) + +const ( + msSysfs = syscall.MS_NODEV | syscall.MS_NOEXEC | syscall.MS_NOSUID +) + +func init() { + services.Register( + &CommandService{ + Name: "kmod-static-nodes", + PreExec: func() error { + mkdir("/run/tmpfiles.d", 0755) + return nil + }, + Command: []string{"kmod", "static-nodes", "--format=tmpfiles", + "--output=/run/tmpfiles.d/kmod.conf"}, + }, + &Oneshot{ + Name: "devfs", + Func: func() error { + for _, mount := range []struct { + fstype string + target string + mode os.FileMode + flags uintptr + data string + source string + }{ + {"mqueue", "/dev/mqueue", 01777, syscall.MS_NODEV, "", "mqueue"}, + {"devpts", "/dev/pts", 0755, 0, "gid=5", "devpts"}, + {"tmpfs", "/dev/shm", 01777, syscall.MS_NODEV, "mode=1777", "shm"}, + {"tmpfs", "/sys/fs/cgroup", 0755, 0, "mode=755,size=10m", "cgroup"}, + } { + initLog.Print("mounting ", mount.target) + + flags := syscall.MS_NOEXEC | syscall.MS_NOSUID | mount.flags + + mkdir(mount.target, mount.mode) + err := syscall.Mount(mount.source, mount.target, mount.fstype, flags, mount.data) + if err != nil { + fatalf("mount failed: %v", err) + } + } + + // mount cgroup controllers + for line := range readLines("/proc/cgroups") { + parts := strings.Split(line, "\t") + name, enabled := parts[0], parts[3] + + if enabled != "1" { + continue + } + + initLog.Print("mounting cgroup fs for controller ", name) + + mp := "/sys/fs/cgroup/" + name + mkdir(mp, 0755) + mount(name, mp, "cgroup", msSysfs, name) + } + + if err := ioutil.WriteFile("/sys/fs/cgroup/memory/memory.use_hierarchy", + []byte{'1'}, 0644); err != nil { + initLog.Print("failed to enable use_hierarchy in memory cgroup: ", err) + } + + return nil + }, + }, + &CommandService{ + Name: "dmesg", + Command: []string{"dmesg", "-n", "warn"}, + }, + ) +} + +func readLines(path string) chan string { + f, err := os.Open(path) + if err != nil { + fatalf("failed to open %s: %v", path, err) + } + + bf := bufio.NewReader(f) + + ch := make(chan string, 1) + + go func() { + defer f.Close() + defer close(ch) + + for { + line, err := bf.ReadString('\n') + if err == io.EOF { + break + } else if err != nil { + fatalf("error while reading %s: %v", path, err) + } + ch <- line[:len(line)-1] + } + }() + + return ch +} diff --git a/cmd/dkl-system-init/service-registry.go b/cmd/dkl-system-init/service-registry.go new file mode 100644 index 0000000..a2c8b82 --- /dev/null +++ b/cmd/dkl-system-init/service-registry.go @@ -0,0 +1,185 @@ +package main + +import ( + "os" + "sync" + "time" + + "novit.nc/direktil/pkg/log" +) + +var ( + services = ServiceRegistry{ + rw: &sync.RWMutex{}, + cond: sync.NewCond(&sync.Mutex{}), + services: make(map[string]Service), + statuses: make(map[string]ServiceStatus), + flags: make(map[string]bool), + } +) + +type ServiceStatus int + +const ( + Pending ServiceStatus = iota + Starting + Running + Failed + Exited +) + +type ServiceRegistry struct { + rw *sync.RWMutex + cond *sync.Cond + services map[string]Service + statuses map[string]ServiceStatus + flags map[string]bool + started bool +} + +func (sr *ServiceRegistry) Register(services ...Service) { + for _, service := range services { + name := service.GetName() + + if _, ok := sr.services[name]; ok { + fatalf("duplicated service name: %s", name) + } + + sr.services[name] = service + + if sr.started { + go sr.startService(name, service) + } + } +} + +func (sr *ServiceRegistry) Start() { + initLog.Taintf(log.Info, "starting service registry") + sr.started = true + for name, service := range sr.services { + go sr.startService(name, service) + } +} + +func (sr *ServiceRegistry) startService(name string, service Service) { + sr.Set(name, Pending) + sr.Wait(service.CanStart) + + sr.Set(name, Starting) + initLog.Taintf(log.Info, "starting service %s", name) + + if err := service.Run(func() { + sr.Set(name, Running) + sr.SetFlag("service:" + name) + }); err == nil { + initLog.Taintf(log.OK, "service %s finished.", name) + sr.Set(name, Exited) + sr.SetFlag("service:" + name) + + } else { + initLog.Taintf(log.Error, "service %s failed: %v", name, err) + sr.Set(name, Failed) + } +} + +func (sr *ServiceRegistry) Stop() { + wg := sync.WaitGroup{} + wg.Add(len(sr.services)) + + for name, service := range sr.services { + name, service := name, service + go func() { + initLog.Taintf(log.Info, "stopping %s", name) + service.Stop() + wg.Done() + }() + } + + wg.Wait() +} + +func (sr *ServiceRegistry) Wait(check func() bool) { + sr.cond.L.Lock() + defer sr.cond.L.Unlock() + + for { + if check() { + return + } + + sr.cond.Wait() + } +} + +func (sr *ServiceRegistry) WaitAll() { + flags := make([]string, 0, len(sr.services)) + for name, _ := range sr.services { + flags = append(flags, "service:"+name) + } + sr.Wait(sr.HasFlagCond(flags...)) +} + +func (sr *ServiceRegistry) Set(serviceName string, status ServiceStatus) { + sr.cond.L.Lock() + sr.rw.Lock() + defer func() { + sr.rw.Unlock() + sr.cond.L.Unlock() + sr.cond.Broadcast() + }() + + sr.statuses[serviceName] = status +} + +func (sr *ServiceRegistry) HasStatus(status ServiceStatus, serviceNames ...string) bool { + sr.rw.RLock() + defer sr.rw.RUnlock() + + for _, name := range serviceNames { + if sr.statuses[name] != status { + return false + } + } + return true +} + +func (sr *ServiceRegistry) SetFlag(flag string) { + sr.cond.L.Lock() + sr.rw.Lock() + defer func() { + sr.rw.Unlock() + sr.cond.L.Unlock() + sr.cond.Broadcast() + }() + + sr.flags[flag] = true +} + +func (sr *ServiceRegistry) HasFlag(flags ...string) bool { + sr.rw.RLock() + defer sr.rw.RUnlock() + + for _, flag := range flags { + if !sr.flags[flag] { + return false + } + } + return true +} + +func (sr *ServiceRegistry) HasFlagCond(flags ...string) func() bool { + return func() bool { + return sr.HasFlag(flags...) + } +} + +func (sr *ServiceRegistry) WaitPath(path string) { + for { + if _, err := os.Stat(path); err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + sr.SetFlag("file:" + path) +} diff --git a/cmd/dkl-system-init/service-rules.go b/cmd/dkl-system-init/service-rules.go new file mode 100644 index 0000000..6000a14 --- /dev/null +++ b/cmd/dkl-system-init/service-rules.go @@ -0,0 +1,30 @@ +package main + +type ServiceRules struct { + flags []string +} + +func NewServiceRules() *ServiceRules { + return &ServiceRules{make([]string, 0)} +} + +func (r *ServiceRules) Flags(flags ...string) *ServiceRules { + r.flags = append(r.flags, flags...) + return r +} + +func (r *ServiceRules) Services(serviceNames ...string) *ServiceRules { + flags := make([]string, len(serviceNames)) + for i, name := range serviceNames { + flags[i] = "service:" + name + } + return r.Flags(flags...) +} + +func (r ServiceRules) Check() bool { + if !services.HasFlag(r.flags...) { + return false + } + + return true +} diff --git a/cmd/dkl-system-init/service.go b/cmd/dkl-system-init/service.go new file mode 100644 index 0000000..22e7d60 --- /dev/null +++ b/cmd/dkl-system-init/service.go @@ -0,0 +1,151 @@ +package main + +import ( + "os/exec" + "syscall" + "time" + + "novit.nc/direktil/pkg/log" +) + +var ( + // StdRestart is a wait duration between restarts of a service, if you have no inspiration. + StdRestart = 1 * time.Second + + killDelay = 30 * time.Second +) + +// Service represents a service to run as part of the init process. +type Service interface { + GetName() string + CanStart() bool + Run(notify func()) error + Stop() +} + +type CommandService struct { + Name string + Command []string + Restart time.Duration + Needs []string + Provides []string + PreExec func() error + + log *log.Log + stop bool + command *exec.Cmd +} + +var _ Service = &CommandService{} + +func (s *CommandService) GetName() string { + return s.Name +} + +// CanStart is part of the Service interface +func (s *CommandService) CanStart() bool { + return services.HasFlag(s.Needs...) +} + +func (s *CommandService) Stop() { + stopped := false + + s.stop = true + + c := s.command + if c == nil { + return + } + + c.Process.Signal(syscall.SIGTERM) + + go func() { + time.Sleep(killDelay) + if !stopped { + c.Process.Signal(syscall.SIGKILL) + } + }() + + c.Wait() + stopped = true +} + +func (s *CommandService) Run(notify func()) error { + s.stop = false + + if s.log == nil { + s.log = log.Get(s.Name) + } + + isOneshot := s.Restart == time.Duration(0) + + myNotify := func() { + for _, provide := range s.Provides { + services.SetFlag(provide) + } + + notify() + } + + if s.PreExec != nil { + if err := s.PreExec(); err != nil { + return err + } + } + + // Starting + var err error + +retry: + if isOneshot { + // oneshot services are only active after exit + err = s.exec(func() {}) + } else { + err = s.exec(myNotify) + } + + if s.stop { + return err + } + + if isOneshot { + myNotify() + + } else { + // auto-restart service + services.Set(s.Name, Failed) + time.Sleep(s.Restart) + + s.log.Taintf(log.Warning, "-- restarting --") + services.Set(s.Name, Starting) + goto retry + } + + return err +} + +func (s *CommandService) exec(notify func()) error { + c := exec.Command(s.Command[0], s.Command[1:]...) + + s.command = c + defer func() { + s.command = nil + }() + + c.Stdout = s.log + c.Stderr = s.log + + if err := c.Start(); err != nil { + s.log.Taintf(log.Error, "failed to start: %v", err) + return err + } + + notify() + + if err := c.Wait(); err != nil { + s.log.Taintf(log.Error, "failed: %v", err) + return err + } + + return nil +} diff --git a/cmd/dkl-system-init/services-to-migrate.go b/cmd/dkl-system-init/services-to-migrate.go new file mode 100644 index 0000000..48a17a2 --- /dev/null +++ b/cmd/dkl-system-init/services-to-migrate.go @@ -0,0 +1,21 @@ +package main + +/* +func init() { + legacyService := func(name string, needs ...string) *Service { + return &Service{ + Name: name, + PreCond: NewServiceRules().Services(needs...).Check, + Run: func(notify func()) bool { + return Exec(func() {}, "/etc/init.d/"+name, "start", "--nodeps") + }, + } + } + + services.Register([]*Service{ + legacyService("modules-load"), + legacyService("modules", "modules-load"), + legacyService("dmesg", "udev", "modules"), + }...) +} +// */ diff --git a/cmd/dkl-system-init/signal.go b/cmd/dkl-system-init/signal.go new file mode 100644 index 0000000..5b3d9a8 --- /dev/null +++ b/cmd/dkl-system-init/signal.go @@ -0,0 +1,19 @@ +package main + +import ( + "os" + "os/signal" + "syscall" +) + +func handleSignals() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGPWR) + + for sig := range c { + switch sig { + case syscall.SIGPWR: + poweroff() + } + } +} diff --git a/cmd/dkl-system-init/simple-service.go b/cmd/dkl-system-init/simple-service.go new file mode 100644 index 0000000..ac9e352 --- /dev/null +++ b/cmd/dkl-system-init/simple-service.go @@ -0,0 +1,23 @@ +package main + +type Oneshot struct { + Name string + Needs []string + Func func() error +} + +func (s Oneshot) GetName() string { + return s.Name +} + +func (s Oneshot) CanStart() bool { + return services.HasFlag(s.Needs...) +} + +func (s Oneshot) Run(_ func()) error { + return s.Func() +} + +func (s Oneshot) Stop() { + // no-op +} diff --git a/cmd/dkl-system-init/udev.go b/cmd/dkl-system-init/udev.go new file mode 100644 index 0000000..1c75c59 --- /dev/null +++ b/cmd/dkl-system-init/udev.go @@ -0,0 +1,36 @@ +package main + +import ( + "io/ioutil" + "os" +) + +func init() { + services.Register( + &CommandService{ + Name: "udev", + Restart: StdRestart, + Needs: []string{"service:devfs", "service:dmesg", "service:lvm"}, + PreExec: func() error { + if _, err := os.Stat("/proc/net/unix"); os.IsNotExist(err) { + run("modprobe", "unix") + } + if _, err := os.Stat("/proc/sys/kernel/hotplug"); err == nil { + ioutil.WriteFile("/proc/sys/kernel/hotplug", []byte{}, 0644) + } + return nil + }, + Command: []string{"/lib/systemd/systemd-udevd"}, + }, + &CommandService{ + Name: "udev trigger", + Needs: []string{"service:udev"}, + Command: []string{"udevadm", "trigger"}, + }, + &CommandService{ + Name: "udev settle", + Needs: []string{"service:udev trigger"}, + Command: []string{"udevadm", "settle"}, + }, + ) +} diff --git a/cmd/dkl-system-init/user-service.go b/cmd/dkl-system-init/user-service.go new file mode 100644 index 0000000..f88e7fb --- /dev/null +++ b/cmd/dkl-system-init/user-service.go @@ -0,0 +1,105 @@ +package main + +import ( + "bufio" + "bytes" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + yaml "gopkg.in/yaml.v2" + + "novit.nc/direktil/pkg/log" +) + +var ( + reYamlStart = regexp.MustCompile("^#\\s+---\\s*$") +) + +type UserService struct { + Restart int + Needs []string + Provides []string +} + +func loadUserServices() { +retry: + files, err := filepath.Glob("/etc/direktil/services/*") + if err != nil { + initLog.Taint(log.Error, "failed to load user services: ", err) + time.Sleep(10 * time.Second) + goto retry + } + + for _, path := range files { + path := path + go func() { + for { + if err := loadUserService(path); err != nil { + initLog.Taintf(log.Error, "failed to load %s: %v", path, err) + time.Sleep(10 * time.Second) + continue + } + break + } + }() + } +} + +func loadUserService(path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + + defer f.Close() + + r := bufio.NewReader(f) + + yamlBuf := &bytes.Buffer{} + inYaml := false + + for { + line, err := r.ReadString('\n') + if err == io.EOF { + break + } else if err != nil { + return err + } + + if inYaml { + if !strings.HasPrefix(line, "# ") { + break + } + + yamlBuf.WriteString(line[2:]) + + } else if reYamlStart.MatchString(line) { + inYaml = true + } + } + + spec := &UserService{} + + if inYaml { + if err := yaml.Unmarshal(yamlBuf.Bytes(), &spec); err != nil { + return err + } + } + + svc := &CommandService{ + Name: filepath.Base(path), + Restart: time.Duration(spec.Restart) * time.Second, + Needs: spec.Needs, + Provides: spec.Provides, + Command: []string{path}, + } + + initLog.Taintf(log.OK, "user service: %s", path) + services.Register(svc) + + return nil +} diff --git a/pkg/apply/files.go b/pkg/apply/files.go new file mode 100644 index 0000000..1e1a13d --- /dev/null +++ b/pkg/apply/files.go @@ -0,0 +1,67 @@ +package apply + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "novit.nc/direktil/inits/pkg/vars" + "novit.nc/direktil/pkg/config" + dlog "novit.nc/direktil/pkg/log" +) + +// Files writes the files from the given config +func Files(cfg *config.Config, log *dlog.Log) (err error) { + err = writeFile( + "/root/.ssh/authorized_keys", + []byte(strings.Join(cfg.RootUser.AuthorizedKeys, "\n")), + 0600, 0700, cfg, log, + ) + + if err != nil { + return + } + + for _, file := range cfg.Files { + mode := file.Mode + if mode == 0 { + mode = 0644 + } + + content := []byte(file.Content) + + err = writeFile( + file.Path, + content, + mode, + 0755, + cfg, + log, + ) + + if err != nil { + return + } + } + + return +} + +func writeFile(path string, content []byte, fileMode, dirMode os.FileMode, + cfg *config.Config, log *dlog.Log) (err error) { + + if err = os.MkdirAll(filepath.Dir(path), dirMode); err != nil { + return + } + + content = vars.Substitute(content, cfg) + + log.Printf("writing %q, mode %04o, %d bytes", path, fileMode, len(content)) + if err = ioutil.WriteFile(path, content, fileMode); err != nil { + err = fmt.Errorf("failed to write %s: %v", path, err) + } + + return +} diff --git a/pkg/vars/boot.go b/pkg/vars/boot.go new file mode 100644 index 0000000..40ec624 --- /dev/null +++ b/pkg/vars/boot.go @@ -0,0 +1,32 @@ +package vars + +import ( + "bytes" + "fmt" + "io/ioutil" +) + +var ( + bootVarPrefix = []byte("direktil.var.") +) + +func BootArgs() [][]byte { + ba, err := ioutil.ReadFile("/proc/cmdline") + if err != nil { + // should not happen + panic(fmt.Errorf("failed to read /proc/cmdline: ", err)) + } + + return bytes.Split(ba, []byte{' '}) +} + +func BootArgValue(prefix, defaultValue string) string { + prefixB := []byte("direktil." + prefix + "=") + for _, ba := range BootArgs() { + if bytes.HasPrefix(ba, prefixB) { + return string(ba[len(prefixB):]) + } + } + + return defaultValue +} diff --git a/pkg/vars/vars.go b/pkg/vars/vars.go new file mode 100644 index 0000000..073c080 --- /dev/null +++ b/pkg/vars/vars.go @@ -0,0 +1,60 @@ +package vars + +import ( + "bytes" + + "novit.nc/direktil/pkg/config" +) + +type Var struct { + Template []byte + Value []byte +} + +func Vars(cfg *config.Config) []Var { + res := make([]Var, 0) + + for _, arg := range BootArgs() { + if !bytes.HasPrefix(arg, bootVarPrefix) { + continue + } + + parts := bytes.SplitN(arg[len(bootVarPrefix):], []byte{'='}, 2) + + res = append(res, Var{ + Template: append(append([]byte("$(var:"), parts[0]...), ')'), + Value: parts[1], + }) + } + +configVarsLoop: + for _, v := range cfg.Vars { + t := []byte("$(var:" + v.Name + ")") + for _, prev := range res { + if bytes.Equal(prev.Template, t) { + continue configVarsLoop + } + } + + res = append(res, Var{t, []byte(v.Default)}) + } + + return res +} + +// Substitute variables in src +func Substitute(src []byte, cfg *config.Config) (dst []byte) { + dst = src + + for _, bv := range Vars(cfg) { + if !bytes.Contains(dst, bv.Template) { + continue + } + + v := bytes.TrimSpace(bv.Value) + + dst = bytes.Replace(dst, bv.Template, v, -1) + } + + return +}