diff --git a/Documentation/cloud-config.md b/Documentation/cloud-config.md index fe95a1f..1e64579 100644 --- a/Documentation/cloud-config.md +++ b/Documentation/cloud-config.md @@ -70,6 +70,23 @@ Note that hyphens in the coreos.etcd.* keys are mapped to underscores. [etcd-config]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md +#### update + +The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated. + +- **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed. + - _reboot_: Reboot immediately after an update is applied. + - _etcd-lock_: Reboot after first taking a distributed lock in etcd, this guarantees that only one host will reboot concurrently and that the cluster will remain available during the update. + - _best-effort_ - If etcd is running, "etcd-lock", otherwise simply "reboot". + - _off_ - Disable rebooting after updates are applied (not recommended). + +``` +#cloud-config +coreos: + update: + reboot-strategy: etcd-lock +``` + #### oem The `coreos.oem.*` parameters follow the [os-release spec][os-release], but have been repurposed as a way for coreos-cloudinit to know about the OEM partition on this machine: diff --git a/initialize/config.go b/initialize/config.go index 85de85a..9c1f12a 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -13,9 +13,10 @@ import ( type CloudConfig struct { SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"` Coreos struct { - Etcd EtcdEnvironment - Units []system.Unit - OEM OEMRelease + Etcd EtcdEnvironment + Update map[string]string + Units []system.Unit + OEM OEMRelease } WriteFiles []system.File `yaml:"write_files"` Hostname string @@ -128,6 +129,13 @@ func Apply(cfg CloudConfig, env *Environment) error { log.Printf("Wrote etcd config file to filesystem") } + if s, ok := cfg.Coreos.Update["reboot-strategy"]; ok { + if err := WriteLocksmithEnvironment(s, env.Root()); err != nil { + log.Fatalf("Failed to write locksmith config to filesystem: %v", err) + } + log.Printf("Wrote locksmith config file to filesystem") + } + if len(cfg.Coreos.Units) > 0 { commands := make(map[string]string, 0) for _, unit := range cfg.Coreos.Units { diff --git a/initialize/config_test.go b/initialize/config_test.go index bc36de5..4a20e3f 100644 --- a/initialize/config_test.go +++ b/initialize/config_test.go @@ -32,6 +32,8 @@ func TestCloudConfig(t *testing.T) { coreos: etcd: discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" + update: + reboot-strategy: reboot units: - name: 50-eth0.network runtime: yes @@ -129,6 +131,9 @@ Address=10.209.171.177/19 if cfg.Hostname != "trontastic" { t.Errorf("Failed to parse hostname") } + if cfg.Coreos.Update["reboot-strategy"] != "reboot" { + t.Errorf("Failed to parse locksmith strategy") + } } // Assert that our interface conversion doesn't panic diff --git a/initialize/locksmith.go b/initialize/locksmith.go new file mode 100644 index 0000000..f696beb --- /dev/null +++ b/initialize/locksmith.go @@ -0,0 +1,82 @@ +package initialize + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "github.com/coreos/coreos-cloudinit/system" +) + +const locksmithUnit = "locksmithd.service" + +// addStrategy creates an `/etc/coreos/update.conf` file with the requested +// strategy via rewriting the file on disk or by starting from +// `/usr/share/coreos/update.conf`. +func addStrategy(strategy string, root string) error { + etcUpdate := path.Join(root, "etc", "coreos", "update.conf") + usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf") + + tmp, err := ioutil.TempFile(path.Join(root, "etc", "coreos"), ".update.conf") + if err != nil { + return err + } + err = tmp.Chmod(0644) + if err != nil { + return err + } + + conf, err := os.Open(etcUpdate) + if os.IsNotExist(err) { + conf, err = os.Open(usrUpdate) + if err != nil { + return err + } + } + + scanner := bufio.NewScanner(conf) + + sawStrat := false + stratLine := "STRATEGY="+strategy + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "STRATEGY=") { + line = stratLine + sawStrat = true + } + fmt.Fprintln(tmp, line) + if err := scanner.Err(); err != nil { + return err + } + } + + if !sawStrat { + fmt.Fprintln(tmp, stratLine) + } + + return os.Rename(tmp.Name(), etcUpdate) +} + +// WriteLocksmithEnvironment writes a drop-in unit for locksmith +func WriteLocksmithEnvironment(strategy string, root string) error { + cmd := "restart" + if strategy == "off" { + err := system.MaskUnit(locksmithUnit, root) + if err != nil { + return err + } + cmd = "stop" + } else { + return addStrategy(strategy, root) + } + if err := system.DaemonReload(); err != nil { + return err + } + if _, err := system.RunUnitCommand(cmd, locksmithUnit); err != nil { + return err + } + return nil +} diff --git a/initialize/locksmith_test.go b/initialize/locksmith_test.go new file mode 100644 index 0000000..7be9ed5 --- /dev/null +++ b/initialize/locksmith_test.go @@ -0,0 +1,92 @@ +package initialize + +import ( + "io/ioutil" + "os" + "path" + "testing" +) + +const ( + base = `SERVER=https://example.com +GROUP=thegroupc` + + configured = base + ` +STRATEGY=awesome +` + + expected = base + ` +STRATEGY=etcd-lock +` +) + +func setupFixtures(dir string) { + os.MkdirAll(path.Join(dir, "usr", "share", "coreos"), 0755) + os.MkdirAll(path.Join(dir, "etc", "coreos"), 0755) + os.MkdirAll(path.Join(dir, "run", "systemd", "system"), 0755) + + ioutil.WriteFile(path.Join(dir, "usr", "share", "coreos", "update.conf"), []byte(base), 0644) +} + +func TestLocksmithEnvironmentWrittenToDisk(t *testing.T) { + dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") + if err != nil { + t.Fatalf("Unable to create tempdir: %v", err) + } + defer os.RemoveAll(dir) + setupFixtures(dir) + + for i := 0; i < 2; i++ { + if i == 1 { + err = ioutil.WriteFile(path.Join(dir, "etc", "coreos", "update.conf"), []byte(configured), 0644) + if err != nil { + t.Fatal(err) + } + } + + if err := WriteLocksmithEnvironment("etcd-lock", dir); err != nil { + t.Fatalf("Processing of LocksmithEnvironment failed: %v", err) + } + + fullPath := path.Join(dir, "etc", "coreos", "update.conf") + + fi, err := os.Stat(fullPath) + if err != nil { + t.Fatalf("Unable to stat file: %v", err) + } + + if fi.Mode() != os.FileMode(0644) { + t.Errorf("File has incorrect mode: %v", fi.Mode()) + } + + contents, err := ioutil.ReadFile(fullPath) + if err != nil { + t.Fatalf("Unable to read expected file: %v", err) + } + + if string(contents) != expected { + t.Fatalf("File has incorrect contents, got %v, wanted %v", string(contents), expected) + } + } +} +func TestLocksmithEnvironmentMasked(t *testing.T) { + dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") + if err != nil { + t.Fatalf("Unable to create tempdir: %v", err) + } + defer os.RemoveAll(dir) + setupFixtures(dir) + + if err := WriteLocksmithEnvironment("off", dir); err != nil { + t.Fatalf("Processing of LocksmithEnvironment failed: %v", err) + } + + fullPath := path.Join(dir, "etc", "systemd", "system", "locksmithd.service") + target, err := os.Readlink(fullPath) + if err != nil { + t.Fatalf("Unable to read link %v", err) + } + if target != "/dev/null" { + t.Fatalf("Locksmith not masked, unit target %v", target) + } +}