Merge pull request #268 from crawford/dropins

drop-in: add support for drop-ins
This commit is contained in:
Alex Crawford 2014-11-26 14:14:49 -08:00
commit 6d0fdf1a47
19 changed files with 615 additions and 375 deletions

View File

@ -16,7 +16,7 @@ We've designed our implementation to allow the same cloud-config file to work ac
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values. The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
A cloud-config file should contain `#cloud-config`, followed by an associative array which has zero or more of the following keys: A cloud-config file must contain `#cloud-config`, followed by an associative array which has zero or more of the following keys:
- `coreos` - `coreos`
- `ssh_authorized_keys` - `ssh_authorized_keys`
@ -66,7 +66,6 @@ Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
``` ```
For more information about the available configuration parameters, see the [etcd documentation][etcd-config]. For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
_Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._ _Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._
@ -158,6 +157,10 @@ Each item is an object with the following fields:
- **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already. - **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already.
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands. - **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands.
- **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. The default value is false. - **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. The default value is false.
- **drop-ins**: A list of unit drop-ins with the following fields:
- **name**: String representing unit's name. Required.
- **content**: Plaintext string representing entire file. Required.
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place. **NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
@ -184,6 +187,21 @@ coreos:
ExecStop=/usr/bin/docker stop -t 2 redis_server ExecStop=/usr/bin/docker stop -t 2 redis_server
``` ```
Add the DOCKER_OPTS environment variable to docker.service.
```yaml
#cloud-config
coreos:
units:
- name: docker.service
drop-ins:
- name: 50-insecure-registry.conf
content: |
[Service]
Environment=DOCKER_OPTS='--insecure-registry="10.0.1.0/24"'
```
Start the built-in `etcd` and `fleet` services: Start the built-in `etcd` and `fleet` services:
```yaml ```yaml

View File

@ -261,9 +261,6 @@ Address=10.209.171.177/19
if u.Name != "50-eth0.network" { if u.Name != "50-eth0.network" {
t.Errorf("Unit has incorrect name %s", u.Name) t.Errorf("Unit has incorrect name %s", u.Name)
} }
if u.Type() != "network" {
t.Errorf("Unit has incorrect type '%s'", u.Type())
}
} }
if cfg.Coreos.OEM.ID != "rackspace" { if cfg.Coreos.OEM.ID != "rackspace" {
@ -338,26 +335,6 @@ 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

@ -16,11 +16,6 @@
package config package config
import (
"path/filepath"
"strings"
)
type Unit struct { type Unit struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Mask bool `yaml:"mask"` Mask bool `yaml:"mask"`
@ -28,23 +23,10 @@ type Unit struct {
Runtime bool `yaml:"runtime"` Runtime bool `yaml:"runtime"`
Content string `yaml:"content"` Content string `yaml:"content"`
Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"` Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"`
DropIns []UnitDropIn `yaml:"drop_ins"`
// 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 { type UnitDropIn struct {
ext := filepath.Ext(u.Name) Name string `yaml:"name"`
return strings.TrimLeft(ext, ".") Content string `yaml:"content"`
}
func (u *Unit) Group() string {
switch u.Type() {
case "network", "netdev", "link":
return "network"
default:
return "system"
}
} }

View File

@ -195,66 +195,82 @@ func Apply(cfg config.CloudConfig, env *Environment) error {
// commands against units. It returns any error encountered. // commands against units. It returns any error encountered.
func processUnits(units []system.Unit, root string, um system.UnitManager) error { func processUnits(units []system.Unit, root string, um system.UnitManager) error {
type action struct { type action struct {
unit string unit system.Unit
command string command string
} }
actions := make([]action, 0, len(units)) actions := make([]action, 0, len(units))
reload := false reload := false
for _, unit := range units { for _, unit := range units {
dst := unit.Destination(root) if unit.Name == "" {
log.Printf("Skipping unit without name")
continue
}
if unit.Content != "" { if unit.Content != "" {
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) log.Printf("Writing unit %q to filesystem", unit.Name)
if err := um.PlaceUnit(&unit, dst); err != nil { if err := um.PlaceUnit(unit); err != nil {
return err return err
} }
log.Printf("Placed unit %s at %s", unit.Name, dst) log.Printf("Wrote unit %q", unit.Name)
reload = true reload = true
} }
for _, dropin := range unit.DropIns {
if dropin.Name != "" && dropin.Content != "" {
log.Printf("Writing drop-in unit %q to filesystem", dropin.Name)
if err := um.PlaceUnitDropIn(unit, dropin); err != nil {
return err
}
log.Printf("Wrote drop-in unit %q", dropin.Name)
reload = true
}
}
if unit.Mask { if unit.Mask {
log.Printf("Masking unit file %s", unit.Name) log.Printf("Masking unit file %q", unit.Name)
if err := um.MaskUnit(&unit); err != nil { if err := um.MaskUnit(unit); err != nil {
return err return err
} }
} else if unit.Runtime { } else if unit.Runtime {
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name) log.Printf("Ensuring runtime unit file %q is unmasked", unit.Name)
if err := um.UnmaskUnit(&unit); err != nil { if err := um.UnmaskUnit(unit); err != nil {
return err return err
} }
} }
if unit.Enable { if unit.Enable {
if unit.Group() != "network" { if unit.Group() != "network" {
log.Printf("Enabling unit file %s", unit.Name) log.Printf("Enabling unit file %q", unit.Name)
if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil { if err := um.EnableUnitFile(unit); err != nil {
return err return err
} }
log.Printf("Enabled unit %s", unit.Name) log.Printf("Enabled unit %q", unit.Name)
} else { } else {
log.Printf("Skipping enable for network-like unit %s", unit.Name) log.Printf("Skipping enable for network-like unit %q", unit.Name)
} }
} }
if unit.Group() == "network" { if unit.Group() == "network" {
actions = append(actions, action{"systemd-networkd.service", "restart"}) networkd := system.Unit{Unit: config.Unit{Name: "systemd-networkd.service"}}
actions = append(actions, action{networkd, "restart"})
} else if unit.Command != "" { } else if unit.Command != "" {
actions = append(actions, action{unit.Name, unit.Command}) actions = append(actions, action{unit, unit.Command})
} }
} }
if reload { if reload {
if err := um.DaemonReload(); err != nil { if err := um.DaemonReload(); err != nil {
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err)) return errors.New(fmt.Sprintf("failed systemd daemon-reload: %s", err))
} }
} }
for _, action := range actions { for _, action := range actions {
log.Printf("Calling unit command '%s %s'", action.command, action.unit) log.Printf("Calling unit command %q on %q'", action.command, action.unit.Name)
res, err := um.RunUnitCommand(action.command, action.unit) res, err := um.RunUnitCommand(action.unit, action.command)
if err != nil { if err != nil {
return err return err
} }
log.Printf("Result of '%s %s': %s", action.command, action.unit, res) log.Printf("Result of %q on %q': %s", action.command, action.unit.Name, res)
} }
return nil return nil

View File

@ -17,6 +17,7 @@
package initialize package initialize
import ( import (
"reflect"
"testing" "testing"
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
@ -32,99 +33,171 @@ type TestUnitManager struct {
reload bool reload bool
} }
func (tum *TestUnitManager) PlaceUnit(unit *system.Unit, dst string) error { func (tum *TestUnitManager) PlaceUnit(u system.Unit) error {
tum.placed = append(tum.placed, unit.Name) tum.placed = append(tum.placed, u.Name)
return nil return nil
} }
func (tum *TestUnitManager) EnableUnitFile(unit string, runtime bool) error { func (tum *TestUnitManager) PlaceUnitDropIn(u system.Unit, d config.UnitDropIn) error {
tum.enabled = append(tum.enabled, unit) tum.placed = append(tum.placed, u.Name+".d/"+d.Name)
return nil return nil
} }
func (tum *TestUnitManager) RunUnitCommand(command, unit string) (string, error) { func (tum *TestUnitManager) EnableUnitFile(u system.Unit) error {
tum.enabled = append(tum.enabled, u.Name)
return nil
}
func (tum *TestUnitManager) RunUnitCommand(u system.Unit, c string) (string, error) {
tum.commands = make(map[string]string) tum.commands = make(map[string]string)
tum.commands[unit] = command tum.commands[u.Name] = c
return "", nil return "", nil
} }
func (tum *TestUnitManager) DaemonReload() error { func (tum *TestUnitManager) DaemonReload() error {
tum.reload = true tum.reload = true
return nil return nil
} }
func (tum *TestUnitManager) MaskUnit(unit *system.Unit) error { func (tum *TestUnitManager) MaskUnit(u system.Unit) error {
tum.masked = append(tum.masked, unit.Name) tum.masked = append(tum.masked, u.Name)
return nil return nil
} }
func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error { func (tum *TestUnitManager) UnmaskUnit(u system.Unit) error {
tum.unmasked = append(tum.unmasked, unit.Name) tum.unmasked = append(tum.unmasked, u.Name)
return nil return nil
} }
func TestProcessUnits(t *testing.T) { func TestProcessUnits(t *testing.T) {
tum := &TestUnitManager{} tests := []struct {
units := []system.Unit{ units []system.Unit
result TestUnitManager
}{
{
units: []system.Unit{
system.Unit{Unit: config.Unit{ system.Unit{Unit: config.Unit{
Name: "foo", Name: "foo",
Mask: true, Mask: true,
}}, }},
} },
if err := processUnits(units, "", tum); err != nil { result: TestUnitManager{
t.Fatalf("unexpected error calling processUnits: %v", err) masked: []string{"foo"},
} },
if len(tum.masked) != 1 || tum.masked[0] != "foo" { },
t.Errorf("expected foo to be masked, but found %v", tum.masked) {
} units: []system.Unit{
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{Unit: config.Unit{ system.Unit{Unit: config.Unit{
Name: "bar.network", Name: "bar.network",
}}, }},
} },
if err := processUnits(units, "", tum); err != nil { result: TestUnitManager{
t.Fatalf("unexpected error calling processUnits: %v", err) commands: map[string]string{
} "systemd-networkd.service": "restart",
if _, ok := tum.commands["systemd-networkd.service"]; !ok { },
t.Errorf("expected systemd-networkd.service to be reloaded!") },
} },
{
tum = &TestUnitManager{} units: []system.Unit{
units = []system.Unit{
system.Unit{Unit: config.Unit{ system.Unit{Unit: config.Unit{
Name: "baz.service", Name: "baz.service",
Content: "[Service]\nExecStart=/bin/true", Content: "[Service]\nExecStart=/bin/true",
}}, }},
} },
if err := processUnits(units, "", tum); err != nil { result: TestUnitManager{
t.Fatalf("unexpected error calling processUnits: %v", err) placed: []string{"baz.service"},
} reload: true,
if len(tum.placed) != 1 || tum.placed[0] != "baz.service" { },
t.Fatalf("expected baz.service to be written, but got %v", tum.placed) },
} {
units: []system.Unit{
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{Unit: config.Unit{ system.Unit{Unit: config.Unit{
Name: "locksmithd.service", Name: "locksmithd.service",
Runtime: true, Runtime: true,
}}, }},
} },
if err := processUnits(units, "", tum); err != nil { result: TestUnitManager{
t.Fatalf("unexpected error calling processUnits: %v", err) unmasked: []string{"locksmithd.service"},
} },
if len(tum.unmasked) != 1 || tum.unmasked[0] != "locksmithd.service" { },
t.Fatalf("expected locksmithd.service to be unmasked, but got %v", tum.unmasked) {
} units: []system.Unit{
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{Unit: config.Unit{ system.Unit{Unit: config.Unit{
Name: "woof", Name: "woof",
Enable: true, Enable: true,
}}, }},
},
result: TestUnitManager{
enabled: []string{"woof"},
},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "hi.service",
Runtime: true,
Content: "[Service]\nExecStart=/bin/echo hi",
DropIns: []config.UnitDropIn{
{
Name: "lo.conf",
Content: "[Service]\nExecStart=/bin/echo lo",
},
{
Name: "bye.conf",
Content: "[Service]\nExecStart=/bin/echo bye",
},
},
}},
},
result: TestUnitManager{
placed: []string{"hi.service", "hi.service.d/lo.conf", "hi.service.d/bye.conf"},
unmasked: []string{"hi.service"},
reload: true,
},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
DropIns: []config.UnitDropIn{
{
Name: "lo.conf",
Content: "[Service]\nExecStart=/bin/echo lo",
},
},
}},
},
result: TestUnitManager{},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "hi.service",
DropIns: []config.UnitDropIn{
{
Content: "[Service]\nExecStart=/bin/echo lo",
},
},
}},
},
result: TestUnitManager{},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "hi.service",
DropIns: []config.UnitDropIn{
{
Name: "lo.conf",
},
},
}},
},
result: TestUnitManager{},
},
} }
if err := processUnits(units, "", tum); err != nil {
t.Fatalf("unexpected error calling processUnits: %v", err) for _, tt := range tests {
tum := &TestUnitManager{}
if err := processUnits(tt.units, "", tum); err != nil {
t.Errorf("bad error (%+v): want nil, got %s", tt.units, err)
}
if !reflect.DeepEqual(tt.result, *tum) {
t.Errorf("bad result (%+v): want %+v, got %+v", tt.units, tt.result, tum)
} }
if len(tum.enabled) != 1 || tum.enabled[0] != "woof" {
t.Fatalf("expected woof to be enabled, but got %v", tum.enabled)
} }
} }

View File

@ -25,7 +25,7 @@ import (
// dropinContents generates the contents for a drop-in unit given the config. // dropinContents generates the contents for a drop-in unit given the config.
// The argument must be a struct from the 'config' package. // The argument must be a struct from the 'config' package.
func dropinContents(e interface{}) string { func serviceContents(e interface{}) string {
et := reflect.TypeOf(e) et := reflect.TypeOf(e)
ev := reflect.ValueOf(e) ev := reflect.ValueOf(e)
@ -42,16 +42,3 @@ func dropinContents(e interface{}) string {
} }
return "[Service]\n" + out return "[Service]\n" + out
} }
func dropinFromConfig(cfg interface{}, name string) []Unit {
content := dropinContents(cfg)
if content == "" {
return nil
}
return []Unit{{config.Unit{
Name: name,
Runtime: true,
DropIn: true,
Content: content,
}}}
}

View File

@ -4,7 +4,7 @@ import (
"testing" "testing"
) )
func TestDropinContents(t *testing.T) { func TestServiceContents(t *testing.T) {
tests := []struct { tests := []struct {
Config interface{} Config interface{}
Contents string Contents string
@ -48,7 +48,7 @@ Environment="D=0.1"
} }
for _, tt := range tests { for _, tt := range tests {
if c := dropinContents(tt.Config); c != tt.Contents { if c := serviceContents(tt.Config); c != tt.Contents {
t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Contents, c) t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Contents, c)
} }
} }

View File

@ -28,5 +28,12 @@ type Etcd struct {
// Units creates a Unit file drop-in for etcd, using any configured options. // Units creates a Unit file drop-in for etcd, using any configured options.
func (ee Etcd) Units() []Unit { func (ee Etcd) Units() []Unit {
return dropinFromConfig(ee.Etcd, "etcd.service") return []Unit{{config.Unit{
Name: "etcd.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: serviceContents(ee.Etcd),
}},
}}}
} }

View File

@ -30,7 +30,11 @@ func TestEtcdUnits(t *testing.T) {
}{ }{
{ {
config.Etcd{}, config.Etcd{},
nil, []Unit{{config.Unit{
Name: "etcd.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
}, },
{ {
config.Etcd{ config.Etcd{
@ -40,11 +44,13 @@ func TestEtcdUnits(t *testing.T) {
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "etcd.service", Name: "etcd.service",
Runtime: true, Runtime: true,
DropIn: true, DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service] Content: `[Service]
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar" Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`, `,
}},
}}}, }}},
}, },
{ {
@ -56,12 +62,14 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "etcd.service", Name: "etcd.service",
Runtime: true, Runtime: true,
DropIn: true, DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service] Content: `[Service]
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar" Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_NAME=node001" Environment="ETCD_NAME=node001"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`, `,
}},
}}}, }}},
}, },
} { } {

View File

@ -19,6 +19,7 @@ package system
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"os/exec" "os/exec"
"path" "path"
@ -47,13 +48,14 @@ func (f *File) Permissions() (os.FileMode, error) {
} }
func WriteFile(f *File, root string) (string, error) { func WriteFile(f *File, root string) (string, error) {
fullpath := path.Join(root, f.Path)
dir := path.Dir(fullpath)
log.Printf("Writing file to %q", fullpath)
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)
} }
fullpath := path.Join(root, f.Path)
dir := path.Dir(fullpath)
if err := EnsureDirectoryExists(dir); err != nil { if err := EnsureDirectoryExists(dir); err != nil {
return "", err return "", err
} }
@ -94,6 +96,7 @@ func WriteFile(f *File, root string) (string, error) {
return "", err return "", err
} }
log.Printf("Wrote file to %q", fullpath)
return fullpath, nil return fullpath, nil
} }

View File

@ -13,5 +13,12 @@ type Flannel struct {
// Units generates a Unit file drop-in for flannel, if any flannel options were // Units generates a Unit file drop-in for flannel, if any flannel options were
// configured in cloud-config // configured in cloud-config
func (fl Flannel) Units() []Unit { func (fl Flannel) Units() []Unit {
return dropinFromConfig(fl.Flannel, "flanneld.service") return []Unit{{config.Unit{
Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: serviceContents(fl.Flannel),
}},
}}}
} }

View File

@ -14,7 +14,11 @@ func TestFlannelUnits(t *testing.T) {
}{ }{
{ {
config.Flannel{}, config.Flannel{},
nil, []Unit{{config.Unit{
Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
}, },
{ {
config.Flannel{ config.Flannel{
@ -23,12 +27,14 @@ func TestFlannelUnits(t *testing.T) {
}, },
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "flanneld.service", Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service] Content: `[Service]
Environment="FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001" Environment="FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001"
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1" Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1"
`, `,
Runtime: true, }},
DropIn: true,
}}}, }}},
}, },
} { } {

View File

@ -29,5 +29,12 @@ type Fleet struct {
// Units 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 Fleet) Units() []Unit { func (fe Fleet) Units() []Unit {
return dropinFromConfig(fe.Fleet, "fleet.service") return []Unit{{config.Unit{
Name: "fleet.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: serviceContents(fe.Fleet),
}},
}}}
} }

View File

@ -30,7 +30,11 @@ func TestFleetUnits(t *testing.T) {
}{ }{
{ {
config.Fleet{}, config.Fleet{},
nil, []Unit{{config.Unit{
Name: "fleet.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
}, },
{ {
config.Fleet{ config.Fleet{
@ -38,11 +42,13 @@ func TestFleetUnits(t *testing.T) {
}, },
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "fleet.service", Name: "fleet.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service] Content: `[Service]
Environment="FLEET_PUBLIC_IP=12.34.56.78" Environment="FLEET_PUBLIC_IP=12.34.56.78"
`, `,
Runtime: true, }},
DropIn: true,
}}}, }}},
}, },
} { } {

View File

@ -96,7 +96,8 @@ func maybeProbeBonding(interfaces []network.InterfaceGenerator) error {
func restartNetworkd() error { func restartNetworkd() error {
log.Printf("Restarting networkd.service\n") log.Printf("Restarting networkd.service\n")
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service") networkd := Unit{config.Unit{Name: "systemd-networkd.service"}}
_, err := NewUnitManager("").RunUnitCommand(networkd, "restart")
return err return err
} }

View File

@ -23,7 +23,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"strings" "strings"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/go-systemd/dbus" "github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/go-systemd/dbus"
@ -42,49 +41,51 @@ type systemd struct {
// never be used as a true MachineID // never be used as a true MachineID
const fakeMachineID = "42000000000000000000000000000042" const fakeMachineID = "42000000000000000000000000000042"
// PlaceUnit writes a unit file at the provided destination, creating // PlaceUnit writes a unit file at its desired destination, creating parent
// parent directories as necessary. // directories as necessary.
func (s *systemd) PlaceUnit(u *Unit, dst string) error { func (s *systemd) PlaceUnit(u Unit) error {
dir := filepath.Dir(dst)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
return err
}
}
file := File{config.File{ file := File{config.File{
Path: filepath.Base(dst), Path: u.Destination(s.root),
Content: u.Content, Content: u.Content,
RawFilePermissions: "0644", RawFilePermissions: "0644",
}} }}
_, err := WriteFile(&file, dir) _, err := WriteFile(&file, "/")
if err != nil {
return err return err
}
return nil
} }
func (s *systemd) EnableUnitFile(unit string, runtime bool) error { // PlaceUnitDropIn writes a unit drop-in file at its desired destination,
// creating parent directories as necessary.
func (s *systemd) PlaceUnitDropIn(u Unit, d config.UnitDropIn) error {
file := File{config.File{
Path: u.DropInDestination(s.root, d),
Content: d.Content,
RawFilePermissions: "0644",
}}
_, err := WriteFile(&file, "/")
return err
}
func (s *systemd) EnableUnitFile(u Unit) error {
conn, err := dbus.New() conn, err := dbus.New()
if err != nil { if err != nil {
return err return err
} }
units := []string{unit} units := []string{u.Name}
_, _, err = conn.EnableUnitFiles(units, runtime, true) _, _, err = conn.EnableUnitFiles(units, u.Runtime, true)
return err return err
} }
func (s *systemd) RunUnitCommand(command, unit string) (string, error) { func (s *systemd) RunUnitCommand(u Unit, c string) (string, error) {
conn, err := dbus.New() conn, err := dbus.New()
if err != nil { if err != nil {
return "", err return "", err
} }
var fn func(string, string) (string, error) var fn func(string, string) (string, error)
switch command { switch c {
case "start": case "start":
fn = conn.StartUnit fn = conn.StartUnit
case "stop": case "stop":
@ -100,10 +101,10 @@ func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
case "reload-or-try-restart": case "reload-or-try-restart":
fn = conn.ReloadOrTryRestartUnit fn = conn.ReloadOrTryRestartUnit
default: default:
return "", fmt.Errorf("Unsupported systemd command %q", command) return "", fmt.Errorf("Unsupported systemd command %q", c)
} }
return fn(unit, "replace") return fn(u.Name, "replace")
} }
func (s *systemd) DaemonReload() error { func (s *systemd) DaemonReload() error {
@ -119,8 +120,8 @@ func (s *systemd) DaemonReload() error {
// /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 at the location*, to ensure that the mask will succeed. // file at the location*, to ensure that the mask will succeed.
func (s *systemd) MaskUnit(unit *Unit) error { func (s *systemd) MaskUnit(u Unit) error {
masked := unit.Destination(s.root) masked := u.Destination(s.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
@ -134,8 +135,8 @@ func (s *systemd) MaskUnit(unit *Unit) error {
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file // 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 // associated with the given Unit is empty or appears to be a symlink to
// /dev/null, it is removed. // /dev/null, it is removed.
func (s *systemd) UnmaskUnit(unit *Unit) error { func (s *systemd) UnmaskUnit(u Unit) error {
masked := unit.Destination(s.root) masked := u.Destination(s.root)
ne, err := nullOrEmpty(masked) ne, err := nullOrEmpty(masked)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil return nil

View File

@ -17,6 +17,7 @@
package system package system
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
@ -25,133 +26,109 @@ import (
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
) )
func TestPlaceNetworkUnit(t *testing.T) { func TestPlaceUnit(t *testing.T) {
u := Unit{config.Unit{ tests := []config.Unit{
{
Name: "50-eth0.network", Name: "50-eth0.network",
Runtime: true, Runtime: true,
Content: `[Match] Content: "[Match]\nName=eth47\n\n[Network]\nAddress=10.209.171.177/19\n",
Name=eth47 },
{
[Network]
Address=10.209.171.177/19
`,
}}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
sd := &systemd{dir}
dst := u.Destination(dir)
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
if dst != expectDst {
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
}
if err := sd.PlaceUnit(&u, dst); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
fi, err := os.Stat(dst)
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(dst)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expectContents := `[Match]
Name=eth47
[Network]
Address=10.209.171.177/19
`
if string(contents) != expectContents {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
}
}
func TestUnitDestination(t *testing.T) {
dir := "/some/dir"
name := "foobar.service"
u := Unit{config.Unit{
Name: name,
DropIn: false,
}}
dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
if dst != expectDst {
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
}
u.DropIn = true
dst = u.Destination(dir)
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
if dst != expectDst {
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
}
}
func TestPlaceMountUnit(t *testing.T) {
u := Unit{config.Unit{
Name: "media-state.mount", Name: "media-state.mount",
Runtime: false, Content: "[Mount]\nWhat=/dev/sdb1\nWhere=/media/state\n",
Content: `[Mount] },
What=/dev/sdb1 }
Where=/media/state
`,
}}
for _, tt := range tests {
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) panic(fmt.Sprintf("Unable to create tempdir: %v", err))
} }
defer os.RemoveAll(dir)
u := Unit{tt}
sd := &systemd{dir} sd := &systemd{dir}
dst := u.Destination(dir) if err := sd.PlaceUnit(u); err != nil {
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount") t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err)
if dst != expectDst {
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
if err := sd.PlaceUnit(&u, dst); err != nil { fi, err := os.Stat(u.Destination(dir))
t.Fatalf("PlaceUnit failed: %v", err)
}
fi, err := os.Stat(dst)
if err != nil { if err != nil {
t.Fatalf("Unable to stat file: %v", err) t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err)
} }
if fi.Mode() != os.FileMode(0644) { if mode := fi.Mode(); mode != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode()) t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode)
} }
contents, err := ioutil.ReadFile(dst) c, err := ioutil.ReadFile(u.Destination(dir))
if err != nil { if err != nil {
t.Fatalf("Unable to read expected file: %v", err) t.Fatalf("ReadFile(): bad error (%+v): want nil, got %s", tt, err)
} }
expectContents := `[Mount] if string(c) != tt.Content {
What=/dev/sdb1 t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Content, string(c))
Where=/media/state }
`
if string(contents) != expectContents { os.RemoveAll(dir)
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents) }
}
func TestPlaceUnitDropIn(t *testing.T) {
tests := []config.Unit{
{
Name: "false.service",
Runtime: true,
DropIns: []config.UnitDropIn{
{
Name: "00-true.conf",
Content: "[Service]\nExecStart=\nExecStart=/usr/bin/true\n",
},
},
},
{
Name: "true.service",
DropIns: []config.UnitDropIn{
{
Name: "00-false.conf",
Content: "[Service]\nExecStart=\nExecStart=/usr/bin/false\n",
},
},
},
}
for _, tt := range tests {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
panic(fmt.Sprintf("Unable to create tempdir: %v", err))
}
u := Unit{tt}
sd := &systemd{dir}
if err := sd.PlaceUnitDropIn(u, u.DropIns[0]); err != nil {
t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err)
}
fi, err := os.Stat(u.DropInDestination(dir, u.DropIns[0]))
if err != nil {
t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err)
}
if mode := fi.Mode(); mode != os.FileMode(0644) {
t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode)
}
c, err := ioutil.ReadFile(u.DropInDestination(dir, u.DropIns[0]))
if err != nil {
t.Fatalf("ReadFile(): bad error (%+v): want nil, got %s", tt, err)
}
if string(c) != u.DropIns[0].Content {
t.Errorf("bad contents (%+v): want %q, got %q", tt, u.DropIns[0].Content, string(c))
}
os.RemoveAll(dir)
} }
} }
@ -180,7 +157,7 @@ func TestMaskUnit(t *testing.T) {
sd := &systemd{dir} sd := &systemd{dir}
// Ensure mask works with units that do not currently exist // Ensure mask works with units that do not currently exist
uf := &Unit{config.Unit{Name: "foo.service"}} uf := Unit{config.Unit{Name: "foo.service"}}
if err := sd.MaskUnit(uf); err != nil { if err := sd.MaskUnit(uf); err != nil {
t.Fatalf("Unable to mask new unit: %v", err) t.Fatalf("Unable to mask new unit: %v", err)
} }
@ -194,7 +171,7 @@ 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{config.Unit{Name: "bar.service"}} ub := Unit{config.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)
@ -220,12 +197,12 @@ func TestUnmaskUnit(t *testing.T) {
sd := &systemd{dir} sd := &systemd{dir}
nilUnit := &Unit{config.Unit{Name: "null.service"}} nilUnit := Unit{config.Unit{Name: "null.service"}}
if err := sd.UnmaskUnit(nilUnit); err != nil { if err := sd.UnmaskUnit(nilUnit); err != nil {
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err) t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
} }
uf := &Unit{config.Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}} uf := Unit{config.Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}}
dst := uf.Destination(dir) dst := uf.Destination(dir)
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil { if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
t.Fatalf("Unable to create unit directory: %v", err) t.Fatalf("Unable to create unit directory: %v", err)
@ -245,7 +222,7 @@ func TestUnmaskUnit(t *testing.T) {
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly") t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
} }
ub := &Unit{config.Unit{Name: "bar.service"}} ub := Unit{config.Unit{Name: "bar.service"}}
dst = ub.Destination(dir) dst = ub.Destination(dir)
if err := os.Symlink("/dev/null", dst); err != nil { if err := os.Symlink("/dev/null", dst); err != nil {
t.Fatalf("Unable to create masked unit: %v", err) t.Fatalf("Unable to create masked unit: %v", err)

View File

@ -19,40 +19,66 @@ package system
import ( import (
"fmt" "fmt"
"path" "path"
"path/filepath"
"strings"
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
) )
// Name for drop-in service configuration files created by cloudconfig
const cloudConfigDropIn = "20-cloudinit.conf"
type UnitManager interface { type UnitManager interface {
PlaceUnit(unit *Unit, dst string) error PlaceUnit(unit Unit) error
EnableUnitFile(unit string, runtime bool) error PlaceUnitDropIn(unit Unit, dropIn config.UnitDropIn) error
RunUnitCommand(command, unit string) (string, error) EnableUnitFile(unit Unit) error
RunUnitCommand(unit Unit, command string) (string, error)
MaskUnit(unit Unit) error
UnmaskUnit(unit Unit) error
DaemonReload() error DaemonReload() error
MaskUnit(unit *Unit) error
UnmaskUnit(unit *Unit) error
} }
// Unit is a top-level structure which embeds its underlying configuration, // Unit is a top-level structure which embeds its underlying configuration,
// config.Unit, and provides the system-specific Destination(). // config.Unit, and provides the system-specific Destination(), Type(), and
// Group().
type Unit struct { type Unit struct {
config.Unit config.Unit
} }
// Destination builds the appropriate absolute file path for // Type returns the extension of the unit (everything that follows the final
// the Unit. The root argument indicates the effective base // period).
// directory of the system (similar to a chroot). func (u Unit) Type() string {
func (u *Unit) Destination(root string) string { ext := filepath.Ext(u.Name)
return strings.TrimLeft(ext, ".")
}
// Group returns "network" or "system" depending on whether or not the unit is
// a network unit or otherwise.
func (u Unit) Group() string {
switch u.Type() {
case "network", "netdev", "link":
return "network"
default:
return "system"
}
}
// Destination builds the appropriate absolute file path for the Unit. The root
// argument indicates the effective base directory of the system (similar to a
// chroot).
func (u Unit) Destination(root string) string {
return path.Join(u.prefix(root), u.Name)
}
// DropInDestination builds the appropriate absolute file path for the
// UnitDropIn. The root argument indicates the effective base directory of the
// system (similar to a chroot) and the dropIn argument is the UnitDropIn for
// which the destination is being calculated.
func (u Unit) DropInDestination(root string, dropIn config.UnitDropIn) string {
return path.Join(u.prefix(root), fmt.Sprintf("%s.d", u.Name), dropIn.Name)
}
func (u Unit) prefix(root string) string {
dir := "etc" dir := "etc"
if u.Runtime { if u.Runtime {
dir = "run" dir = "run"
} }
return path.Join(root, dir, "systemd", u.Group())
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)
}
} }

138
system/unit_test.go Normal file
View File

@ -0,0 +1,138 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
"testing"
"github.com/coreos/coreos-cloudinit/config"
)
func TestType(t *testing.T) {
tests := []struct {
name string
typ string
}{
{},
{"test.service", "service"},
{"hello", ""},
{"lots.of.dots", "dots"},
}
for _, tt := range tests {
u := Unit{config.Unit{
Name: tt.name,
}}
if typ := u.Type(); tt.typ != typ {
t.Errorf("bad type (%+v): want %q, got %q", tt, tt.typ, typ)
}
}
}
func TestGroup(t *testing.T) {
tests := []struct {
name string
group string
}{
{"test.service", "system"},
{"test.link", "network"},
{"test.network", "network"},
{"test.netdev", "network"},
{"test.conf", "system"},
}
for _, tt := range tests {
u := Unit{config.Unit{
Name: tt.name,
}}
if group := u.Group(); tt.group != group {
t.Errorf("bad group (%+v): want %q, got %q", tt, tt.group, group)
}
}
}
func TestDestination(t *testing.T) {
tests := []struct {
root string
name string
runtime bool
destination string
}{
{
root: "/some/dir",
name: "foobar.service",
destination: "/some/dir/etc/systemd/system/foobar.service",
},
{
root: "/some/dir",
name: "foobar.service",
runtime: true,
destination: "/some/dir/run/systemd/system/foobar.service",
},
}
for _, tt := range tests {
u := Unit{config.Unit{
Name: tt.name,
Runtime: tt.runtime,
}}
if d := u.Destination(tt.root); tt.destination != d {
t.Errorf("bad destination (%+v): want %q, got %q", tt, tt.destination, d)
}
}
}
func TestDropInDestination(t *testing.T) {
tests := []struct {
root string
unitName string
dropInName string
runtime bool
destination string
}{
{
root: "/some/dir",
unitName: "foo.service",
dropInName: "bar.conf",
destination: "/some/dir/etc/systemd/system/foo.service.d/bar.conf",
},
{
root: "/some/dir",
unitName: "foo.service",
dropInName: "bar.conf",
runtime: true,
destination: "/some/dir/run/systemd/system/foo.service.d/bar.conf",
},
}
for _, tt := range tests {
u := Unit{config.Unit{
Name: tt.unitName,
Runtime: tt.runtime,
DropIns: []config.UnitDropIn{{
Name: tt.dropInName,
}},
}}
if d := u.DropInDestination(tt.root, u.DropIns[0]); tt.destination != d {
t.Errorf("bad destination (%+v): want %q, got %q", tt, tt.destination, d)
}
}
}