Merge pull request #100 from jonboulle/rework

refactor(*): rework cloudconfig for better extensibility and consistency
This commit is contained in:
Jonathan Boulle 2014-05-14 11:39:53 -07:00
commit 32c52d8729
13 changed files with 366 additions and 231 deletions

View File

@ -1,6 +1,7 @@
package initialize package initialize
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"path" "path"
@ -10,18 +11,35 @@ import (
"github.com/coreos/coreos-cloudinit/system" "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 { type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"` SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct { Coreos struct {
Etcd EtcdEnvironment Etcd EtcdEnvironment
Update map[string]string
Units []system.Unit
OEM OEMRelease OEM OEMRelease
Update UpdateConfig
Units []system.Unit
} }
WriteFiles []system.File `yaml:"write_files"` WriteFiles []system.File `yaml:"write_files"`
Hostname string Hostname string
Users []system.User Users []system.User
ManageEtcHosts string `yaml:"manage_etc_hosts"` ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
} }
func NewCloudConfig(contents string) (*CloudConfig, error) { func NewCloudConfig(contents string) (*CloudConfig, error) {
@ -42,6 +60,9 @@ func (cc CloudConfig) String() string {
return stringified 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 { func Apply(cfg CloudConfig, env *Environment) error {
if cfg.Hostname != "" { if cfg.Hostname != "" {
if err := system.SetHostname(cfg.Hostname); err != nil { 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) log.Printf("Set hostname to %s", cfg.Hostname)
} }
if cfg.Coreos.OEM.ID != "" { for _, user := range cfg.Users {
if err := WriteOEMRelease(&cfg.Coreos.OEM, env.Root()); err != nil { if user.Name == "" {
return err log.Printf("User object has no 'name' field, skipping")
continue
} }
log.Printf("Wrote /etc/oem-release to filesystem")
}
if len(cfg.Users) > 0 { if system.UserExists(&user) {
for _, user := range cfg.Users { log.Printf("User '%s' exists, ignoring creation-time fields", user.Name)
if user.Name == "" { if user.PasswordHash != "" {
log.Printf("User object has no 'name' field, skipping") log.Printf("Setting '%s' user's password", user.Name)
continue 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) { if len(user.SSHAuthorizedKeys) > 0 {
log.Printf("User '%s' exists, ignoring creation-time fields", user.Name) log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name)
if user.PasswordHash != "" { if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil {
log.Printf("Setting '%s' user's password", user.Name) return err
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 { if user.SSHImportGithubUser != "" {
log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name) log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name)
if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil { if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil {
return err return err
}
} }
if user.SSHImportGithubUser != "" { }
log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name) if user.SSHImportURL != "" {
if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil { log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL)
return err 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 _, ccf := range []CloudConfigFile{cfg.Coreos.OEM, cfg.Coreos.Update, cfg.ManageEtcHosts} {
for _, file := range cfg.WriteFiles { f, err := ccf.File(env.Root())
file.Path = path.Join(env.Root(), file.Path) if err != nil {
if err := system.WriteFile(&file); 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 return err
} }
log.Printf("Wrote file %s to filesystem", file.Path) log.Printf("Placed unit %s at %s", unit.Name, dst)
} reload = true
}
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("Wrote etcd config file to filesystem") if unit.Mask {
} log.Printf("Masking unit file %s", unit.Name)
if err := system.MaskUnit(unit.Name, env.Root()); err != nil {
if s, ok := cfg.Coreos.Update["reboot-strategy"]; ok { return err
if err := WriteLocksmithConfig(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 { if unit.Enable {
commands := make(map[string]string, 0) if unit.Group() != "network" {
for _, unit := range cfg.Coreos.Units { log.Printf("Enabling unit file %s", dst)
dst := system.UnitDestination(&unit, env.Root()) if err := system.EnableUnitFile(dst, unit.Runtime); err != nil {
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 return err
} }
log.Printf("Placed unit %s at %s", unit.Name, dst) log.Printf("Enabled unit %s", unit.Name)
}
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"
} else { } else {
if unit.Command != "" { log.Printf("Skipping enable for network-like unit %s", unit.Name)
commands[unit.Name] = unit.Command
}
} }
} }
if err := system.DaemonReload(); err != nil { if unit.Group() == "network" {
log.Fatalf("Failed systemd daemon-reload: %v", err) commands["systemd-networkd.service"] = "restart"
} } else if unit.Command != "" {
commands[unit.Name] = unit.Command
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 cfg.ManageEtcHosts != "" { if reload {
if err := system.DaemonReload(); err != nil {
if err := WriteEtcHosts(cfg.ManageEtcHosts, env.Root()); err != nil { return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
log.Fatalf("Failed to write /etc/hosts to filesystem: %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 return nil

View File

@ -144,7 +144,7 @@ ssh_authorized_keys:
` `
cfg, err := NewCloudConfig(contents) cfg, err := NewCloudConfig(contents)
if err != nil { if err != nil {
t.Fatalf("Encountered unexpected error :%v", err) t.Fatalf("Encountered unexpected error: %v", err)
} }
keys := cfg.SSHAuthorizedKeys 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) { func TestCloudConfigUsers(t *testing.T) {
contents := ` contents := `
users: users:

View File

@ -45,3 +45,16 @@ func (self *Environment) Apply(data string) string {
} }
return data 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
}

View File

@ -3,26 +3,14 @@ package initialize
import ( import (
"errors" "errors"
"fmt" "fmt"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system" "github.com/coreos/coreos-cloudinit/system"
) )
type EtcdEnvironment map[string]string type EtcdEnvironment map[string]string
func (ec EtcdEnvironment) normalized() map[string]string { func (ee EtcdEnvironment) String() (out string) {
out := make(map[string]string, len(ec)) norm := normalizeSvcEnv(ee)
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()
if val, ok := norm["DISCOVERY_URL"]; ok { if val, ok := norm["DISCOVERY_URL"]; ok {
delete(norm, "DISCOVERY_URL") delete(norm, "DISCOVERY_URL")
@ -40,23 +28,23 @@ func (ec EtcdEnvironment) String() (out string) {
return return
} }
// Write an EtcdEnvironment to the appropriate path on disk for etcd.service // Unit creates a Unit file drop-in for etcd, using any configured
func WriteEtcdEnvironment(env EtcdEnvironment, root string) error { // options and adding a default MachineID if unset.
if _, ok := env["name"]; !ok { func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) {
if _, ok := ee["name"]; !ok {
if machineID := system.MachineID(root); machineID != "" { if machineID := system.MachineID(root); machineID != "" {
env["name"] = machineID ee["name"] = machineID
} else if hostname, err := system.Hostname(); err == nil { } else if hostname, err := system.Hostname(); err == nil {
env["name"] = hostname ee["name"] = hostname
} else { } else {
return errors.New("Unable to determine default etcd name") return nil, errors.New("Unable to determine default etcd name")
} }
} }
file := system.File{ return &system.Unit{
Path: path.Join(root, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf"), Name: "etcd.service",
RawFilePermissions: "0644", Runtime: true,
Content: env.String(), DropIn: true,
} Content: ee.String(),
}, nil
return system.WriteFile(&file)
} }

View File

@ -3,9 +3,10 @@ package initialize
import ( import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"path" "path"
"testing" "testing"
"github.com/coreos/coreos-cloudinit/system"
) )
func TestEtcdEnvironment(t *testing.T) { func TestEtcdEnvironment(t *testing.T) {
@ -69,8 +70,18 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
if err := WriteEtcdEnvironment(ec, dir); err != nil { u, err := ec.Unit(dir)
t.Fatalf("Processing of EtcdEnvironment failed: %v", err) 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") 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) { func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
ec := EtcdEnvironment{} ee := EtcdEnvironment{}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) 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) t.Fatalf("Failed writing out /etc/machine-id: %v", err)
} }
if err := WriteEtcdEnvironment(ec, dir); err != nil { u, err := ee.Unit(dir)
t.Fatalf("Processing of EtcdEnvironment failed: %v", err) 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") 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") t.Fatalf("File has incorrect contents")
} }
} }
func rmdir(path string) error {
cmd := exec.Command("rm", "-rf", path)
return cmd.Run()
}

View File

@ -2,8 +2,6 @@ package initialize
import ( import (
"bufio" "bufio"
"fmt"
"io/ioutil"
"os" "os"
"path" "path"
"strings" "strings"
@ -13,73 +11,79 @@ import (
const locksmithUnit = "locksmithd.service" const locksmithUnit = "locksmithd.service"
// addStrategy creates an `/etc/coreos/update.conf` file with the requested type UpdateConfig map[string]string
// strategy via rewriting the file on disk or by starting from
// `/usr/share/coreos/update.conf`. func (uc UpdateConfig) strategy() string {
func addStrategy(strategy string, root string) error { 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") etcUpdate := path.Join(root, "etc", "coreos", "update.conf")
usrUpdate := path.Join(root, "usr", "share", "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) conf, err := os.Open(etcUpdate)
if os.IsNotExist(err) { if os.IsNotExist(err) {
conf, err = os.Open(usrUpdate) conf, err = os.Open(usrUpdate)
if err != nil { }
return err if err != nil {
} return nil, err
} }
scanner := bufio.NewScanner(conf) scanner := bufio.NewScanner(conf)
sawStrat := false sawStrat := false
stratLine := "REBOOT_STRATEGY="+strategy stratLine := "REBOOT_STRATEGY=" + uc.strategy()
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if strings.HasPrefix(line, "REBOOT_STRATEGY=") { if strings.HasPrefix(line, "REBOOT_STRATEGY=") {
line = stratLine line = stratLine
sawStrat = true sawStrat = true
} }
fmt.Fprintln(tmp, line) out += line
out += "\n"
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return err return nil, err
} }
} }
if !sawStrat { if !sawStrat {
fmt.Fprintln(tmp, stratLine) out += stratLine
out += "\n"
} }
return &system.File{
return os.Rename(tmp.Name(), etcUpdate) Path: path.Join("etc", "coreos", "update.conf"),
RawFilePermissions: "0644",
Content: out,
}, nil
} }
// WriteLocksmithConfig updates the `update.conf` file with a REBOOT_STRATEGY for locksmith. // Unit generates a locksmith system.Unit for the cloud-init initializer to
func WriteLocksmithConfig(strategy string, root string) error { // act on appropriately
cmd := "restart" func (uc UpdateConfig) Unit(root string) (*system.Unit, error) {
if strategy == "off" { u := &system.Unit{
err := system.MaskUnit(locksmithUnit, root) Name: locksmithUnit,
if err != nil { Enable: true,
return err Command: "restart",
} Mask: false,
cmd = "stop"
} else {
return addStrategy(strategy, root)
} }
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 u, nil
}
return nil
} }

View File

@ -5,6 +5,8 @@ import (
"os" "os"
"path" "path"
"testing" "testing"
"github.com/coreos/coreos-cloudinit/system"
) )
const ( const (
@ -42,9 +44,19 @@ func TestLocksmithEnvironmentWrittenToDisk(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
uc := &UpdateConfig{"reboot-strategy": "etcd-lock"}
if err := WriteLocksmithConfig("etcd-lock", dir); err != nil { f, err := uc.File(dir)
t.Fatalf("Processing of LocksmithEnvironment failed: %v", err) 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") fullPath := path.Join(dir, "etc", "coreos", "update.conf")
@ -76,9 +88,17 @@ func TestLocksmithEnvironmentMasked(t *testing.T) {
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
setupFixtures(dir) setupFixtures(dir)
if err := WriteLocksmithConfig("off", dir); err != nil { uc := &UpdateConfig{"reboot-strategy": "off"}
t.Fatalf("Processing of LocksmithEnvironment failed: %v", err)
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") fullPath := path.Join(dir, "etc", "systemd", "system", "locksmithd.service")
target, err := os.Readlink(fullPath) target, err := os.Readlink(fullPath)

View File

@ -11,8 +11,10 @@ import (
const DefaultIpv4Address = "127.0.0.1" const DefaultIpv4Address = "127.0.0.1"
func generateEtcHosts(option string) (out string, err error) { type EtcHosts string
if option != "localhost" {
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
if eh != "localhost" {
return "", errors.New("Invalid option to manage_etc_hosts") 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 (eh EtcHosts) File(root string) (*system.File, error) {
func WriteEtcHosts(option string, root string) error { if eh == "" {
return nil, nil
etcHosts, err := generateEtcHosts(option)
if err != nil {
return err
} }
file := system.File{ etcHosts, err := eh.generateEtcHosts()
Path: path.Join(root, "etc", "hosts"), if err != nil {
return nil, err
}
return &system.File{
Path: path.Join("etc", "hosts"),
RawFilePermissions: "0644", RawFilePermissions: "0644",
Content: etcHosts, Content: etcHosts,
} }, nil
return system.WriteFile(&file)
} }

View File

@ -6,6 +6,8 @@ import (
"os" "os"
"path" "path"
"testing" "testing"
"github.com/coreos/coreos-cloudinit/system"
) )
func TestCloudConfigManageEtcHosts(t *testing.T) { func TestCloudConfigManageEtcHosts(t *testing.T) {
@ -25,14 +27,9 @@ manage_etc_hosts: localhost
} }
func TestManageEtcHostsInvalidValue(t *testing.T) { func TestManageEtcHostsInvalidValue(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") eh := EtcHosts("invalid")
if err != nil { if f, err := eh.File(""); err == nil || f != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("EtcHosts File succeeded with invalid value!")
}
defer rmdir(dir)
if err := WriteEtcHosts("invalid", dir); err == nil {
t.Fatalf("WriteEtcHosts succeeded with invalid value: %v", err)
} }
} }
@ -41,10 +38,22 @@ func TestEtcHostsWrittenToDisk(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer rmdir(dir) defer os.RemoveAll(dir)
if err := WriteEtcHosts("localhost", dir); err != nil { eh := EtcHosts("localhost")
t.Fatalf("WriteEtcHosts failed: %v", err)
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") fullPath := path.Join(dir, "etc", "hosts")

View File

@ -16,7 +16,7 @@ type OEMRelease struct {
BugReportURL string `yaml:"bug-report-url"` BugReportURL string `yaml:"bug-report-url"`
} }
func (oem *OEMRelease) String() string { func (oem OEMRelease) String() string {
fields := []string{ fields := []string{
fmt.Sprintf("ID=%s", oem.ID), fmt.Sprintf("ID=%s", oem.ID),
fmt.Sprintf("VERSION_ID=%s", oem.VersionID), fmt.Sprintf("VERSION_ID=%s", oem.VersionID),
@ -28,12 +28,14 @@ func (oem *OEMRelease) String() string {
return strings.Join(fields, "\n") + "\n" return strings.Join(fields, "\n") + "\n"
} }
func WriteOEMRelease(oem *OEMRelease, root string) error { func (oem OEMRelease) File(root string) (*system.File, error) {
file := system.File{ if oem.ID == "" {
Path: path.Join(root, "etc", "oem-release"), return nil, nil
RawFilePermissions: "0644",
Content: oem.String(),
} }
return system.WriteFile(&file) return &system.File{
Path: path.Join("etc", "oem-release"),
RawFilePermissions: "0644",
Content: oem.String(),
}, nil
} }

View File

@ -5,6 +5,8 @@ import (
"os" "os"
"path" "path"
"testing" "testing"
"github.com/coreos/coreos-cloudinit/system"
) )
func TestOEMReleaseWrittenToDisk(t *testing.T) { func TestOEMReleaseWrittenToDisk(t *testing.T) {
@ -21,8 +23,17 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
if err := WriteOEMRelease(&oem, dir); err != nil { f, err := oem.File(dir)
t.Fatalf("Processing of EtcdEnvironment failed: %v", err) 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") fullPath := path.Join(dir, "etc", "oem-release")

View File

@ -17,12 +17,21 @@ import (
// never be used as a true MachineID // never be used as a true MachineID
const fakeMachineID = "42000000000000000000000000000042" const fakeMachineID = "42000000000000000000000000000042"
// Name for drop-in service configuration files created by cloudconfig
const cloudConfigDropIn = "20-cloudinit.conf"
type Unit struct { type Unit struct {
Name string Name string
Mask bool
Enable bool Enable bool
Runtime bool Runtime bool
Content string Content string
Command 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 { func (u *Unit) Type() string {
@ -42,8 +51,8 @@ func (u *Unit) Group() (group string) {
type Script []byte type Script []byte
// UnitDestination builds the appropriate absolte file path for // UnitDestination builds the appropriate absolute file path for
// the given unit. The root argument indicates the effective base // the given Unit. The root argument indicates the effective base
// directory of the system (similar to a chroot). // directory of the system (similar to a chroot).
func UnitDestination(u *Unit, root string) string { func UnitDestination(u *Unit, root string) string {
dir := "etc" dir := "etc"
@ -51,7 +60,11 @@ func UnitDestination(u *Unit, root string) string {
dir = "run" 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 // PlaceUnit writes a unit file at the provided destination, creating

View File

@ -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) { func TestPlaceMountUnit(t *testing.T) {
u := Unit{ u := Unit{
Name: "media-state.mount", Name: "media-state.mount",
@ -123,6 +147,7 @@ func TestMachineID(t *testing.T) {
t.Fatalf("File has incorrect contents") t.Fatalf("File has incorrect contents")
} }
} }
func TestMaskUnit(t *testing.T) { func TestMaskUnit(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil { if err != nil {