Merge pull request #7 from bcwaldon/units
feat(unit): Provide units on boot
This commit is contained in:
		
							
								
								
									
										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-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 | ||||
|  | ||||
| @@ -40,8 +50,7 @@ echo 'Hello, world!' | ||||
|  | ||||
| ## 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 | ||||
|  | ||||
| @@ -50,8 +59,29 @@ coreos: | ||||
| 		discovery_url: https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877 | ||||
|     fleet: | ||||
|         autostart: yes | ||||
|  | ||||
| ssh_authorized_keys: | ||||
|   - 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 { | ||||
| 		Etcd  struct{ Discovery_URL string } | ||||
| 		Fleet struct{ Autostart bool } | ||||
| 		Units []Unit | ||||
| 	} | ||||
| 	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 { | ||||
| 		err := StartUnit("fleet.service") | ||||
| 		err := StartUnitByName("fleet.service") | ||||
| 		if err == nil { | ||||
| 			log.Printf("Started fleet service.") | ||||
| 		} else { | ||||
|   | ||||
| @@ -37,6 +37,19 @@ coreos: | ||||
|     discovery_url: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" | ||||
|   fleet: | ||||
|     autostart: Yes | ||||
|   units: | ||||
|     - name: 50-eth0.network | ||||
|       runtime: yes | ||||
|       content: '[Match] | ||||
|   | ||||
|     Name=eth47 | ||||
|   | ||||
|   | ||||
|     [Network] | ||||
|   | ||||
|     Address=10.209.171.177/19 | ||||
|   | ||||
| ' | ||||
| ssh_authorized_keys: | ||||
|   - foobar | ||||
|   - foobaz | ||||
| @@ -90,6 +103,31 @@ write_files: | ||||
| 			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 | ||||
|   | ||||
| @@ -2,15 +2,124 @@ package cloudinit | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"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 | ||||
|  | ||||
| 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() | ||||
| 	if err != nil { | ||||
| 		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) | ||||
| 	} | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user