Merge pull request #7 from bcwaldon/units
feat(unit): Provide units on boot
This commit is contained in:
commit
8fa8495100
36
README.md
36
README.md
@ -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
|
||||||
|
```
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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
102
cloudinit/systemd_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user