From 7741051b2032d79ca0c71803b6a6aee4c27aab6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Cluseau?= Date: Fri, 8 Mar 2019 12:21:29 +1100 Subject: [PATCH] global command --- .gitignore | 1 + cmd/dkl-system-init/bootstrap.go | 33 --- cmd/dkl-system-init/configure.go | 267 ------------------ cmd/dkl-system-init/errors.go | 36 --- cmd/dkl-system-init/initctl.go | 49 ---- cmd/dkl-system-init/lvm.go | 167 ----------- cmd/dkl-system-init/main.go | 225 --------------- cmd/dkl-system-init/reaper.go | 44 --- cmd/dkl-system-init/reaper_test.go | 44 --- cmd/dkl-system-init/runlevel-default.go | 28 -- cmd/dkl-system-init/runlevel-sysinit.go | 111 -------- cmd/dkl-system-init/service-registry.go | 185 ------------ cmd/dkl-system-init/service-rules.go | 30 -- cmd/dkl-system-init/service.go | 151 ---------- cmd/dkl-system-init/services-to-migrate.go | 21 -- cmd/dkl-system-init/signal.go | 19 -- cmd/dkl-system-init/simple-service.go | 23 -- cmd/dkl-system-init/udev.go | 36 --- cmd/dkl-system-init/user-service.go | 105 ------- cmd/dkl/main.go | 19 ++ go.mod | 6 +- go.sum | 6 + layer/etc/init.d/dkl-boot | 15 + layer/etc/init.d/dkl-default | 16 ++ layer/etc/init.d/dkl-user-services | 30 ++ layer/etc/inittab | 54 ++++ layer/etc/runlevels/boot/dkl-boot | 1 + layer/etc/runlevels/default/dkl-default | 1 + layer/etc/runlevels/default/dkl-user-services | 1 + modd.conf | 6 + pkg/apply/files.go | 13 +- pkg/cmd/init/boot/boot.go | 43 +++ pkg/cmd/init/boot/files.go | 60 ++++ pkg/cmd/init/boot/lvm.go | 186 ++++++++++++ pkg/cmd/init/boot/network.go | 155 ++++++++++ pkg/cmd/init/default/default.go | 16 ++ pkg/cmd/init/init.go | 34 +++ pkg/cmd/init/service/service.go | 90 ++++++ pkg/sys/config.go | 36 +++ pkg/sys/mount.go | 19 ++ pkg/sys/sys.go | 87 ++++++ 41 files changed, 886 insertions(+), 1583 deletions(-) create mode 100644 .gitignore delete mode 100644 cmd/dkl-system-init/bootstrap.go delete mode 100644 cmd/dkl-system-init/configure.go delete mode 100644 cmd/dkl-system-init/errors.go delete mode 100644 cmd/dkl-system-init/initctl.go delete mode 100644 cmd/dkl-system-init/lvm.go delete mode 100644 cmd/dkl-system-init/main.go delete mode 100644 cmd/dkl-system-init/reaper.go delete mode 100644 cmd/dkl-system-init/reaper_test.go delete mode 100644 cmd/dkl-system-init/runlevel-default.go delete mode 100644 cmd/dkl-system-init/runlevel-sysinit.go delete mode 100644 cmd/dkl-system-init/service-registry.go delete mode 100644 cmd/dkl-system-init/service-rules.go delete mode 100644 cmd/dkl-system-init/service.go delete mode 100644 cmd/dkl-system-init/services-to-migrate.go delete mode 100644 cmd/dkl-system-init/signal.go delete mode 100644 cmd/dkl-system-init/simple-service.go delete mode 100644 cmd/dkl-system-init/udev.go delete mode 100644 cmd/dkl-system-init/user-service.go create mode 100644 cmd/dkl/main.go create mode 100755 layer/etc/init.d/dkl-boot create mode 100755 layer/etc/init.d/dkl-default create mode 100755 layer/etc/init.d/dkl-user-services create mode 100644 layer/etc/inittab create mode 120000 layer/etc/runlevels/boot/dkl-boot create mode 120000 layer/etc/runlevels/default/dkl-default create mode 120000 layer/etc/runlevels/default/dkl-user-services create mode 100644 modd.conf create mode 100644 pkg/cmd/init/boot/boot.go create mode 100644 pkg/cmd/init/boot/files.go create mode 100644 pkg/cmd/init/boot/lvm.go create mode 100644 pkg/cmd/init/boot/network.go create mode 100644 pkg/cmd/init/default/default.go create mode 100644 pkg/cmd/init/init.go create mode 100644 pkg/cmd/init/service/service.go create mode 100644 pkg/sys/config.go create mode 100644 pkg/sys/mount.go create mode 100644 pkg/sys/sys.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ceeb05b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/tmp diff --git a/cmd/dkl-system-init/bootstrap.go b/cmd/dkl-system-init/bootstrap.go deleted file mode 100644 index cba135a..0000000 --- a/cmd/dkl-system-init/bootstrap.go +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index fc4e571..0000000 --- a/cmd/dkl-system-init/configure.go +++ /dev/null @@ -1,267 +0,0 @@ -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/errors.go b/cmd/dkl-system-init/errors.go deleted file mode 100644 index 63bbc66..0000000 --- a/cmd/dkl-system-init/errors.go +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index b0378fc..0000000 --- a/cmd/dkl-system-init/initctl.go +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 3b05c5d..0000000 --- a/cmd/dkl-system-init/lvm.go +++ /dev/null @@ -1,167 +0,0 @@ -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") - } - - // wait the device link - devPath := "/dev/storage/" + volume.Name - for i := 0; i < 300; i++ { - _, err := os.Stat(devPath) - if err == nil { - break - } - time.Sleep(100 * time.Millisecond) - } - - 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, devPath)...) -} diff --git a/cmd/dkl-system-init/main.go b/cmd/dkl-system-init/main.go deleted file mode 100644 index 4629959..0000000 --- a/cmd/dkl-system-init/main.go +++ /dev/null @@ -1,225 +0,0 @@ -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/reaper.go b/cmd/dkl-system-init/reaper.go deleted file mode 100644 index 42850bb..0000000 --- a/cmd/dkl-system-init/reaper.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/signal" - "sync" - "syscall" - - "golang.org/x/sys/unix" - "novit.nc/direktil/pkg/log" -) - -var reapLock = sync.RWMutex{} - -func handleChildren() { - sigchld := make(chan os.Signal, 2048) - signal.Notify(sigchld, syscall.SIGCHLD) - - // set us as a sub-reaper - if err := unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0); err != nil { - initLog.Taintf(log.Error, "reaper: failed to set myself a child sub-reaper: %v", err) - } - - for range sigchld { - reapChildren() - } -} - -func reapChildren() { - reapLock.Lock() - defer reapLock.Unlock() - for { - pid, err := syscall.Wait4(-1, nil, syscall.WNOHANG, nil) - if err != nil && err != syscall.ECHILD { - initLog.Taintf(log.Warning, "reaper: wait4 failed: %v", err) - fmt.Printf("reaper: wait4 failed: %v\n", err) - break - } - if pid <= 0 { - break - } - } -} diff --git a/cmd/dkl-system-init/reaper_test.go b/cmd/dkl-system-init/reaper_test.go deleted file mode 100644 index 651d686..0000000 --- a/cmd/dkl-system-init/reaper_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "os" - "os/exec" - "sync" - "testing" -) - -func _TestReap(t *testing.T) { - truePath, err := exec.LookPath("true") - if err != nil { - t.Log("true binary not found, ignoring this test.") - return - } - - go handleChildren() - - count := 1000 - - wg := &sync.WaitGroup{} - wg.Add(count) - for i := 0; i < count; i++ { - i := i - go func() { - cmd := exec.Command(truePath) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - if err := cmd.Run(); err != nil { - t.Errorf("[%d] %v", i, err) - } - wg.Done() - }() - } - - wg.Wait() - - cmd := exec.Command("sh", "-c", "ps aux |grep Z") - cmd.Stdout = os.Stdout - cmd.Run() - - t.Fail() -} diff --git a/cmd/dkl-system-init/runlevel-default.go b/cmd/dkl-system-init/runlevel-default.go deleted file mode 100644 index c6c829b..0000000 --- a/cmd/dkl-system-init/runlevel-default.go +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index c6d497d..0000000 --- a/cmd/dkl-system-init/runlevel-sysinit.go +++ /dev/null @@ -1,111 +0,0 @@ -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 deleted file mode 100644 index a2c8b82..0000000 --- a/cmd/dkl-system-init/service-registry.go +++ /dev/null @@ -1,185 +0,0 @@ -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 deleted file mode 100644 index 6000a14..0000000 --- a/cmd/dkl-system-init/service-rules.go +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 22e7d60..0000000 --- a/cmd/dkl-system-init/service.go +++ /dev/null @@ -1,151 +0,0 @@ -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 deleted file mode 100644 index 48a17a2..0000000 --- a/cmd/dkl-system-init/services-to-migrate.go +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 5b3d9a8..0000000 --- a/cmd/dkl-system-init/signal.go +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index ac9e352..0000000 --- a/cmd/dkl-system-init/simple-service.go +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 1c75c59..0000000 --- a/cmd/dkl-system-init/udev.go +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index f88e7fb..0000000 --- a/cmd/dkl-system-init/user-service.go +++ /dev/null @@ -1,105 +0,0 @@ -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/cmd/dkl/main.go b/cmd/dkl/main.go new file mode 100644 index 0000000..19f6f9c --- /dev/null +++ b/cmd/dkl/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "log" + + "github.com/spf13/cobra" + + cmdinit "novit.nc/direktil/inits/pkg/cmd/init" +) + +func main() { + root := &cobra.Command{} + + root.AddCommand(cmdinit.Command()) + + if err := root.Execute(); err != nil { + log.Fatal("error: ", err) + } +} diff --git a/go.mod b/go.mod index 68ba3d1..f5eab83 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,12 @@ module novit.nc/direktil/inits require ( + github.com/fsnotify/fsnotify v1.4.7 github.com/sparrc/go-ping v0.0.0-20160208162908-416e72114cd1 - golang.org/x/net v0.0.0-20180706051357-32a936f46389 // indirect + github.com/spf13/cobra v0.0.3 + github.com/spf13/pflag v1.0.3 // indirect + github.com/ulikunitz/xz v0.5.4 + golang.org/x/net v0.0.0-20180706051357-32a936f46389 golang.org/x/sys v0.0.0-20180709060233-1b2967e3c290 gopkg.in/yaml.v2 v2.2.1 novit.nc/direktil/pkg v0.0.0-20180706230842-852aa03280f9 diff --git a/go.sum b/go.sum index 8ba1b91..3b37dd0 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,10 @@ +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/sparrc/go-ping v0.0.0-20160208162908-416e72114cd1/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/ulikunitz/xz v0.5.4/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= golang.org/x/net v0.0.0-20180706051357-32a936f46389/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.0.0-20180709060233-1b2967e3c290/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/layer/etc/init.d/dkl-boot b/layer/etc/init.d/dkl-boot new file mode 100755 index 0000000..dc653cf --- /dev/null +++ b/layer/etc/init.d/dkl-boot @@ -0,0 +1,15 @@ +#!/sbin/openrc-run + +description="Direktil boot operations." + +depend() +{ + provide net +} + +start() +{ + ebegin "Running Direktil boot operations" + /sbin/dkl init boot + eend $? "Direktil boot operations failed" +} diff --git a/layer/etc/init.d/dkl-default b/layer/etc/init.d/dkl-default new file mode 100755 index 0000000..7d7238a --- /dev/null +++ b/layer/etc/init.d/dkl-default @@ -0,0 +1,16 @@ +#!/sbin/openrc-run + +description="Direktil default operations." + +depend() +{ + # need anything? + : +} + +start() +{ + ebegin "Running Direktil default operations" + /sbin/dkl init default + eend $? "Direktil default operations failed" +} diff --git a/layer/etc/init.d/dkl-user-services b/layer/etc/init.d/dkl-user-services new file mode 100755 index 0000000..6d6fcb2 --- /dev/null +++ b/layer/etc/init.d/dkl-user-services @@ -0,0 +1,30 @@ +#!/sbin/openrc-run + +description="Direktil user services." + +pidfile=/run/direktil/services.pid + +depend() +{ + need dkl-default +} + +start() +{ + mkdir -p $(dirname $pidfile) + + einfo "Starting Direktil services manager" + start-stop-daemon --start \ + --pidfile $pidfile \ + --background \ + --exec "/sbin/dkl" \ + -- init services + eend $? "Failed to start Direktil services manager" +} + +stop() +{ + einfo "Stopping Direktil services manager" + start-stop-daemon --stop --pidfile $pidfile + eend $? "Failed to stop Direktil services manager" +} diff --git a/layer/etc/inittab b/layer/etc/inittab new file mode 100644 index 0000000..27f9daf --- /dev/null +++ b/layer/etc/inittab @@ -0,0 +1,54 @@ +# +# /etc/inittab: This file describes how the INIT process should set up +# the system in a certain run-level. +# +# Author: Miquel van Smoorenburg, +# Modified by: Patrick J. Volkerding, +# Modified by: Daniel Robbins, +# Modified by: Martin Schlemmer, +# Modified by: Mike Frysinger, +# Modified by: Robin H. Johnson, +# Modified by: William Hubbs, +# + +# Default runlevel. +id:3:initdefault: + +# System initialization, mount local filesystems, etc. +si::sysinit:/sbin/openrc sysinit + +# Further system initialization, brings up the boot runlevel. +rc::bootwait:/sbin/openrc boot + +l0u:0:wait:/sbin/telinit u +l0:0:wait:/sbin/openrc shutdown +l0s:0:wait:/sbin/halt -dhnp +l1:1:wait:/sbin/openrc single +l2:2:wait:/sbin/openrc nonetwork +l3:3:wait:/sbin/openrc default +l4:4:wait:/sbin/openrc default +l5:5:wait:/sbin/openrc default +l6u:6:wait:/sbin/telinit u +l6:6:wait:/sbin/openrc reboot +l6r:6:wait:/sbin/reboot -dkn +#z6:6:respawn:/sbin/sulogin + +# new-style single-user +su0:S:wait:/sbin/openrc single +su1:S:wait:/sbin/sulogin + +# TERMINALS +#x1:12345:respawn:/sbin/agetty 38400 console linux +c1:12345:respawn:/sbin/agetty --noclear 38400 tty1 linux +c2:2345:respawn:/sbin/agetty 38400 tty2 linux +c3:2345:respawn:/sbin/agetty 38400 tty3 linux +c4:2345:respawn:/sbin/agetty 38400 tty4 linux +c5:2345:respawn:/sbin/agetty 38400 tty5 linux +c6:2345:respawn:/sbin/agetty 38400 tty6 linux + +# SERIAL CONSOLES +s0:12345:respawn:/sbin/agetty --noclear -L 115200 ttyS0 vt100 +#s1:12345:respawn:/sbin/agetty -L 115200 ttyS1 vt100 + +# What to do at the "Three Finger Salute". +ca:12345:ctrlaltdel:/sbin/shutdown -r now diff --git a/layer/etc/runlevels/boot/dkl-boot b/layer/etc/runlevels/boot/dkl-boot new file mode 120000 index 0000000..4b7709b --- /dev/null +++ b/layer/etc/runlevels/boot/dkl-boot @@ -0,0 +1 @@ +../../init.d/dkl-boot \ No newline at end of file diff --git a/layer/etc/runlevels/default/dkl-default b/layer/etc/runlevels/default/dkl-default new file mode 120000 index 0000000..cff26c8 --- /dev/null +++ b/layer/etc/runlevels/default/dkl-default @@ -0,0 +1 @@ +../../init.d/dkl-default \ No newline at end of file diff --git a/layer/etc/runlevels/default/dkl-user-services b/layer/etc/runlevels/default/dkl-user-services new file mode 120000 index 0000000..4b145f9 --- /dev/null +++ b/layer/etc/runlevels/default/dkl-user-services @@ -0,0 +1 @@ +../../init.d/dkl-user-services \ No newline at end of file diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..d7df20a --- /dev/null +++ b/modd.conf @@ -0,0 +1,6 @@ +**/*.go layer/**/* test-vm update-test-data { + prep: go test ./pkg/... ./cmd/dkl + prep: CGO_ENABLED=0 go build ./cmd/dkl + prep: ./update-test-data + daemon: ./test-vm +} diff --git a/pkg/apply/files.go b/pkg/apply/files.go index 5f8e100..b7b2481 100644 --- a/pkg/apply/files.go +++ b/pkg/apply/files.go @@ -3,13 +3,13 @@ package apply import ( "fmt" "io/ioutil" + "log" "os" "path/filepath" "strings" "novit.nc/direktil/inits/pkg/vars" "novit.nc/direktil/pkg/config" - dlog "novit.nc/direktil/pkg/log" ) const ( @@ -17,14 +17,14 @@ const ( ) // Files writes the files from the given config -func Files(cfg *config.Config, log *dlog.Log, filters ...string) (err error) { +func Files(cfg *config.Config, filters ...string) (err error) { accept := func(n string) bool { return true } if len(filters) > 0 { accept = func(n string) bool { for _, filter := range filters { if matched, err := filepath.Match(filter, n); err != nil { - log.Taintf(dlog.Error, "bad filter ignored: %q: %v", filter, err) + log.Printf("ERROR: bad filter ignored: %q: %v", filter, err) } else if matched { return true } @@ -37,7 +37,7 @@ func Files(cfg *config.Config, log *dlog.Log, filters ...string) (err error) { err = writeFile( authorizedKeysPath, []byte(strings.Join(cfg.RootUser.AuthorizedKeys, "\n")), - 0600, 0700, cfg, log, + 0600, 0700, cfg, ) if err != nil { @@ -63,7 +63,6 @@ func Files(cfg *config.Config, log *dlog.Log, filters ...string) (err error) { mode, 0755, cfg, - log, ) if err != nil { @@ -74,9 +73,7 @@ func Files(cfg *config.Config, log *dlog.Log, filters ...string) (err error) { return } -func writeFile(path string, content []byte, fileMode, dirMode os.FileMode, - cfg *config.Config, log *dlog.Log) (err error) { - +func writeFile(path string, content []byte, fileMode, dirMode os.FileMode, cfg *config.Config) (err error) { if err = os.MkdirAll(filepath.Dir(path), dirMode); err != nil { return } diff --git a/pkg/cmd/init/boot/boot.go b/pkg/cmd/init/boot/boot.go new file mode 100644 index 0000000..89e386e --- /dev/null +++ b/pkg/cmd/init/boot/boot.go @@ -0,0 +1,43 @@ +package initboot + +import ( + "log" + + "github.com/spf13/cobra" + + "novit.nc/direktil/inits/pkg/sys" +) + +var ( + doNetwork bool +) + +func Command() (c *cobra.Command) { + c = &cobra.Command{ + Use: "boot", + Short: "boot stage", + Run: run, + } + + c.Flags().BoolVar(&doNetwork, "do-network", true, "setup network") + + return +} + +func run(c *cobra.Command, args []string) { + setupFiles() + setupModules() + + if doNetwork { + setupNetworking() + } + + setupLVM() +} + +func setupModules() { + for _, mod := range sys.Config().Modules { + log.Print("loading module ", mod) + sys.Run("modprobe", mod) + } +} diff --git a/pkg/cmd/init/boot/files.go b/pkg/cmd/init/boot/files.go new file mode 100644 index 0000000..3443514 --- /dev/null +++ b/pkg/cmd/init/boot/files.go @@ -0,0 +1,60 @@ +package initboot + +import ( + "log" + "strconv" + "syscall" + + "novit.nc/direktil/inits/pkg/apply" + "novit.nc/direktil/inits/pkg/sys" +) + +func setupFiles() { + cfg := sys.Config() + + // 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 { + log.Fatalf("FATAL: mount --make-rshared / failed: %v", err) + } + + // - setup root user + if passwordHash := cfg.RootUser.PasswordHash; passwordHash == "" { + sys.MustRun("/usr/bin/passwd", "-d", "root") + } else { + sys.MustRun("/bin/sh", "-c", "chpasswd --encrypted < 30 { + log.Fatal("FATAL: storage device not found after 30s: ", udevMatch) + } + goto retry + } + + log.Print("found storage device at ", dev) + + sys.MustRun("pvcreate", dev) + sys.MustRun("vgcreate", "storage", dev) +} + +func setupLV(volume config.VolumeDef) { + if volume.Extents != "" { + sys.MustRun("lvcreate", "-l", volume.Extents, "-n", volume.Name, "storage") + } else { + sys.MustRun("lvcreate", "-L", volume.Size, "-n", volume.Name, "storage") + } + + // wait the device link + devPath := "/dev/storage/" + volume.Name + sys.WaitFile(devPath, time.After(30*time.Second)) + + args := make([]string, 0) + + switch volume.FS { + case "btrfs": + args = append(args, "-f") + case "ext4": + args = append(args, "-F") + } + + sys.MustRun("mkfs."+volume.FS, append(args, devPath)...) +} diff --git a/pkg/cmd/init/boot/network.go b/pkg/cmd/init/boot/network.go new file mode 100644 index 0000000..a4405e4 --- /dev/null +++ b/pkg/cmd/init/boot/network.go @@ -0,0 +1,155 @@ +package initboot + +import ( + "bytes" + "log" + "net" + "os" + "os/exec" + "path/filepath" + "time" + + ping "github.com/sparrc/go-ping" + "novit.nc/direktil/pkg/config" + + "novit.nc/direktil/inits/pkg/sys" + "novit.nc/direktil/inits/pkg/vars" +) + +var networkStarted = map[string]bool{} + +func setupNetworking() { + cfg := sys.Config() + for idx, network := range cfg.Networks { + setupNetwork(idx, network) + } +} + +func setupNetwork(idx int, network config.NetworkDef) { + tries := 0 +retry: + ifaces, err := net.Interfaces() + if err != nil { + log.Fatalf("FATAL: failed to get network interfaces: %v", err) + } + + match := false + for _, iface := range ifaces { + if networkStarted[iface.Name] { + continue + } + + if network.Match.Name != "" { + if ok, err := filepath.Match(network.Match.Name, iface.Name); err != nil { + log.Fatalf("FATAL: network[%d] name match error: %v", idx, err) + } else if !ok { + continue + } + } + + if network.Match.Ping != nil { + log.Printf("network[%d] ping check on %s", idx, iface.Name) + + if ok, err := networkPingCheck(iface.Name, network); err != nil { + log.Printf("ERROR: network[%d] ping check failed: %v", idx, err) + + } else if !ok { + continue + } + } + + log.Printf("network[%d] matches interface %s", idx, iface.Name) + match = true + + startNetwork(iface.Name, idx, network) + + if !network.Match.All { + return + } + } + + if !match { + log.Printf("WARNING: network[%d] did not match any interface", idx) + + tries++ + if network.Optional && tries > 3 { + return + } + + time.Sleep(1 * time.Second) + log.Printf("WARNING: network[%d] retrying (try: %d)", idx, tries) + goto retry + } +} + +func startNetwork(ifaceName string, idx int, network config.NetworkDef) { + cfg := sys.Config() + + log.Printf("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() + log.Fatalf("FATAL: network setup failed (link list below): %v\n%s", err, string(links)) + } + + networkStarted[ifaceName] = true +} + +func networkPingCheck(ifName string, network config.NetworkDef) (b bool, err error) { + check := network.Match.Ping + + source := string(vars.Substitute([]byte(check.Source), sys.Config())) + + if err = sys.Run("ip", "addr", "add", source, "dev", ifName); err != nil { + return + } + if err = sys.Run("ip", "link", "set", ifName, "up"); err != nil { + return + } + + defer func() { + sys.MustRun("ip", "link", "set", ifName, "down") + sys.MustRun("ip", "addr", "del", source, "dev", ifName) + }() + + count := 3 + if check.Count != 0 { + count = check.Count + } + + for n := 0; n < count; n++ { + // TODO probably better to use golang.org/x/net/icmp directly + pinger, e := ping.NewPinger(network.Match.Ping.Target) + if e != nil { + err = e + return + } + + pinger.Count = 1 + + pinger.Timeout = 1 * time.Second + if check.Timeout > 0 { + pinger.Timeout = time.Duration(check.Timeout) * time.Second + } + + pinger.SetPrivileged(true) + pinger.Run() + + if pinger.Statistics().PacketsRecv > 0 { + b = true + return + } + } + + return +} diff --git a/pkg/cmd/init/default/default.go b/pkg/cmd/init/default/default.go new file mode 100644 index 0000000..93a627c --- /dev/null +++ b/pkg/cmd/init/default/default.go @@ -0,0 +1,16 @@ +package initdefault + +import "github.com/spf13/cobra" + +func Command() (c *cobra.Command) { + c = &cobra.Command{ + Use: "default", + Short: "default stage", + Run: run, + } + + return +} + +func run(c *cobra.Command, args []string) { +} diff --git a/pkg/cmd/init/init.go b/pkg/cmd/init/init.go new file mode 100644 index 0000000..0c776fc --- /dev/null +++ b/pkg/cmd/init/init.go @@ -0,0 +1,34 @@ +package cmdinit + +import ( + "os" + "strings" + + "github.com/spf13/cobra" + + initboot "novit.nc/direktil/inits/pkg/cmd/init/boot" + initdefault "novit.nc/direktil/inits/pkg/cmd/init/default" + initservice "novit.nc/direktil/inits/pkg/cmd/init/service" +) + +func Command() (c *cobra.Command) { + c = &cobra.Command{ + Use: "init", + Short: "init stages", + + PersistentPreRun: func(_ *cobra.Command, _ []string) { + // set a reasonable path + os.Setenv("PATH", strings.Join([]string{ + "/usr/local/bin:/usr/local/sbin", + "/usr/bin:/usr/sbin", + "/bin:/sbin", + }, ":")) + }, + } + + c.AddCommand(initboot.Command()) + c.AddCommand(initdefault.Command()) + c.AddCommand(initservice.Command()) + + return +} diff --git a/pkg/cmd/init/service/service.go b/pkg/cmd/init/service/service.go new file mode 100644 index 0000000..d8dc824 --- /dev/null +++ b/pkg/cmd/init/service/service.go @@ -0,0 +1,90 @@ +package initservices + +import ( + "log" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/spf13/cobra" + plog "novit.nc/direktil/pkg/log" +) + +var ( + delays = []time.Duration{ + 1 * time.Second, + 2 * time.Second, + 4 * time.Second, + 8 * time.Second, + } + + crashForgiveDelay = 10 * time.Minute +) + +func Command() (c *cobra.Command) { + c = &cobra.Command{ + Use: "services", + Short: "run user services", + Run: run, + } + + return +} + +func run(c *cobra.Command, args []string) { + paths, err := filepath.Glob("/etc/direktil/services/*") + + if err != nil && !os.IsNotExist(err) { + log.Fatal("failed to list services: ", err) + } + + for _, path := range paths { + stat, err := os.Stat(path) + if err != nil { + log.Fatalf("failed to stat %s: %v", path, err) + } + + if stat.Mode()&0100 == 0 { + // not executable + continue + } + + go runService(path) + } + + select {} +} + +func runService(svcPath string) { + svc := filepath.Base(svcPath) + + logger := plog.Get(svc) + plog.EnableFiles() + + n := 0 + for { + lastStart := time.Now() + + cmd := exec.Command(svcPath) + cmd.Stdout = logger + cmd.Stderr = logger + err := cmd.Run() + + if time.Since(lastStart) > crashForgiveDelay { + n = 0 + } + + if err == nil { + logger.Taintf(plog.Error, "service exited (%v), waiting %v", err, delays[n]) + } else { + logger.Taintf(plog.Error, "service exited on error (%v), waiting %v", err, delays[n]) + } + + time.Sleep(delays[n]) + + if n+1 < len(delays) { + n++ + } + } +} diff --git a/pkg/sys/config.go b/pkg/sys/config.go new file mode 100644 index 0000000..e009958 --- /dev/null +++ b/pkg/sys/config.go @@ -0,0 +1,36 @@ +package sys + +import ( + "log" + "sync" + + "novit.nc/direktil/pkg/config" +) + +const cfgPath = "/config.yaml" + +var ( + cfg *config.Config + cfgLock sync.Mutex +) + +func Config() *config.Config { + if cfg != nil { + return cfg + } + + cfgLock.Lock() + defer cfgLock.Unlock() + + if cfg != nil { + return cfg + } + + c, err := config.Load(cfgPath) + if err != nil { + log.Fatal("FATAL: failed to load config: ", err) + } + + cfg = c + return cfg +} diff --git a/pkg/sys/mount.go b/pkg/sys/mount.go new file mode 100644 index 0000000..0b6675c --- /dev/null +++ b/pkg/sys/mount.go @@ -0,0 +1,19 @@ +package sys + +import ( + "log" + "os" + "syscall" +) + +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 { + log.Fatalf("FATAL: mount %q %q -t %q -o %q failed: %v", source, target, fstype, data, err) + } + + log.Printf("mounted %q", target) +} diff --git a/pkg/sys/sys.go b/pkg/sys/sys.go new file mode 100644 index 0000000..e3537b5 --- /dev/null +++ b/pkg/sys/sys.go @@ -0,0 +1,87 @@ +package sys + +import ( + "log" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/fsnotify/fsnotify" +) + +func Run(cmd string, args ...string) (err error) { + c := exec.Command(cmd, args...) + c.Stdout = os.Stdout + c.Stderr = os.Stderr + + if err = c.Run(); err != nil { + log.Printf("command %s %q failed: %v", cmd, args, err) + } + return +} + +func MustRun(cmd string, args ...string) { + if err := Run(cmd, args...); err != nil { + log.Fatal("FATAL: mandatory command did not succeed") + } +} + +func Mkdir(dir string, mode os.FileMode) { + if err := os.MkdirAll(dir, mode); err != nil { + log.Fatalf("FATAL: mkdir %q failed: %v", dir, err) + } +} + +func FileExists(path string) bool { + _, err := os.Stat(path) + if err != nil { + if !os.IsNotExist(err) { + log.Printf("WARNING: failed to stat %q, assuming not exist: %v", path, err) + } + return false + } + return true +} + +func WaitFile(path string, timeout <-chan time.Time) { + if FileExists(path) { + return + } + + watch, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal("FATAL: fsnotify: failed to create: ", err) + } + + defer watch.Close() + + dir := filepath.Dir(path) + + if err = watch.Add(dir); err != nil { + log.Fatalf("FATAL: fsnotify: failed to add %s: %v", dir, err) + } + + go func() { + for err := range watch.Errors { + log.Fatal("FATAL: fsnotify: error: ", err) + } + }() + + timedOut := false + for !timedOut { + select { + case <-watch.Events: + // skip + + case <-timeout: + timedOut = true + } + + if FileExists(path) { + return + } + } + + log.Fatal("FATAL: timed out waiting for ", path) +}