Compare commits

...

14 Commits

Author SHA1 Message Date
Jonathan Boulle
d0cbbd2007 chore(coreos-cloudinit): bump to 0.7.5 2014-06-06 11:10:48 -07:00
Jonathan Boulle
7b5e542eb4 Merge pull request #132 from jonboulle/locksmith
reboot-strategy=off breaks subsequent reboot strategies
2014-06-06 11:08:06 -07:00
Jonathan Boulle
376d82ba63 doc(*): add note about runtime locksmithd unit file 2014-06-06 10:55:42 -07:00
Jonathan Boulle
a6aa9f82b8 fix(systemd): unmask runtime units when mask=False 2014-06-06 10:55:42 -07:00
Jonathan Boulle
00ee047753 fix(locksmith): use a runtime unit for locksmith 2014-06-06 10:55:42 -07:00
Jonathan Boulle
f127406d01 Merge pull request #140 from jonboulle/atomic
fix(system): write all files atomically
2014-06-06 10:37:09 -07:00
Jonathan Boulle
0ddc08d55a fix(system): write all files atomically 2014-06-06 10:36:36 -07:00
Jonathan Boulle
56f455f890 Merge pull request #141 from jonboulle/141
cloudinit doesn't restart update-engine.service
2014-06-06 10:25:24 -07:00
Jonathan Boulle
dd861b9f88 fix(initialize): ensure update-engine is restarted after group/server
changes
2014-06-05 16:12:40 -07:00
Alex Crawford
f7d01da267 Merge pull request #138 from spkane/github-ent-key-docs
Add a valid URL example for Github Enterprise token based API auth
2014-06-04 16:15:04 -07:00
Sean P. Kane
fc8f30bf08 Add a valid URL example for Github Enterprise token based API auth 2014-06-04 16:03:02 -07:00
Brandon Philips
075c0557e7 Merge pull request #137 from robszumski/patch-1
fix(docs): remove unneeded install section
2014-06-04 14:22:55 -07:00
Rob Szumski
d25e13a2c6 fix(docs): remove unneeded install section 2014-06-04 13:57:18 -07:00
Alex Crawford
cf1ffad533 chore(coreos-cloudinit): bump to 0.7.4+git 2014-06-03 14:14:47 -07:00
16 changed files with 326 additions and 131 deletions

View File

@@ -100,6 +100,7 @@ For more information on fleet configuration, see the [fleet documentation][fleet
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated. The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field. These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field.
The `reboot-strategy` parameter also affects the behaviour of [locksmith](https://github.com/coreos/locksmith).
- **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed. - **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. - _reboot_: Reboot immediately after an update is applied.
@@ -109,6 +110,10 @@ These fields will be written out to and replace `/etc/coreos/update.conf`. If on
- **server**: is the omaha endpoint URL which will be queried for updates. - **server**: is the omaha endpoint URL which will be queried for updates.
- **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable") - **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable")
*Note: cloudinit will only manipulate the locksmith unit file in the systemd runtime directory (`/run/systemd/system/locksmithd.service`). If any manual modifications are made to an overriding unit configuration file (e.g. `/etc/systemd/system/locksmithd.service`), cloudinit will no longer be able to control the locksmith service unit.*
##### Example
``` ```
#cloud-config #cloud-config
coreos: coreos:
@@ -150,9 +155,6 @@ coreos:
Restart=always Restart=always
ExecStart=/usr/bin/docker start -a redis_server ExecStart=/usr/bin/docker start -a redis_server
ExecStop=/usr/bin/docker stop -t 2 redis_server ExecStop=/usr/bin/docker stop -t 2 redis_server
[Install]
WantedBy=local.target
``` ```
Start the built-in `etcd` and `fleet` services: Start the built-in `etcd` and `fleet` services:
@@ -277,7 +279,7 @@ For example, if you have an installation of GitHub Enterprise, you can provide a
users: users:
- name: elroy - name: elroy
coreos-ssh-import-url: https://token:<OAUTH-TOKEN>@github-enterprise.example.com/users/elroy/keys coreos-ssh-import-url: https://github-enterprise.example.com/api/v3/users/elroy/keys?access_token=<TOKEN>
``` ```
You can also specify any URL whose response matches the JSON format for public keys: You can also specify any URL whose response matches the JSON format for public keys:

View File

@@ -14,7 +14,7 @@ import (
"github.com/coreos/coreos-cloudinit/system" "github.com/coreos/coreos-cloudinit/system"
) )
const version = "0.7.4" const version = "0.7.5"
func main() { func main() {
var printVersion bool var printVersion bool

View File

@@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"path"
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml" "github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
@@ -20,11 +19,9 @@ type CloudConfigFile interface {
} }
// CloudConfigUnit represents a CoreOS specific configuration option that can generate // CloudConfigUnit represents a CoreOS specific configuration option that can generate
// an associated system.Unit to be created/enabled appropriately // associated system.Units to be created/enabled appropriately
type CloudConfigUnit interface { type CloudConfigUnit interface {
// Unit should either return (*system.Unit, error), or (nil, nil) if nothing Units(root string) ([]system.Unit, error)
// 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 // CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
@@ -215,27 +212,25 @@ func Apply(cfg CloudConfig, env *Environment) error {
} }
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} { for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
u, err := ccu.Unit(env.Root()) u, err := ccu.Units(env.Root())
if err != nil { if err != nil {
return err return err
} }
if u != nil { cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
cfg.Coreos.Units = append(cfg.Coreos.Units, *u)
}
} }
for _, file := range cfg.WriteFiles { for _, file := range cfg.WriteFiles {
file.Path = path.Join(env.Root(), file.Path) path, err := system.WriteFile(&file, env.Root())
if err := system.WriteFile(&file); err != nil { if err != nil {
return err return err
} }
log.Printf("Wrote file %s to filesystem", file.Path) log.Printf("Wrote file %s to filesystem", path)
} }
commands := make(map[string]string, 0) commands := make(map[string]string, 0)
reload := false reload := false
for _, unit := range cfg.Coreos.Units { for _, unit := range cfg.Coreos.Units {
dst := system.UnitDestination(&unit, env.Root()) dst := unit.Destination(env.Root())
if unit.Content != "" { if unit.Content != "" {
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
if err := system.PlaceUnit(&unit, dst); err != nil { if err := system.PlaceUnit(&unit, dst); err != nil {
@@ -247,7 +242,12 @@ func Apply(cfg CloudConfig, env *Environment) error {
if unit.Mask { if unit.Mask {
log.Printf("Masking unit file %s", unit.Name) log.Printf("Masking unit file %s", unit.Name)
if err := system.MaskUnit(unit.Name, env.Root()); err != nil { if err := system.MaskUnit(&unit, env.Root()); err != nil {
return err
}
} else if unit.Runtime {
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
if err := system.UnmaskUnit(&unit, env.Root()); err != nil {
return err return err
} }
} }

View File

@@ -28,9 +28,9 @@ func (ee EtcdEnvironment) String() (out string) {
return return
} }
// Unit creates a Unit file drop-in for etcd, using any configured // Units creates a Unit file drop-in for etcd, using any configured
// options and adding a default MachineID if unset. // options and adding a default MachineID if unset.
func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) { func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
if ee == nil { if ee == nil {
return nil, nil return nil, nil
} }
@@ -45,10 +45,11 @@ func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) {
} }
} }
return &system.Unit{ etcd := system.Unit{
Name: "etcd.service", Name: "etcd.service",
Runtime: true, Runtime: true,
DropIn: true, DropIn: true,
Content: ee.String(), Content: ee.String(),
}, nil }
return []system.Unit{etcd}, nil
} }

View File

@@ -59,7 +59,7 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
} }
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) { func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
ec := EtcdEnvironment{ ee := EtcdEnvironment{
"name": "node001", "name": "node001",
"discovery": "http://disco.example.com/foobar", "discovery": "http://disco.example.com/foobar",
"peer-bind-addr": "127.0.0.1:7002", "peer-bind-addr": "127.0.0.1:7002",
@@ -70,17 +70,18 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
u, err := ec.Unit(dir) uu, err := ee.Units(dir)
if err != nil { if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err) t.Fatalf("Generating etcd unit failed: %v", err)
} }
if u == nil { if len(uu) != 1 {
t.Fatalf("Returned nil etcd unit unexpectedly") t.Fatalf("Expected 1 unit to be returned, got %d", len(uu))
} }
u := uu[0]
dst := system.UnitDestination(u, dir) dst := u.Destination(dir)
os.Stderr.WriteString("writing to " + dir + "\n") os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil { if err := system.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err) t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
} }
@@ -124,17 +125,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)
} }
u, err := ee.Unit(dir) uu, err := ee.Units(dir)
if err != nil { if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err) t.Fatalf("Generating etcd unit failed: %v", err)
} }
if u == nil { if len(uu) == 0 {
t.Fatalf("Returned nil etcd unit unexpectedly") t.Fatalf("Returned empty etcd units unexpectedly")
} }
u := uu[0]
dst := system.UnitDestination(u, dir) dst := u.Destination(dir)
os.Stderr.WriteString("writing to " + dir + "\n") os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil { if err := system.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err) t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
} }
@@ -159,8 +161,8 @@ func TestEtcdEnvironmentWhenNil(t *testing.T) {
if ee != nil { if ee != nil {
t.Fatalf("EtcdEnvironment is not nil") t.Fatalf("EtcdEnvironment is not nil")
} }
u, err := ee.Unit("") uu, err := ee.Units("")
if u != nil || err != nil { if len(uu) != 0 || err != nil {
t.Fatalf("Unit returned a non-nil value for nil input") t.Fatalf("Units returned value for nil input")
} }
} }

View File

@@ -19,16 +19,17 @@ func (fe FleetEnvironment) String() (out string) {
return return
} }
// Unit generates a Unit file drop-in for fleet, if any fleet options were // Units generates a Unit file drop-in for fleet, if any fleet options were
// configured in cloud-config // configured in cloud-config
func (fe FleetEnvironment) Unit(root string) (*system.Unit, error) { func (fe FleetEnvironment) Units(root string) ([]system.Unit, error) {
if len(fe) < 1 { if len(fe) < 1 {
return nil, nil return nil, nil
} }
return &system.Unit{ fleet := system.Unit{
Name: "fleet.service", Name: "fleet.service",
Runtime: true, Runtime: true,
DropIn: true, DropIn: true,
Content: fe.String(), Content: fe.String(),
}, nil }
return []system.Unit{fleet}, nil
} }

View File

@@ -19,20 +19,21 @@ Environment="FLEET_PUBLIC_IP=12.34.56.78"
func TestFleetUnit(t *testing.T) { func TestFleetUnit(t *testing.T) {
cfg := make(FleetEnvironment, 0) cfg := make(FleetEnvironment, 0)
u, err := cfg.Unit("/") uu, err := cfg.Units("/")
if u != nil { if len(uu) != 0 {
t.Errorf("unexpectedly generated unit with empty FleetEnvironment") t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
} }
cfg["public-ip"] = "12.34.56.78" cfg["public-ip"] = "12.34.56.78"
u, err = cfg.Unit("/") uu, err = cfg.Units("/")
if err != nil { if err != nil {
t.Errorf("error generating fleet unit: %v", err) t.Errorf("error generating fleet unit: %v", err)
} }
if u == nil { if len(uu) != 1 {
t.Fatalf("unexpectedly got nil unit generating fleet unit!") t.Fatalf("expected 1 unit generated, got %d", len(uu))
} }
u := uu[0]
if !u.Runtime { if !u.Runtime {
t.Errorf("bad Runtime for generated fleet unit!") t.Errorf("bad Runtime for generated fleet unit!")
} }

View File

@@ -50,9 +50,7 @@ func TestEtcHostsWrittenToDisk(t *testing.T) {
t.Fatalf("manageEtcHosts returned nil file unexpectedly") t.Fatalf("manageEtcHosts returned nil file unexpectedly")
} }
f.Path = path.Join(dir, f.Path) if _, err := system.WriteFile(f, dir); err != nil {
if err := system.WriteFile(f); err != nil {
t.Fatalf("Error writing EtcHosts: %v", err) t.Fatalf("Error writing EtcHosts: %v", err)
} }

View File

@@ -31,8 +31,7 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
t.Fatalf("OEMRelease returned nil file unexpectedly") t.Fatalf("OEMRelease returned nil file unexpectedly")
} }
f.Path = path.Join(dir, f.Path) if _, err := system.WriteFile(f, dir); err != nil {
if err := system.WriteFile(f); err != nil {
t.Fatalf("Writing of OEMRelease failed: %v", err) t.Fatalf("Writing of OEMRelease failed: %v", err)
} }

View File

@@ -126,24 +126,40 @@ func (uc UpdateConfig) File(root string) (*system.File, error) {
}, nil }, nil
} }
// GetUnit generates a locksmith system.Unit, if reboot-strategy was set in // Units generates units for the cloud-init initializer to act on:
// cloud-config, for the cloud-init initializer to act on appropriately // - a locksmith system.Unit, if "reboot-strategy" was set in cloud-config
func (uc UpdateConfig) Unit(root string) (*system.Unit, error) { // - an update_engine system.Unit, if "group" was set in cloud-config
strategy, ok := uc["reboot-strategy"] func (uc UpdateConfig) Units(root string) ([]system.Unit, error) {
if !ok { var units []system.Unit
return nil, nil if strategy, ok := uc["reboot-strategy"]; ok {
} ls := &system.Unit{
u := &system.Unit{
Name: locksmithUnit, Name: locksmithUnit,
Command: "restart", Command: "restart",
Mask: false, Mask: false,
Runtime: true,
} }
if strategy == "off" { if strategy == "off" {
u.Command = "stop" ls.Command = "stop"
u.Mask = true ls.Mask = true
}
units = append(units, *ls)
} }
return u, nil rue := false
if _, ok := uc["group"]; ok {
rue = true
}
if _, ok := uc["server"]; ok {
rue = true
}
if rue {
ue := system.Unit{
Name: "update-engine",
Command: "restart",
}
units = append(units, ue)
}
return units, nil
} }

View File

@@ -38,12 +38,12 @@ func TestEmptyUpdateConfig(t *testing.T) {
if f != nil { if f != nil {
t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f) t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f)
} }
u, err := uc.Unit("") uu, err := uc.Units("")
if err != nil { if err != nil {
t.Error("unexpected error getting unit from empty UpdateConfig") t.Error("unexpected error getting unit from empty UpdateConfig")
} }
if u != nil { if len(uu) != 0 {
t.Errorf("getting unit from empty UpdateConfig should have returned nil, got %v", u) t.Errorf("getting unit from empty UpdateConfig should have returned zero units, got %d", len(uu))
} }
} }
@@ -106,6 +106,21 @@ SERVER=http://foo.com`
t.Errorf("File has incorrect contents, got %v, want %v", got, want) t.Errorf("File has incorrect contents, got %v, want %v", got, want)
} }
} }
uu, err := u.Units(dir)
if err != nil {
t.Errorf("unexpected error getting units from UpdateConfig: %v", err)
} else if len(uu) != 1 {
t.Errorf("unexpected number of files returned from UpdateConfig: want 1, got %d", len(uu))
} else {
unit := uu[0]
if unit.Name != "update-engine" {
t.Errorf("bad name for generated unit: want update-engine, got %s", unit.Name)
}
if unit.Command != "restart" {
t.Errorf("bad command for generated unit: want restart, got %s", unit.Command)
}
}
} }
func TestRebootStrategies(t *testing.T) { func TestRebootStrategies(t *testing.T) {
@@ -145,12 +160,13 @@ func TestRebootStrategies(t *testing.T) {
t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line) t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line)
} }
} }
u, err := uc.Unit(dir) uu, err := uc.Units(dir)
if err != nil { if err != nil {
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name) t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
} else if u == nil { } else if len(uu) != 1 {
t.Errorf("generated empty unit for reboot-strategy=%v", s.name) t.Errorf("unexpected number of units for reboot-strategy=%v: %d", s.name, len(uu))
} else { } else {
u := uu[0]
if u.Name != locksmithUnit { if u.Name != locksmithUnit {
t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name) t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name)
} }
@@ -189,8 +205,7 @@ func TestUpdateConfWrittenToDisk(t *testing.T) {
t.Fatal("Unexpectedly got nil updateconfig file") t.Fatal("Unexpectedly got nil updateconfig file")
} }
f.Path = path.Join(dir, f.Path) if _, err := system.WriteFile(f, dir); err != nil {
if err := system.WriteFile(f); err != nil {
t.Fatalf("Error writing update config: %v", err) t.Fatalf("Error writing update config: %v", err)
} }

View File

@@ -3,6 +3,7 @@ package initialize
import ( import (
"io/ioutil" "io/ioutil"
"path" "path"
"strings"
"github.com/coreos/coreos-cloudinit/system" "github.com/coreos/coreos-cloudinit/system"
) )
@@ -28,21 +29,23 @@ func PersistScriptInWorkspace(script system.Script, workspace string) (string, e
} }
tmp.Close() tmp.Close()
relpath := strings.TrimPrefix(tmp.Name(), workspace)
file := system.File{ file := system.File{
Path: tmp.Name(), Path: relpath,
RawFilePermissions: "0744", RawFilePermissions: "0744",
Content: string(script), Content: string(script),
} }
err = system.WriteFile(&file) return system.WriteFile(&file, workspace)
return file.Path, err
} }
func PersistUnitNameInWorkspace(name string, workspace string) error { func PersistUnitNameInWorkspace(name string, workspace string) error {
file := system.File{ file := system.File{
Path: path.Join(workspace, "scripts", "unit-name"), Path: path.Join("scripts", "unit-name"),
RawFilePermissions: "0644", RawFilePermissions: "0644",
Content: name, Content: name,
} }
return system.WriteFile(&file) _, err := system.WriteFile(&file, workspace)
return err
} }

View File

@@ -31,33 +31,55 @@ func (f *File) Permissions() (os.FileMode, error) {
return os.FileMode(perm), nil return os.FileMode(perm), nil
} }
func WriteFile(f *File) error { func WriteFile(f *File, root string) (string, error) {
if f.Encoding != "" { if f.Encoding != "" {
return fmt.Errorf("Unable to write file with encoding %s", f.Encoding) return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
} }
if err := os.MkdirAll(path.Dir(f.Path), os.FileMode(0755)); err != nil { fullpath := path.Join(root, f.Path)
return err dir := path.Dir(fullpath)
if err := EnsureDirectoryExists(dir); err != nil {
return "", err
} }
perm, err := f.Permissions() perm, err := f.Permissions()
if err != nil { if err != nil {
return err return "", err
} }
if err := ioutil.WriteFile(f.Path, []byte(f.Content), perm); err != nil { var tmp *os.File
return err // Create a temporary file in the same directory to ensure it's on the same filesystem
if tmp, err = ioutil.TempFile(dir, "cloudinit-temp"); err != nil {
return "", err
}
if err := ioutil.WriteFile(tmp.Name(), []byte(f.Content), perm); err != nil {
return "", err
}
if err := tmp.Close(); err != nil {
return "", err
}
// Ensure the permissions are as requested (since WriteFile can be affected by sticky bit)
if err := os.Chmod(tmp.Name(), perm); err != nil {
return "", err
} }
if f.Owner != "" { if f.Owner != "" {
// We shell out since we don't have a way to look up unix groups natively // We shell out since we don't have a way to look up unix groups natively
cmd := exec.Command("chown", f.Owner, f.Path) cmd := exec.Command("chown", f.Owner, tmp.Name())
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return err return "", err
} }
} }
return nil if err := os.Rename(tmp.Name(), fullpath); err != nil {
return "", err
}
return fullpath, nil
} }
func EnsureDirectoryExists(dir string) error { func EnsureDirectoryExists(dir string) error {

View File

@@ -4,7 +4,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"syscall"
"testing" "testing"
) )
@@ -13,18 +12,22 @@ func TestWriteFileUnencodedContent(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 syscall.Rmdir(dir) defer os.RemoveAll(dir)
fullPath := path.Join(dir, "tmp", "foo") fn := "foo"
fullPath := path.Join(dir, fn)
wf := File{ wf := File{
Path: fullPath, Path: fn,
Content: "bar", Content: "bar",
RawFilePermissions: "0644", RawFilePermissions: "0644",
} }
if err := WriteFile(&wf); err != nil { path, err := WriteFile(&wf, dir)
if err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err) t.Fatalf("Processing of WriteFile failed: %v", err)
} else if path != fullPath {
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
} }
fi, err := os.Stat(fullPath) fi, err := os.Stat(fullPath)
@@ -51,7 +54,7 @@ func TestWriteFileInvalidPermission(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 syscall.Rmdir(dir) defer os.RemoveAll(dir)
wf := File{ wf := File{
Path: path.Join(dir, "tmp", "foo"), Path: path.Join(dir, "tmp", "foo"),
@@ -59,7 +62,7 @@ func TestWriteFileInvalidPermission(t *testing.T) {
RawFilePermissions: "pants", RawFilePermissions: "pants",
} }
if err := WriteFile(&wf); err == nil { if _, err := WriteFile(&wf, dir); err == nil {
t.Fatalf("Expected error to be raised when writing file with invalid permission") t.Fatalf("Expected error to be raised when writing file with invalid permission")
} }
} }
@@ -69,17 +72,21 @@ func TestWriteFilePermissions(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 syscall.Rmdir(dir) defer os.RemoveAll(dir)
fullPath := path.Join(dir, "tmp", "foo") fn := "foo"
fullPath := path.Join(dir, fn)
wf := File{ wf := File{
Path: fullPath, Path: fn,
RawFilePermissions: "0755", RawFilePermissions: "0755",
} }
if err := WriteFile(&wf); err != nil { path, err := WriteFile(&wf, dir)
if err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err) t.Fatalf("Processing of WriteFile failed: %v", err)
} else if path != fullPath {
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
} }
fi, err := os.Stat(fullPath) fi, err := os.Stat(fullPath)
@@ -97,7 +104,7 @@ func TestWriteFileEncodedContent(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 syscall.Rmdir(dir) defer os.RemoveAll(dir)
wf := File{ wf := File{
Path: path.Join(dir, "tmp", "foo"), Path: path.Join(dir, "tmp", "foo"),
@@ -105,7 +112,7 @@ func TestWriteFileEncodedContent(t *testing.T) {
Encoding: "base64", Encoding: "base64",
} }
if err := WriteFile(&wf); err == nil { if _, err := WriteFile(&wf, dir); err == nil {
t.Fatalf("Expected error to be raised when writing file with encoding") t.Fatalf("Expected error to be raised when writing file with encoding")
} }
} }

View File

@@ -51,10 +51,10 @@ func (u *Unit) Group() (group string) {
type Script []byte type Script []byte
// UnitDestination builds the appropriate absolute file path for // Destination builds the appropriate absolute file path for
// the given Unit. The root argument indicates the effective base // the 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 (u *Unit) Destination(root string) string {
dir := "etc" dir := "etc"
if u.Runtime { if u.Runtime {
dir = "run" dir = "run"
@@ -78,12 +78,12 @@ func PlaceUnit(u *Unit, dst string) error {
} }
file := File{ file := File{
Path: dst, Path: filepath.Base(dst),
Content: u.Content, Content: u.Content,
RawFilePermissions: "0644", RawFilePermissions: "0644",
} }
err := WriteFile(&file) _, err := WriteFile(&file, dir)
if err != nil { if err != nil {
return err return err
} }
@@ -179,12 +179,12 @@ func MachineID(root string) string {
return id return id
} }
// MaskUnit masks a Unit by the given name by symlinking its unit file (in // MaskUnit masks the given Unit by symlinking its unit file to
// /etc/systemd/system) to /dev/null, analogous to `systemctl mask` // /dev/null, analogous to `systemctl mask`.
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit // N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
// file* in /etc/systemd/system, to ensure that the mask will succeed. // file at the location*, to ensure that the mask will succeed.
func MaskUnit(unit string, root string) error { func MaskUnit(unit *Unit, root string) error {
masked := path.Join(root, "etc", "systemd", "system", unit) masked := unit.Destination(root)
if _, err := os.Stat(masked); os.IsNotExist(err) { if _, err := os.Stat(masked); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil { if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
return err return err
@@ -194,3 +194,38 @@ func MaskUnit(unit string, root string) error {
} }
return os.Symlink("/dev/null", masked) return os.Symlink("/dev/null", masked)
} }
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
// associated with the given Unit is empty or appears to be a symlink to
// /dev/null, it is removed.
func UnmaskUnit(unit *Unit, root string) error {
masked := unit.Destination(root)
ne, err := nullOrEmpty(masked)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return err
}
if !ne {
log.Printf("%s is not null or empty, refusing to unmask", masked)
return nil
}
return os.Remove(masked)
}
// nullOrEmpty checks whether a given path appears to be an empty regular file
// or a symlink to /dev/null
func nullOrEmpty(path string) (bool, error) {
fi, err := os.Stat(path)
if err != nil {
return false, err
}
m := fi.Mode()
if m.IsRegular() && fi.Size() <= 0 {
return true, nil
}
if m&os.ModeCharDevice > 0 {
return true, nil
}
return false, nil
}

View File

@@ -25,10 +25,10 @@ Address=10.209.171.177/19
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
dst := UnitDestination(&u, dir) dst := u.Destination(dir)
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network") expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
if dst != expectDst { if dst != expectDst {
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst) t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
if err := PlaceUnit(&u, dst); err != nil { if err := PlaceUnit(&u, dst); err != nil {
@@ -69,18 +69,18 @@ func TestUnitDestination(t *testing.T) {
DropIn: false, DropIn: false,
} }
dst := UnitDestination(&u, dir) dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service") expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
if dst != expectDst { if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst) t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
u.DropIn = true u.DropIn = true
dst = UnitDestination(&u, dir) dst = u.Destination(dir)
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn) expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
if dst != expectDst { if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst) t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
} }
@@ -100,10 +100,10 @@ Where=/media/state
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
dst := UnitDestination(&u, dir) dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount") expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
if dst != expectDst { if dst != expectDst {
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst) t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
if err := PlaceUnit(&u, dst); err != nil { if err := PlaceUnit(&u, dst); err != nil {
@@ -156,7 +156,8 @@ func TestMaskUnit(t *testing.T) {
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
// Ensure mask works with units that do not currently exist // Ensure mask works with units that do not currently exist
if err := MaskUnit("foo.service", dir); err != nil { uf := &Unit{Name: "foo.service"}
if err := MaskUnit(uf, dir); err != nil {
t.Fatalf("Unable to mask new unit: %v", err) t.Fatalf("Unable to mask new unit: %v", err)
} }
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service") fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
@@ -169,11 +170,12 @@ func TestMaskUnit(t *testing.T) {
} }
// Ensure mask works with unit files that already exist // Ensure mask works with unit files that already exist
ub := &Unit{Name: "bar.service"}
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service") barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
if _, err := os.Create(barPath); err != nil { if _, err := os.Create(barPath); err != nil {
t.Fatalf("Error creating new unit file: %v", err) t.Fatalf("Error creating new unit file: %v", err)
} }
if err := MaskUnit("bar.service", dir); err != nil { if err := MaskUnit(ub, dir); err != nil {
t.Fatalf("Unable to mask existing unit: %v", err) t.Fatalf("Unable to mask existing unit: %v", err)
} }
barTgt, err := os.Readlink(barPath) barTgt, err := os.Readlink(barPath)
@@ -184,3 +186,94 @@ func TestMaskUnit(t *testing.T) {
t.Fatalf("unit not masked, got unit target", barTgt) t.Fatalf("unit not masked, got unit target", barTgt)
} }
} }
func TestUnmaskUnit(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)
nilUnit := &Unit{Name: "null.service"}
if err := UnmaskUnit(nilUnit, dir); err != nil {
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
}
uf := &Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}
dst := uf.Destination(dir)
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
t.Fatalf("Unable to create unit directory: %v", err)
}
if _, err := os.Create(dst); err != nil {
t.Fatalf("Unable to write unit file: %v", err)
}
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
t.Fatalf("Unable to write unit file: %v", err)
}
if err := UnmaskUnit(uf, dir); err != nil {
t.Errorf("unmask of non-empty unit returned unexpected error: %v", err)
}
got, _ := ioutil.ReadFile(dst)
if string(got) != uf.Content {
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
}
ub := &Unit{Name: "bar.service"}
dst = ub.Destination(dir)
if err := os.Symlink("/dev/null", dst); err != nil {
t.Fatalf("Unable to create masked unit: %v", err)
}
if err := UnmaskUnit(ub, dir); err != nil {
t.Errorf("unmask of unit returned unexpected error: %v", err)
}
if _, err := os.Stat(dst); !os.IsNotExist(err) {
t.Errorf("expected %s to not exist after unmask, but got err: %s", err)
}
}
func TestNullOrEmpty(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)
non := path.Join(dir, "does_not_exist")
ne, err := nullOrEmpty(non)
if !os.IsNotExist(err) {
t.Errorf("nullOrEmpty on nonexistent file returned bad error: %v", err)
}
if ne {
t.Errorf("nullOrEmpty returned true unxpectedly")
}
regEmpty := path.Join(dir, "regular_empty_file")
_, err = os.Create(regEmpty)
if err != nil {
t.Fatalf("Unable to create tempfile: %v", err)
}
gotNe, gotErr := nullOrEmpty(regEmpty)
if !gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of regular empty file returned %t, %v - want true, nil", gotNe, gotErr)
}
reg := path.Join(dir, "regular_file")
if err := ioutil.WriteFile(reg, []byte("asdf"), 700); err != nil {
t.Fatalf("Unable to create tempfile: %v", err)
}
gotNe, gotErr = nullOrEmpty(reg)
if gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of regular file returned %t, %v - want false, nil", gotNe, gotErr)
}
null := path.Join(dir, "null")
if err := os.Symlink(os.DevNull, null); err != nil {
t.Fatalf("Unable to create /dev/null link: %s", err)
}
gotNe, gotErr = nullOrEmpty(null)
if !gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of null symlink returned %t, %v - want true, nil", gotNe, gotErr)
}
}