Merge pull request #7 from bcwaldon/units

feat(unit): Provide units on boot
This commit is contained in:
Brian Waldon 2014-03-12 16:44:27 -07:00
commit 8fa8495100
5 changed files with 302 additions and 5 deletions

View File

@ -27,6 +27,16 @@ The value of `coreos.etcd.discovery_url` will be used to discover the instance's
[disco-proto]: https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md [disco-proto]: https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md
[disco-service]: http://discovery.etcd.io [disco-service]: http://discovery.etcd.io
#### coreos.units
Arbitrary systemd units may be provided in the `coreos.units` attribute.
`coreos.units` is a list of objects with the following fields:
- **name**: string representing unit's name
- **runtime**: boolean indicating whether or not to persist the unit across reboots. This is analagous to the `--runtime` flag to `systemd enable`.
- **content**: plaintext string representing entire unit file
See docker example below.
## user-data Script ## user-data Script
@ -40,8 +50,7 @@ echo 'Hello, world!'
## Examples ## Examples
### Inject an SSH key, bootstrap etcd, and start fleet using a cloud-config ### Inject an SSH key, bootstrap etcd, and start fleet
``` ```
#cloud-config #cloud-config
@ -50,8 +59,29 @@ coreos:
discovery_url: https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877 discovery_url: https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877
fleet: fleet:
autostart: yes autostart: yes
ssh_authorized_keys: ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h... - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
``` ```
### Start a docker container on boot
```
#cloud-config
coreos:
units:
- name: docker-redis.service
content: |
[Unit]
Description=Redis container
Author=Me
After=docker.service
[Service]
Restart=always
ExecStart=/usr/bin/docker start -a redis_server
ExecStop=/usr/bin/docker stop -t 2 redis_server
[Install]
WantedBy=local.target
```

View File

@ -13,6 +13,7 @@ type CloudConfig struct {
Coreos struct { Coreos struct {
Etcd struct{ Discovery_URL string } Etcd struct{ Discovery_URL string }
Fleet struct{ Autostart bool } Fleet struct{ Autostart bool }
Units []Unit
} }
Write_Files []WriteFile Write_Files []WriteFile
} }
@ -60,8 +61,25 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
} }
} }
if len(cfg.Coreos.Units) > 0 {
for _, unit := range cfg.Coreos.Units {
dst, err := PlaceUnit("/", &unit)
if err != nil {
return err
}
log.Printf("Placed unit %s at %s", unit.Name, dst)
if err := EnableUnitFile(dst, unit.Runtime); err != nil {
return err
}
log.Printf("Enabled unit %s", unit.Name)
}
DaemonReload()
StartUnits(cfg.Coreos.Units)
}
if cfg.Coreos.Fleet.Autostart { if cfg.Coreos.Fleet.Autostart {
err := StartUnit("fleet.service") err := StartUnitByName("fleet.service")
if err == nil { if err == nil {
log.Printf("Started fleet service.") log.Printf("Started fleet service.")
} else { } else {

View File

@ -37,6 +37,19 @@ coreos:
discovery_url: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" discovery_url: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
fleet: fleet:
autostart: Yes autostart: Yes
units:
- name: 50-eth0.network
runtime: yes
content: '[Match]
Name=eth47
[Network]
Address=10.209.171.177/19
'
ssh_authorized_keys: ssh_authorized_keys:
- foobar - foobar
- foobaz - foobaz
@ -90,6 +103,31 @@ write_files:
t.Errorf("WriteFile has incorrect owner %s", wf.Owner) t.Errorf("WriteFile has incorrect owner %s", wf.Owner)
} }
} }
if len(cfg.Coreos.Units) != 1 {
t.Error("Failed to parse correct number of units")
} else {
u := cfg.Coreos.Units[0]
expect := `[Match]
Name=eth47
[Network]
Address=10.209.171.177/19
`
if u.Content != expect {
t.Errorf("Unit has incorrect contents '%s'.\nExpected '%s'.", u.Content, expect)
}
if u.Runtime != true {
t.Errorf("Unit has incorrect runtime value")
}
if u.Name != "50-eth0.network" {
t.Errorf("Unit has incorrect name %s", u.Name)
}
if u.Type() != "network" {
t.Errorf("Unit has incorrect type '%s'", u.Type())
}
}
} }
// Assert that our interface conversion doesn't panic // Assert that our interface conversion doesn't panic

View File

@ -2,15 +2,124 @@ package cloudinit
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os"
"path" "path"
"path/filepath"
"strings"
"github.com/coreos/go-systemd/dbus" "github.com/coreos/go-systemd/dbus"
) )
type Unit struct {
Name string
Runtime bool
Content string
}
func (u *Unit) Type() string {
ext := filepath.Ext(u.Name)
return strings.TrimLeft(ext, ".")
}
func (u *Unit) Group() (group string) {
t := u.Type()
if t == "network" || t == "netdev" || t == "link" {
group = "network"
} else {
group = "system"
}
return
}
type Script []byte type Script []byte
func StartUnit(name string) error { func PlaceUnit(root string, u *Unit) (string, error) {
dir := "etc"
if u.Runtime {
dir = "run"
}
dst := path.Join(root, dir, "systemd", u.Group())
if _, err := os.Stat(dst); os.IsNotExist(err) {
if err := os.MkdirAll(dst, os.FileMode(0755)); err != nil {
return "", err
}
}
dst = path.Join(dst, u.Name)
err := ioutil.WriteFile(dst, []byte(u.Content), os.FileMode(0644))
if err != nil {
return "", err
}
return dst, nil
}
func EnableUnitFile(file string, runtime bool) error {
conn, err := dbus.New()
if err != nil {
return err
}
files := []string{file}
_, _, err = conn.EnableUnitFiles(files, runtime, true)
return err
}
func separateNetworkUnits(units []Unit) ([]Unit, []Unit) {
networkUnits := make([]Unit, 0)
nonNetworkUnits := make([]Unit, 0)
for _, unit := range units {
if unit.Group() == "network" {
networkUnits = append(networkUnits, unit)
} else {
nonNetworkUnits = append(nonNetworkUnits, unit)
}
}
return networkUnits, nonNetworkUnits
}
func StartUnits(units []Unit) error {
networkUnits, nonNetworkUnits := separateNetworkUnits(units)
if len(networkUnits) > 0 {
if err := RestartUnitByName("systemd-networkd.service"); err != nil {
return err
}
}
for _, unit := range nonNetworkUnits {
if err := RestartUnitByName(unit.Name); err != nil {
return err
}
}
return nil
}
func DaemonReload() error {
conn, err := dbus.New()
if err != nil {
return err
}
_, err = conn.Reload()
return err
}
func RestartUnitByName(name string) error {
log.Printf("Restarting unit %s", name)
conn, err := dbus.New()
if err != nil {
return err
}
_, err = conn.RestartUnit(name, "replace")
return err
}
func StartUnitByName(name string) error {
conn, err := dbus.New() conn, err := dbus.New()
if err != nil { if err != nil {
return err return err

102
cloudinit/systemd_test.go Normal file
View File

@ -0,0 +1,102 @@
package cloudinit
import (
"io/ioutil"
"os"
"path"
"syscall"
"testing"
)
func TestPlaceNetworkUnit(t *testing.T) {
u := Unit{
Name: "50-eth0.network",
Runtime: true,
Content: `[Match]
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 syscall.Rmdir(dir)
if _, err := PlaceUnit(dir, &u); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
fullPath := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `[Match]
Name=eth47
[Network]
Address=10.209.171.177/19
`
if string(contents) != expect {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expect)
}
}
func TestPlaceMountUnit(t *testing.T) {
u := Unit{
Name: "media-state.mount",
Runtime: false,
Content: `[Mount]
What=/dev/sdb1
Where=/media/state
`,
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
if _, err := PlaceUnit(dir, &u); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
fullPath := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `[Mount]
What=/dev/sdb1
Where=/media/state
`
if string(contents) != expect {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expect)
}
}