Initial commit
This commit is contained in:
commit
2de2a4d0f6
31
cmd/dkl-apply-config/main.go
Normal file
31
cmd/dkl-apply-config/main.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
190
cmd/dkl-initrd-init/main.go
Normal file
190
cmd/dkl-initrd-init/main.go
Normal file
@ -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)
|
||||
}
|
23
cmd/dkl-initrd-init/params.go
Normal file
23
cmd/dkl-initrd-init/params.go
Normal file
@ -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
|
||||
}
|
24
cmd/dkl-initrd-init/zombies.go
Normal file
24
cmd/dkl-initrd-init/zombies.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
33
cmd/dkl-system-init/bootstrap.go
Normal file
33
cmd/dkl-system-init/bootstrap.go
Normal file
@ -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)
|
||||
}
|
267
cmd/dkl-system-init/configure.go
Normal file
267
cmd/dkl-system-init/configure.go
Normal file
@ -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 <<EOF\nroot:"+passwordHash+"\nEOF")
|
||||
}
|
||||
|
||||
// - groups
|
||||
for _, group := range cfg.Groups {
|
||||
opts := make([]string, 0)
|
||||
opts = append(opts, "-r")
|
||||
if group.Gid != 0 {
|
||||
opts = append(opts, "-g", strconv.Itoa(group.Gid))
|
||||
}
|
||||
opts = append(opts, group.Name)
|
||||
|
||||
run("groupadd", opts...)
|
||||
}
|
||||
|
||||
// - user
|
||||
for _, user := range cfg.Users {
|
||||
opts := make([]string, 0)
|
||||
opts = append(opts, "-r")
|
||||
if user.Gid != 0 {
|
||||
opts = append(opts, "-g", strconv.Itoa(user.Gid))
|
||||
}
|
||||
if user.Uid != 0 {
|
||||
opts = append(opts, "-u", strconv.Itoa(user.Uid))
|
||||
}
|
||||
opts = append(opts, user.Name)
|
||||
|
||||
run("useradd", opts...)
|
||||
}
|
||||
|
||||
// - files
|
||||
if err := apply.Files(cfg, initLog); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
services.SetFlag("files-written")
|
||||
|
||||
// - hostname
|
||||
initLog.Taint(log.Info, "setting hostname")
|
||||
run("hostname", "-F", "/etc/hostname")
|
||||
|
||||
// - modules
|
||||
for _, module := range cfg.Modules {
|
||||
initLog.Taint(log.Info, "loading module ", module)
|
||||
run("modprobe", module)
|
||||
}
|
||||
|
||||
// - networks
|
||||
for idx, network := range cfg.Networks {
|
||||
setupNetwork(idx, network)
|
||||
}
|
||||
|
||||
services.SetFlag("network-up")
|
||||
|
||||
// - setup storage
|
||||
initLog.Print("checking storage")
|
||||
if err := exec.Command("vgdisplay", "storage").Run(); err != nil {
|
||||
initLog.Print("creating VG storage")
|
||||
setupVG(vars.BootArgValue("storage", cfg.Storage.UdevMatch))
|
||||
}
|
||||
|
||||
for _, name := range cfg.Storage.RemoveVolumes {
|
||||
dev := "/dev/storage/" + name
|
||||
|
||||
if _, err := os.Stat(dev); os.IsNotExist(err) {
|
||||
continue
|
||||
|
||||
} else if err != nil {
|
||||
fatal("failed to stat ", dev, ": ", err)
|
||||
}
|
||||
|
||||
initLog.Print("removing LV ", name)
|
||||
cmd := exec.Command("lvremove", "-f", "storage/"+name)
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fatal("failed to remove LV ", name)
|
||||
}
|
||||
}
|
||||
|
||||
for _, volume := range cfg.Storage.Volumes {
|
||||
if err := exec.Command("lvdisplay", "storage/"+volume.Name).Run(); err != nil {
|
||||
initLog.Print("creating LV ", volume.Name)
|
||||
setupLV(volume)
|
||||
}
|
||||
|
||||
dev := "/dev/storage/" + volume.Name
|
||||
|
||||
initLog.Printf("checking filesystem on %s", dev)
|
||||
run("fsck", "-p", dev)
|
||||
|
||||
mount(dev, volume.Mount.Path, volume.FS,
|
||||
syscall.MS_NOATIME|syscall.MS_RELATIME,
|
||||
volume.Mount.Options)
|
||||
}
|
||||
|
||||
// finished configuring :-)
|
||||
log.EnableFiles()
|
||||
services.SetFlag("configured")
|
||||
|
||||
// load user services
|
||||
go loadUserServices()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_ configure) Stop() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
var networkStarted = map[string]bool{}
|
||||
|
||||
func setupNetwork(idx int, network config.NetworkDef) {
|
||||
tries := 0
|
||||
retry:
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
fatalf("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 {
|
||||
fatalf("network[%d] name match error: %v", idx, err)
|
||||
} else if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if network.Match.Ping != nil {
|
||||
initLog.Printf("network[%d] ping check on %s", idx, iface.Name)
|
||||
|
||||
if ok, err := networkPingCheck(iface.Name, network); err != nil {
|
||||
initLog.Taintf(log.Error, "network[%d] ping check failed: %v",
|
||||
idx, err)
|
||||
} else if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
initLog.Printf("network[%d] matches interface %s", idx, iface.Name)
|
||||
match = true
|
||||
|
||||
startNetwork(iface.Name, idx, network)
|
||||
|
||||
if !network.Match.All {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
initLog.Taintf(log.Warning, "network[%d] did not match any interface", idx)
|
||||
|
||||
tries++
|
||||
if network.Optional && tries > 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
|
||||
}
|
8
cmd/dkl-system-init/dumb-init-bridge.go
Normal file
8
cmd/dkl-system-init/dumb-init-bridge.go
Normal file
@ -0,0 +1,8 @@
|
||||
package main
|
||||
|
||||
// void handleSignals();
|
||||
import "C"
|
||||
|
||||
func handleChildren() {
|
||||
C.handleSignals()
|
||||
}
|
139
cmd/dkl-system-init/dumb-init.c
Normal file
139
cmd/dkl-system-init/dumb-init.c
Normal file
@ -0,0 +1,139 @@
|
||||
// extracted from https://raw.githubusercontent.com/Yelp/dumb-init/master/dumb-init.c
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
36
cmd/dkl-system-init/errors.go
Normal file
36
cmd/dkl-system-init/errors.go
Normal file
@ -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 {}
|
||||
}
|
49
cmd/dkl-system-init/initctl.go
Normal file
49
cmd/dkl-system-init/initctl.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
157
cmd/dkl-system-init/lvm.go
Normal file
157
cmd/dkl-system-init/lvm.go
Normal file
@ -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)...)
|
||||
}
|
225
cmd/dkl-system-init/main.go
Normal file
225
cmd/dkl-system-init/main.go
Normal file
@ -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()
|
||||
}
|
||||
}
|
28
cmd/dkl-system-init/runlevel-default.go
Normal file
28
cmd/dkl-system-init/runlevel-default.go
Normal file
@ -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"},
|
||||
},
|
||||
)
|
||||
}
|
111
cmd/dkl-system-init/runlevel-sysinit.go
Normal file
111
cmd/dkl-system-init/runlevel-sysinit.go
Normal file
@ -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
|
||||
}
|
185
cmd/dkl-system-init/service-registry.go
Normal file
185
cmd/dkl-system-init/service-registry.go
Normal file
@ -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)
|
||||
}
|
30
cmd/dkl-system-init/service-rules.go
Normal file
30
cmd/dkl-system-init/service-rules.go
Normal file
@ -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
|
||||
}
|
151
cmd/dkl-system-init/service.go
Normal file
151
cmd/dkl-system-init/service.go
Normal file
@ -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
|
||||
}
|
21
cmd/dkl-system-init/services-to-migrate.go
Normal file
21
cmd/dkl-system-init/services-to-migrate.go
Normal file
@ -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"),
|
||||
}...)
|
||||
}
|
||||
// */
|
19
cmd/dkl-system-init/signal.go
Normal file
19
cmd/dkl-system-init/signal.go
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
23
cmd/dkl-system-init/simple-service.go
Normal file
23
cmd/dkl-system-init/simple-service.go
Normal file
@ -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
|
||||
}
|
36
cmd/dkl-system-init/udev.go
Normal file
36
cmd/dkl-system-init/udev.go
Normal file
@ -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"},
|
||||
},
|
||||
)
|
||||
}
|
105
cmd/dkl-system-init/user-service.go
Normal file
105
cmd/dkl-system-init/user-service.go
Normal file
@ -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
|
||||
}
|
67
pkg/apply/files.go
Normal file
67
pkg/apply/files.go
Normal file
@ -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
|
||||
}
|
32
pkg/vars/boot.go
Normal file
32
pkg/vars/boot.go
Normal file
@ -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
|
||||
}
|
60
pkg/vars/vars.go
Normal file
60
pkg/vars/vars.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user