Initial commit
This commit is contained in:
		
							
								
								
									
										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 | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user