diff --git a/initialize/config.go b/initialize/config.go index 4cdcc3a..8731237 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -1,6 +1,7 @@ package initialize import ( + "errors" "fmt" "log" "path" @@ -10,18 +11,35 @@ import ( "github.com/coreos/coreos-cloudinit/system" ) +// CloudConfigFile represents a CoreOS specific configuration option that can generate +// an associated system.File to be written to disk +type CloudConfigFile interface { + // File should either return (*system.File, error), or (nil, nil) if nothing + // needs to be done for this configuration option. + File(root string) (*system.File, error) +} + +// CloudConfigUnit represents a CoreOS specific configuration option that can generate +// an associated system.Unit to be created/enabled appropriately +type CloudConfigUnit interface { + // Unit should either return (*system.Unit, error), or (nil, nil) if nothing + // needs to be done for this configuration option. + Unit(root string) (*system.Unit, error) +} + +// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML type CloudConfig struct { SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"` Coreos struct { Etcd EtcdEnvironment - Update map[string]string - Units []system.Unit OEM OEMRelease + Update UpdateConfig + Units []system.Unit } WriteFiles []system.File `yaml:"write_files"` Hostname string Users []system.User - ManageEtcHosts string `yaml:"manage_etc_hosts"` + ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"` } func NewCloudConfig(contents string) (*CloudConfig, error) { @@ -42,6 +60,9 @@ func (cc CloudConfig) String() string { return stringified } +// Apply renders a CloudConfig to an Environment. This can involve things like +// configuring the hostname, adding new users, writing various configuration +// files to disk, and manipulating systemd services. func Apply(cfg CloudConfig, env *Environment) error { if cfg.Hostname != "" { if err := system.SetHostname(cfg.Hostname); err != nil { @@ -50,54 +71,45 @@ func Apply(cfg CloudConfig, env *Environment) error { log.Printf("Set hostname to %s", cfg.Hostname) } - if cfg.Coreos.OEM.ID != "" { - if err := WriteOEMRelease(&cfg.Coreos.OEM, env.Root()); err != nil { - return err + for _, user := range cfg.Users { + if user.Name == "" { + log.Printf("User object has no 'name' field, skipping") + continue } - log.Printf("Wrote /etc/oem-release to filesystem") - } - if len(cfg.Users) > 0 { - for _, user := range cfg.Users { - if user.Name == "" { - log.Printf("User object has no 'name' field, skipping") - continue + if system.UserExists(&user) { + log.Printf("User '%s' exists, ignoring creation-time fields", user.Name) + if user.PasswordHash != "" { + log.Printf("Setting '%s' user's password", user.Name) + if err := system.SetUserPassword(user.Name, user.PasswordHash); err != nil { + log.Printf("Failed setting '%s' user's password: %v", user.Name, err) + return err + } } + } else { + log.Printf("Creating user '%s'", user.Name) + if err := system.CreateUser(&user); err != nil { + log.Printf("Failed creating user '%s': %v", user.Name, err) + return err + } + } - if system.UserExists(&user) { - log.Printf("User '%s' exists, ignoring creation-time fields", user.Name) - if user.PasswordHash != "" { - log.Printf("Setting '%s' user's password", user.Name) - if err := system.SetUserPassword(user.Name, user.PasswordHash); err != nil { - log.Printf("Failed setting '%s' user's password: %v", user.Name, err) - return err - } - } - } else { - log.Printf("Creating user '%s'", user.Name) - if err := system.CreateUser(&user); err != nil { - log.Printf("Failed creating user '%s': %v", user.Name, err) - return err - } + if len(user.SSHAuthorizedKeys) > 0 { + log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name) + if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil { + return err } - - if len(user.SSHAuthorizedKeys) > 0 { - log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name) - if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil { - return err - } + } + if user.SSHImportGithubUser != "" { + log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name) + if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil { + return err } - if user.SSHImportGithubUser != "" { - log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name) - if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil { - return err - } - } - if user.SSHImportURL != "" { - log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL) - if err := SSHImportKeysFromURL(user.Name, user.SSHImportURL); err != nil { - return err - } + } + if user.SSHImportURL != "" { + log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL) + if err := SSHImportKeysFromURL(user.Name, user.SSHImportURL); err != nil { + return err } } } @@ -111,86 +123,86 @@ func Apply(cfg CloudConfig, env *Environment) error { } } - if len(cfg.WriteFiles) > 0 { - for _, file := range cfg.WriteFiles { - file.Path = path.Join(env.Root(), file.Path) - if err := system.WriteFile(&file); err != nil { + for _, ccf := range []CloudConfigFile{cfg.Coreos.OEM, cfg.Coreos.Update, cfg.ManageEtcHosts} { + f, err := ccf.File(env.Root()) + if err != nil { + return err + } + if f != nil { + cfg.WriteFiles = append(cfg.WriteFiles, *f) + } + } + + for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Update} { + u, err := ccu.Unit(env.Root()) + if err != nil { + return err + } + if u != nil { + cfg.Coreos.Units = append(cfg.Coreos.Units, *u) + } + } + + for _, file := range cfg.WriteFiles { + file.Path = path.Join(env.Root(), file.Path) + if err := system.WriteFile(&file); err != nil { + return err + } + log.Printf("Wrote file %s to filesystem", file.Path) + } + + commands := make(map[string]string, 0) + reload := false + for _, unit := range cfg.Coreos.Units { + dst := system.UnitDestination(&unit, env.Root()) + if unit.Content != "" { + log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) + if err := system.PlaceUnit(&unit, dst); err != nil { return err } - log.Printf("Wrote file %s to filesystem", file.Path) - } - } - - if len(cfg.Coreos.Etcd) > 0 { - if err := WriteEtcdEnvironment(cfg.Coreos.Etcd, env.Root()); err != nil { - log.Fatalf("Failed to write etcd config to filesystem: %v", err) + log.Printf("Placed unit %s at %s", unit.Name, dst) + reload = true } - log.Printf("Wrote etcd config file to filesystem") - } - - if s, ok := cfg.Coreos.Update["reboot-strategy"]; ok { - if err := WriteLocksmithConfig(s, env.Root()); err != nil { - log.Fatalf("Failed to write locksmith config to filesystem: %v", err) + if unit.Mask { + log.Printf("Masking unit file %s", unit.Name) + if err := system.MaskUnit(unit.Name, env.Root()); err != nil { + return 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 { - dst := system.UnitDestination(&unit, env.Root()) - if unit.Content != "" { - log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) - if err := system.PlaceUnit(&unit, dst); err != nil { + if unit.Enable { + if unit.Group() != "network" { + log.Printf("Enabling unit file %s", dst) + if err := system.EnableUnitFile(dst, unit.Runtime); err != nil { return err } - log.Printf("Placed unit %s at %s", unit.Name, dst) - } - - if unit.Enable { - if unit.Group() != "network" { - log.Printf("Enabling unit file %s", dst) - if err := system.EnableUnitFile(dst, unit.Runtime); err != nil { - return err - } - log.Printf("Enabled unit %s", unit.Name) - } else { - log.Printf("Skipping enable for network-like unit %s", unit.Name) - } - } - - if unit.Group() == "network" { - commands["systemd-networkd.service"] = "restart" + log.Printf("Enabled unit %s", unit.Name) } else { - if unit.Command != "" { - commands[unit.Name] = unit.Command - } + log.Printf("Skipping enable for network-like unit %s", unit.Name) } } - if err := system.DaemonReload(); err != nil { - log.Fatalf("Failed systemd daemon-reload: %v", err) - } - - for unit, command := range commands { - log.Printf("Calling unit command '%s %s'", command, unit) - res, err := system.RunUnitCommand(command, unit) - if err != nil { - return err - } - log.Printf("Result of '%s %s': %s", command, unit, res) + if unit.Group() == "network" { + commands["systemd-networkd.service"] = "restart" + } else if unit.Command != "" { + commands[unit.Name] = unit.Command } } - if cfg.ManageEtcHosts != "" { - - if err := WriteEtcHosts(cfg.ManageEtcHosts, env.Root()); err != nil { - log.Fatalf("Failed to write /etc/hosts to filesystem: %v", err) + if reload { + if err := system.DaemonReload(); err != nil { + return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err)) } + } - log.Printf("Wrote /etc/hosts file to filesystem") - + for unit, command := range commands { + log.Printf("Calling unit command '%s %s'", command, unit) + res, err := system.RunUnitCommand(command, unit) + if err != nil { + return err + } + log.Printf("Result of '%s %s': %s", command, unit, res) } return nil diff --git a/initialize/config_test.go b/initialize/config_test.go index 4a20e3f..a26af64 100644 --- a/initialize/config_test.go +++ b/initialize/config_test.go @@ -144,7 +144,7 @@ ssh_authorized_keys: ` cfg, err := NewCloudConfig(contents) if err != nil { - t.Fatalf("Encountered unexpected error :%v", err) + t.Fatalf("Encountered unexpected error: %v", err) } keys := cfg.SSHAuthorizedKeys @@ -162,6 +162,26 @@ func TestCloudConfigSerializationHeader(t *testing.T) { } } +// TestDropInIgnored asserts that users are unable to set DropIn=True on units +func TestDropInIgnored(t *testing.T) { + contents := ` +coreos: + units: + - name: test + dropin: true +` + cfg, err := NewCloudConfig(contents) + if err != nil || len(cfg.Coreos.Units) != 1 { + t.Fatalf("Encountered unexpected error: %v", err) + } + if len(cfg.Coreos.Units) != 1 || cfg.Coreos.Units[0].Name != "test" { + t.Fatalf("Expected 1 unit, but got %d: %v", len(cfg.Coreos.Units), cfg.Coreos.Units) + } + if cfg.Coreos.Units[0].DropIn { + t.Errorf("dropin option on unit in cloud-config was not ignored!") + } +} + func TestCloudConfigUsers(t *testing.T) { contents := ` users: diff --git a/initialize/env.go b/initialize/env.go index 8ebe9e1..7460c2d 100644 --- a/initialize/env.go +++ b/initialize/env.go @@ -45,3 +45,16 @@ func (self *Environment) Apply(data string) string { } return data } + +// normalizeSvcEnv standardizes the keys of the map (environment variables for a service) +// by replacing any dashes with underscores and ensuring they are entirely upper case. +// For example, "some-env" --> "SOME_ENV" +func normalizeSvcEnv(m map[string]string) map[string]string { + out := make(map[string]string, len(m)) + for key, val := range m { + key = strings.ToUpper(key) + key = strings.Replace(key, "-", "_", -1) + out[key] = val + } + return out +} diff --git a/initialize/etcd.go b/initialize/etcd.go index 9f27e2c..d13b98a 100644 --- a/initialize/etcd.go +++ b/initialize/etcd.go @@ -3,26 +3,14 @@ package initialize import ( "errors" "fmt" - "path" - "strings" "github.com/coreos/coreos-cloudinit/system" ) type EtcdEnvironment map[string]string -func (ec EtcdEnvironment) normalized() map[string]string { - out := make(map[string]string, len(ec)) - for key, val := range ec { - key = strings.ToUpper(key) - key = strings.Replace(key, "-", "_", -1) - out[key] = val - } - return out -} - -func (ec EtcdEnvironment) String() (out string) { - norm := ec.normalized() +func (ee EtcdEnvironment) String() (out string) { + norm := normalizeSvcEnv(ee) if val, ok := norm["DISCOVERY_URL"]; ok { delete(norm, "DISCOVERY_URL") @@ -40,23 +28,23 @@ func (ec EtcdEnvironment) String() (out string) { return } -// Write an EtcdEnvironment to the appropriate path on disk for etcd.service -func WriteEtcdEnvironment(env EtcdEnvironment, root string) error { - if _, ok := env["name"]; !ok { +// Unit creates a Unit file drop-in for etcd, using any configured +// options and adding a default MachineID if unset. +func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) { + if _, ok := ee["name"]; !ok { if machineID := system.MachineID(root); machineID != "" { - env["name"] = machineID + ee["name"] = machineID } else if hostname, err := system.Hostname(); err == nil { - env["name"] = hostname + ee["name"] = hostname } else { - return errors.New("Unable to determine default etcd name") + return nil, errors.New("Unable to determine default etcd name") } } - file := system.File{ - Path: path.Join(root, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf"), - RawFilePermissions: "0644", - Content: env.String(), - } - - return system.WriteFile(&file) + return &system.Unit{ + Name: "etcd.service", + Runtime: true, + DropIn: true, + Content: ee.String(), + }, nil } diff --git a/initialize/etcd_test.go b/initialize/etcd_test.go index ef78ca5..8327969 100644 --- a/initialize/etcd_test.go +++ b/initialize/etcd_test.go @@ -3,9 +3,10 @@ package initialize import ( "io/ioutil" "os" - "os/exec" "path" "testing" + + "github.com/coreos/coreos-cloudinit/system" ) func TestEtcdEnvironment(t *testing.T) { @@ -69,8 +70,18 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) { } defer os.RemoveAll(dir) - if err := WriteEtcdEnvironment(ec, dir); err != nil { - t.Fatalf("Processing of EtcdEnvironment failed: %v", err) + u, err := ec.Unit(dir) + if err != nil { + t.Fatalf("Generating etcd unit failed: %v", err) + } + if u == nil { + t.Fatalf("Returned nil etcd unit unexpectedly") + } + + dst := system.UnitDestination(u, dir) + os.Stderr.WriteString("writing to " + dir + "\n") + if err := system.PlaceUnit(u, dst); err != nil { + t.Fatalf("Writing of EtcdEnvironment failed: %v", err) } fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf") @@ -100,7 +111,7 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" } func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) { - ec := EtcdEnvironment{} + ee := EtcdEnvironment{} dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") if err != nil { t.Fatalf("Unable to create tempdir: %v", err) @@ -113,8 +124,18 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) { t.Fatalf("Failed writing out /etc/machine-id: %v", err) } - if err := WriteEtcdEnvironment(ec, dir); err != nil { - t.Fatalf("Processing of EtcdEnvironment failed: %v", err) + u, err := ee.Unit(dir) + if err != nil { + t.Fatalf("Generating etcd unit failed: %v", err) + } + if u == nil { + t.Fatalf("Returned nil etcd unit unexpectedly") + } + + dst := system.UnitDestination(u, dir) + os.Stderr.WriteString("writing to " + dir + "\n") + if err := system.PlaceUnit(u, dst); err != nil { + t.Fatalf("Writing of EtcdEnvironment failed: %v", err) } fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf") @@ -131,8 +152,3 @@ Environment="ETCD_NAME=node007" t.Fatalf("File has incorrect contents") } } - -func rmdir(path string) error { - cmd := exec.Command("rm", "-rf", path) - return cmd.Run() -} diff --git a/initialize/locksmith.go b/initialize/locksmith.go index d5578ba..79523fe 100644 --- a/initialize/locksmith.go +++ b/initialize/locksmith.go @@ -2,8 +2,6 @@ package initialize import ( "bufio" - "fmt" - "io/ioutil" "os" "path" "strings" @@ -13,73 +11,79 @@ import ( 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 { +type UpdateConfig map[string]string + +func (uc UpdateConfig) strategy() string { + s, _ := uc["reboot-strategy"] + return s +} + +// File creates an `/etc/coreos/update.conf` file with the requested +// strategy, by either rewriting the existing file on disk, or starting +// from `/usr/share/coreos/update.conf` +func (uc UpdateConfig) File(root string) (*system.File, error) { + + // If no reboot-strategy is set, we don't need to generate a new config + if _, ok := uc["reboot-strategy"]; !ok { + return nil, nil + } + + var out string + etcUpdate := path.Join(root, "etc", "coreos", "update.conf") usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf") - // Ensure /etc/coreos/ exists before attempting to write a file in it - os.MkdirAll(path.Join(root, "etc", "coreos"), 0755) - - 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 - } + } + if err != nil { + return nil, err } scanner := bufio.NewScanner(conf) sawStrat := false - stratLine := "REBOOT_STRATEGY="+strategy + stratLine := "REBOOT_STRATEGY=" + uc.strategy() for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "REBOOT_STRATEGY=") { line = stratLine sawStrat = true } - fmt.Fprintln(tmp, line) + out += line + out += "\n" if err := scanner.Err(); err != nil { - return err + return nil, err } } if !sawStrat { - fmt.Fprintln(tmp, stratLine) + out += stratLine + out += "\n" } - - return os.Rename(tmp.Name(), etcUpdate) + return &system.File{ + Path: path.Join("etc", "coreos", "update.conf"), + RawFilePermissions: "0644", + Content: out, + }, nil } -// WriteLocksmithConfig updates the `update.conf` file with a REBOOT_STRATEGY for locksmith. -func WriteLocksmithConfig(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) +// Unit generates a locksmith system.Unit for the cloud-init initializer to +// act on appropriately +func (uc UpdateConfig) Unit(root string) (*system.Unit, error) { + u := &system.Unit{ + Name: locksmithUnit, + Enable: true, + Command: "restart", + Mask: false, } - if err := system.DaemonReload(); err != nil { - return err + + if uc.strategy() == "off" { + u.Enable = false + u.Command = "stop" + u.Mask = true } - if _, err := system.RunUnitCommand(cmd, locksmithUnit); err != nil { - return err - } - return nil + + return u, nil } diff --git a/initialize/locksmith_test.go b/initialize/locksmith_test.go index 2243ea3..4df822d 100644 --- a/initialize/locksmith_test.go +++ b/initialize/locksmith_test.go @@ -5,6 +5,8 @@ import ( "os" "path" "testing" + + "github.com/coreos/coreos-cloudinit/system" ) const ( @@ -42,9 +44,19 @@ func TestLocksmithEnvironmentWrittenToDisk(t *testing.T) { t.Fatal(err) } } + uc := &UpdateConfig{"reboot-strategy": "etcd-lock"} - if err := WriteLocksmithConfig("etcd-lock", dir); err != nil { - t.Fatalf("Processing of LocksmithEnvironment failed: %v", err) + f, err := uc.File(dir) + if err != nil { + t.Fatalf("Processing UpdateConfig failed: %v", err) + } + if f == nil { + t.Fatalf("UpdateConfig generated nil file unexpectedly") + } + + f.Path = path.Join(dir, f.Path) + if err := system.WriteFile(f); err != nil { + t.Fatalf("Error writing update config: %v", err) } fullPath := path.Join(dir, "etc", "coreos", "update.conf") @@ -76,9 +88,17 @@ func TestLocksmithEnvironmentMasked(t *testing.T) { defer os.RemoveAll(dir) setupFixtures(dir) - if err := WriteLocksmithConfig("off", dir); err != nil { - t.Fatalf("Processing of LocksmithEnvironment failed: %v", err) + uc := &UpdateConfig{"reboot-strategy": "off"} + + u, err := uc.Unit(dir) + if err != nil { + t.Fatalf("Processing UpdateConfig failed: %v", err) } + if u == nil { + t.Fatalf("UpdateConfig generated nil unit unexpectedly") + } + + system.MaskUnit(u.Name, dir) fullPath := path.Join(dir, "etc", "systemd", "system", "locksmithd.service") target, err := os.Readlink(fullPath) diff --git a/initialize/manage_etc_hosts.go b/initialize/manage_etc_hosts.go index f168d98..b4bd3f6 100644 --- a/initialize/manage_etc_hosts.go +++ b/initialize/manage_etc_hosts.go @@ -11,8 +11,10 @@ import ( const DefaultIpv4Address = "127.0.0.1" -func generateEtcHosts(option string) (out string, err error) { - if option != "localhost" { +type EtcHosts string + +func (eh EtcHosts) generateEtcHosts() (out string, err error) { + if eh != "localhost" { return "", errors.New("Invalid option to manage_etc_hosts") } @@ -26,19 +28,19 @@ func generateEtcHosts(option string) (out string, err error) { } -// Write an /etc/hosts file -func WriteEtcHosts(option string, root string) error { - - etcHosts, err := generateEtcHosts(option) - if err != nil { - return err +func (eh EtcHosts) File(root string) (*system.File, error) { + if eh == "" { + return nil, nil } - file := system.File{ - Path: path.Join(root, "etc", "hosts"), + etcHosts, err := eh.generateEtcHosts() + if err != nil { + return nil, err + } + + return &system.File{ + Path: path.Join("etc", "hosts"), RawFilePermissions: "0644", Content: etcHosts, - } - - return system.WriteFile(&file) + }, nil } diff --git a/initialize/manage_etc_hosts_test.go b/initialize/manage_etc_hosts_test.go index c73dfef..1fbac07 100644 --- a/initialize/manage_etc_hosts_test.go +++ b/initialize/manage_etc_hosts_test.go @@ -6,6 +6,8 @@ import ( "os" "path" "testing" + + "github.com/coreos/coreos-cloudinit/system" ) func TestCloudConfigManageEtcHosts(t *testing.T) { @@ -25,14 +27,9 @@ manage_etc_hosts: localhost } func TestManageEtcHostsInvalidValue(t *testing.T) { - dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") - if err != nil { - t.Fatalf("Unable to create tempdir: %v", err) - } - defer rmdir(dir) - - if err := WriteEtcHosts("invalid", dir); err == nil { - t.Fatalf("WriteEtcHosts succeeded with invalid value: %v", err) + eh := EtcHosts("invalid") + if f, err := eh.File(""); err == nil || f != nil { + t.Fatalf("EtcHosts File succeeded with invalid value!") } } @@ -41,10 +38,22 @@ func TestEtcHostsWrittenToDisk(t *testing.T) { if err != nil { t.Fatalf("Unable to create tempdir: %v", err) } - defer rmdir(dir) + defer os.RemoveAll(dir) - if err := WriteEtcHosts("localhost", dir); err != nil { - t.Fatalf("WriteEtcHosts failed: %v", err) + eh := EtcHosts("localhost") + + f, err := eh.File(dir) + if err != nil { + t.Fatalf("Error calling File on EtcHosts: %v", err) + } + if f == nil { + t.Fatalf("manageEtcHosts returned nil file unexpectedly") + } + + f.Path = path.Join(dir, f.Path) + + if err := system.WriteFile(f); err != nil { + t.Fatalf("Error writing EtcHosts: %v", err) } fullPath := path.Join(dir, "etc", "hosts") diff --git a/initialize/oem.go b/initialize/oem.go index 7fe1fd6..aea43bc 100644 --- a/initialize/oem.go +++ b/initialize/oem.go @@ -16,7 +16,7 @@ type OEMRelease struct { BugReportURL string `yaml:"bug-report-url"` } -func (oem *OEMRelease) String() string { +func (oem OEMRelease) String() string { fields := []string{ fmt.Sprintf("ID=%s", oem.ID), fmt.Sprintf("VERSION_ID=%s", oem.VersionID), @@ -28,12 +28,14 @@ func (oem *OEMRelease) String() string { return strings.Join(fields, "\n") + "\n" } -func WriteOEMRelease(oem *OEMRelease, root string) error { - file := system.File{ - Path: path.Join(root, "etc", "oem-release"), - RawFilePermissions: "0644", - Content: oem.String(), +func (oem OEMRelease) File(root string) (*system.File, error) { + if oem.ID == "" { + return nil, nil } - return system.WriteFile(&file) + return &system.File{ + Path: path.Join("etc", "oem-release"), + RawFilePermissions: "0644", + Content: oem.String(), + }, nil } diff --git a/initialize/oem_test.go b/initialize/oem_test.go index 85a69ad..9f46215 100644 --- a/initialize/oem_test.go +++ b/initialize/oem_test.go @@ -5,6 +5,8 @@ import ( "os" "path" "testing" + + "github.com/coreos/coreos-cloudinit/system" ) func TestOEMReleaseWrittenToDisk(t *testing.T) { @@ -21,8 +23,17 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) { } defer os.RemoveAll(dir) - if err := WriteOEMRelease(&oem, dir); err != nil { - t.Fatalf("Processing of EtcdEnvironment failed: %v", err) + f, err := oem.File(dir) + if err != nil { + t.Fatalf("Processing of OEMRelease failed: %v", err) + } + if f == nil { + t.Fatalf("OEMRelease returned nil file unexpectedly") + } + + f.Path = path.Join(dir, f.Path) + if err := system.WriteFile(f); err != nil { + t.Fatalf("Writing of OEMRelease failed: %v", err) } fullPath := path.Join(dir, "etc", "oem-release") diff --git a/system/systemd.go b/system/systemd.go index f5ef23a..48caaaf 100644 --- a/system/systemd.go +++ b/system/systemd.go @@ -17,12 +17,21 @@ import ( // never be used as a true MachineID const fakeMachineID = "42000000000000000000000000000042" +// Name for drop-in service configuration files created by cloudconfig +const cloudConfigDropIn = "20-cloudinit.conf" + type Unit struct { Name string + Mask bool Enable bool Runtime bool Content string Command string + + // For drop-in units, a cloudinit.conf is generated. + // This is currently unbound in YAML (and hence unsettable in cloud-config files) + // until the correct behaviour for multiple drop-in units is determined. + DropIn bool `yaml:"-"` } func (u *Unit) Type() string { @@ -42,8 +51,8 @@ func (u *Unit) Group() (group string) { type Script []byte -// UnitDestination builds the appropriate absolte file path for -// the given unit. The root argument indicates the effective base +// UnitDestination builds the appropriate absolute file path for +// the given Unit. The root argument indicates the effective base // directory of the system (similar to a chroot). func UnitDestination(u *Unit, root string) string { dir := "etc" @@ -51,7 +60,11 @@ func UnitDestination(u *Unit, root string) string { dir = "run" } - return path.Join(root, dir, "systemd", u.Group(), u.Name) + if u.DropIn { + return path.Join(root, dir, "systemd", u.Group(), fmt.Sprintf("%s.d", u.Name), cloudConfigDropIn) + } else { + return path.Join(root, dir, "systemd", u.Group(), u.Name) + } } // PlaceUnit writes a unit file at the provided destination, creating diff --git a/system/systemd_test.go b/system/systemd_test.go index 677afcb..fb29035 100644 --- a/system/systemd_test.go +++ b/system/systemd_test.go @@ -60,6 +60,30 @@ Address=10.209.171.177/19 } } +func TestUnitDestination(t *testing.T) { + dir := "/some/dir" + name := "foobar.service" + + u := Unit{ + Name: name, + DropIn: false, + } + + dst := UnitDestination(&u, dir) + expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service") + if dst != expectDst { + t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst) + } + + u.DropIn = true + + dst = UnitDestination(&u, dir) + expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn) + if dst != expectDst { + t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst) + } +} + func TestPlaceMountUnit(t *testing.T) { u := Unit{ Name: "media-state.mount", @@ -123,6 +147,7 @@ func TestMachineID(t *testing.T) { t.Fatalf("File has incorrect contents") } } + func TestMaskUnit(t *testing.T) { dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") if err != nil {