Merge pull request #268 from crawford/dropins
drop-in: add support for drop-ins
This commit is contained in:
		| @@ -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. | ||||
|  | ||||
| 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` | ||||
| - `ssh_authorized_keys` | ||||
| @@ -46,13 +46,13 @@ If the platform environment supports the templating feature of coreos-cloudinit | ||||
| #cloud-config | ||||
|  | ||||
| coreos: | ||||
|     etcd: | ||||
|         name: node001 | ||||
| 	# generate a new token for each unique cluster from https://discovery.etcd.io/new | ||||
|         discovery: https://discovery.etcd.io/<token> | ||||
| 	# multi-region and multi-cloud deployments need to use $public_ipv4 | ||||
|         addr: $public_ipv4:4001 | ||||
|         peer-addr: $private_ipv4:7001 | ||||
|   etcd: | ||||
|       name: node001 | ||||
|       # generate a new token for each unique cluster from https://discovery.etcd.io/new | ||||
|       discovery: https://discovery.etcd.io/<token> | ||||
|       # multi-region and multi-cloud deployments need to use $public_ipv4 | ||||
|       addr: $public_ipv4:4001 | ||||
|       peer-addr: $private_ipv4:7001 | ||||
| ``` | ||||
|  | ||||
| ...will generate a systemd unit drop-in like this: | ||||
| @@ -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]. | ||||
| 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._ | ||||
|  | ||||
| @@ -80,9 +79,9 @@ The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allo | ||||
| #cloud-config | ||||
|  | ||||
| coreos: | ||||
|     fleet: | ||||
|         public-ip: $public_ipv4 | ||||
|         metadata: region=us-west | ||||
|   fleet: | ||||
|       public-ip: $public_ipv4 | ||||
|       metadata: region=us-west | ||||
| ``` | ||||
|  | ||||
| ...will generate a systemd unit drop-in like this: | ||||
| @@ -105,8 +104,8 @@ The `coreos.flannel.*` parameters also work very similarly to `coreos.etcd.*` an | ||||
| #cloud-config | ||||
|  | ||||
| coreos: | ||||
|     flannel: | ||||
|         etcd-prefix: /coreos.com/network2 | ||||
|   flannel: | ||||
|       etcd-prefix: /coreos.com/network2 | ||||
| ``` | ||||
|  | ||||
| ...will generate systemd unit drop-in like so: | ||||
| @@ -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. | ||||
| - **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. | ||||
| - **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. | ||||
|  | ||||
| @@ -169,19 +172,34 @@ Write a unit to disk, automatically starting it. | ||||
| #cloud-config | ||||
|  | ||||
| coreos: | ||||
|     units: | ||||
|       - name: docker-redis.service | ||||
|         command: start | ||||
|         content: | | ||||
|           [Unit] | ||||
|           Description=Redis container | ||||
|           Author=Me | ||||
|           After=docker.service | ||||
|   units: | ||||
|     - name: docker-redis.service | ||||
|       command: start | ||||
|       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 | ||||
|         [Service] | ||||
|         Restart=always | ||||
|         ExecStart=/usr/bin/docker start -a 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: | ||||
| @@ -190,11 +208,11 @@ Start the built-in `etcd` and `fleet` services: | ||||
| #cloud-config | ||||
|  | ||||
| coreos: | ||||
|     units: | ||||
|       - name: etcd.service | ||||
|         command: start | ||||
|       - name: fleet.service | ||||
|         command: start | ||||
|   units: | ||||
|     - name: etcd.service | ||||
|       command: start | ||||
|     - name: fleet.service | ||||
|       command: start | ||||
| ``` | ||||
|  | ||||
| ### ssh_authorized_keys | ||||
|   | ||||
| @@ -261,9 +261,6 @@ Address=10.209.171.177/19 | ||||
| 		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()) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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) { | ||||
| 	contents := ` | ||||
| users: | ||||
|   | ||||
| @@ -16,35 +16,17 @@ | ||||
|  | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type Unit struct { | ||||
| 	Name    string       `yaml:"name"` | ||||
| 	Mask    bool         `yaml:"mask"` | ||||
| 	Enable  bool         `yaml:"enable"` | ||||
| 	Runtime bool         `yaml:"runtime"` | ||||
| 	Content string       `yaml:"content"` | ||||
| 	Command string       `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"` | ||||
| 	DropIns []UnitDropIn `yaml:"drop_ins"` | ||||
| } | ||||
|  | ||||
| type UnitDropIn struct { | ||||
| 	Name    string `yaml:"name"` | ||||
| 	Mask    bool   `yaml:"mask"` | ||||
| 	Enable  bool   `yaml:"enable"` | ||||
| 	Runtime bool   `yaml:"runtime"` | ||||
| 	Content string `yaml:"content"` | ||||
| 	Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"` | ||||
|  | ||||
| 	// 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 { | ||||
| 	ext := filepath.Ext(u.Name) | ||||
| 	return strings.TrimLeft(ext, ".") | ||||
| } | ||||
|  | ||||
| func (u *Unit) Group() string { | ||||
| 	switch u.Type() { | ||||
| 	case "network", "netdev", "link": | ||||
| 		return "network" | ||||
| 	default: | ||||
| 		return "system" | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -195,66 +195,82 @@ func Apply(cfg config.CloudConfig, env *Environment) error { | ||||
| // commands against units. It returns any error encountered. | ||||
| func processUnits(units []system.Unit, root string, um system.UnitManager) error { | ||||
| 	type action struct { | ||||
| 		unit    string | ||||
| 		unit    system.Unit | ||||
| 		command string | ||||
| 	} | ||||
| 	actions := make([]action, 0, len(units)) | ||||
| 	reload := false | ||||
| 	for _, unit := range units { | ||||
| 		dst := unit.Destination(root) | ||||
| 		if unit.Name == "" { | ||||
| 			log.Printf("Skipping unit without name") | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if unit.Content != "" { | ||||
| 			log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) | ||||
| 			if err := um.PlaceUnit(&unit, dst); err != nil { | ||||
| 			log.Printf("Writing unit %q to filesystem", unit.Name) | ||||
| 			if err := um.PlaceUnit(unit); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			log.Printf("Placed unit %s at %s", unit.Name, dst) | ||||
| 			log.Printf("Wrote unit %q", unit.Name) | ||||
| 			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 { | ||||
| 			log.Printf("Masking unit file %s", unit.Name) | ||||
| 			if err := um.MaskUnit(&unit); err != nil { | ||||
| 			log.Printf("Masking unit file %q", unit.Name) | ||||
| 			if err := um.MaskUnit(unit); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} else if unit.Runtime { | ||||
| 			log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name) | ||||
| 			if err := um.UnmaskUnit(&unit); err != nil { | ||||
| 			log.Printf("Ensuring runtime unit file %q is unmasked", unit.Name) | ||||
| 			if err := um.UnmaskUnit(unit); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if unit.Enable { | ||||
| 			if unit.Group() != "network" { | ||||
| 				log.Printf("Enabling unit file %s", unit.Name) | ||||
| 				if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil { | ||||
| 				log.Printf("Enabling unit file %q", unit.Name) | ||||
| 				if err := um.EnableUnitFile(unit); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 				log.Printf("Enabled unit %s", unit.Name) | ||||
| 				log.Printf("Enabled unit %q", unit.Name) | ||||
| 			} 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" { | ||||
| 			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 != "" { | ||||
| 			actions = append(actions, action{unit.Name, unit.Command}) | ||||
| 			actions = append(actions, action{unit, unit.Command}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if reload { | ||||
| 		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 { | ||||
| 		log.Printf("Calling unit command '%s %s'", action.command, action.unit) | ||||
| 		res, err := um.RunUnitCommand(action.command, action.unit) | ||||
| 		log.Printf("Calling unit command %q on %q'", action.command, action.unit.Name) | ||||
| 		res, err := um.RunUnitCommand(action.unit, action.command) | ||||
| 		if err != nil { | ||||
| 			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 | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
| package initialize | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| @@ -32,99 +33,171 @@ type TestUnitManager struct { | ||||
| 	reload   bool | ||||
| } | ||||
|  | ||||
| func (tum *TestUnitManager) PlaceUnit(unit *system.Unit, dst string) error { | ||||
| 	tum.placed = append(tum.placed, unit.Name) | ||||
| func (tum *TestUnitManager) PlaceUnit(u system.Unit) error { | ||||
| 	tum.placed = append(tum.placed, u.Name) | ||||
| 	return nil | ||||
| } | ||||
| func (tum *TestUnitManager) EnableUnitFile(unit string, runtime bool) error { | ||||
| 	tum.enabled = append(tum.enabled, unit) | ||||
| func (tum *TestUnitManager) PlaceUnitDropIn(u system.Unit, d config.UnitDropIn) error { | ||||
| 	tum.placed = append(tum.placed, u.Name+".d/"+d.Name) | ||||
| 	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[unit] = command | ||||
| 	tum.commands[u.Name] = c | ||||
| 	return "", nil | ||||
| } | ||||
| func (tum *TestUnitManager) DaemonReload() error { | ||||
| 	tum.reload = true | ||||
| 	return nil | ||||
| } | ||||
| func (tum *TestUnitManager) MaskUnit(unit *system.Unit) error { | ||||
| 	tum.masked = append(tum.masked, unit.Name) | ||||
| func (tum *TestUnitManager) MaskUnit(u system.Unit) error { | ||||
| 	tum.masked = append(tum.masked, u.Name) | ||||
| 	return nil | ||||
| } | ||||
| func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error { | ||||
| 	tum.unmasked = append(tum.unmasked, unit.Name) | ||||
| func (tum *TestUnitManager) UnmaskUnit(u system.Unit) error { | ||||
| 	tum.unmasked = append(tum.unmasked, u.Name) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func TestProcessUnits(t *testing.T) { | ||||
| 	tum := &TestUnitManager{} | ||||
| 	units := []system.Unit{ | ||||
| 		system.Unit{Unit: config.Unit{ | ||||
| 			Name: "foo", | ||||
| 			Mask: true, | ||||
| 		}}, | ||||
| 	} | ||||
| 	if err := processUnits(units, "", tum); err != nil { | ||||
| 		t.Fatalf("unexpected error calling processUnits: %v", err) | ||||
| 	} | ||||
| 	if len(tum.masked) != 1 || tum.masked[0] != "foo" { | ||||
| 		t.Errorf("expected foo to be masked, but found %v", tum.masked) | ||||
| 	tests := []struct { | ||||
| 		units []system.Unit | ||||
|  | ||||
| 		result TestUnitManager | ||||
| 	}{ | ||||
| 		{ | ||||
| 			units: []system.Unit{ | ||||
| 				system.Unit{Unit: config.Unit{ | ||||
| 					Name: "foo", | ||||
| 					Mask: true, | ||||
| 				}}, | ||||
| 			}, | ||||
| 			result: TestUnitManager{ | ||||
| 				masked: []string{"foo"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			units: []system.Unit{ | ||||
| 				system.Unit{Unit: config.Unit{ | ||||
| 					Name: "bar.network", | ||||
| 				}}, | ||||
| 			}, | ||||
| 			result: TestUnitManager{ | ||||
| 				commands: map[string]string{ | ||||
| 					"systemd-networkd.service": "restart", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			units: []system.Unit{ | ||||
| 				system.Unit{Unit: config.Unit{ | ||||
| 					Name:    "baz.service", | ||||
| 					Content: "[Service]\nExecStart=/bin/true", | ||||
| 				}}, | ||||
| 			}, | ||||
| 			result: TestUnitManager{ | ||||
| 				placed: []string{"baz.service"}, | ||||
| 				reload: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			units: []system.Unit{ | ||||
| 				system.Unit{Unit: config.Unit{ | ||||
| 					Name:    "locksmithd.service", | ||||
| 					Runtime: true, | ||||
| 				}}, | ||||
| 			}, | ||||
| 			result: TestUnitManager{ | ||||
| 				unmasked: []string{"locksmithd.service"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			units: []system.Unit{ | ||||
| 				system.Unit{Unit: config.Unit{ | ||||
| 					Name:   "woof", | ||||
| 					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{}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	tum = &TestUnitManager{} | ||||
| 	units = []system.Unit{ | ||||
| 		system.Unit{Unit: config.Unit{ | ||||
| 			Name: "bar.network", | ||||
| 		}}, | ||||
| 	} | ||||
| 	if err := processUnits(units, "", tum); err != nil { | ||||
| 		t.Fatalf("unexpected error calling processUnits: %v", err) | ||||
| 	} | ||||
| 	if _, ok := tum.commands["systemd-networkd.service"]; !ok { | ||||
| 		t.Errorf("expected systemd-networkd.service to be reloaded!") | ||||
| 	} | ||||
|  | ||||
| 	tum = &TestUnitManager{} | ||||
| 	units = []system.Unit{ | ||||
| 		system.Unit{Unit: config.Unit{ | ||||
| 			Name:    "baz.service", | ||||
| 			Content: "[Service]\nExecStart=/bin/true", | ||||
| 		}}, | ||||
| 	} | ||||
| 	if err := processUnits(units, "", tum); err != nil { | ||||
| 		t.Fatalf("unexpected error calling processUnits: %v", err) | ||||
| 	} | ||||
| 	if len(tum.placed) != 1 || tum.placed[0] != "baz.service" { | ||||
| 		t.Fatalf("expected baz.service to be written, but got %v", tum.placed) | ||||
| 	} | ||||
|  | ||||
| 	tum = &TestUnitManager{} | ||||
| 	units = []system.Unit{ | ||||
| 		system.Unit{Unit: config.Unit{ | ||||
| 			Name:    "locksmithd.service", | ||||
| 			Runtime: true, | ||||
| 		}}, | ||||
| 	} | ||||
| 	if err := processUnits(units, "", tum); err != nil { | ||||
| 		t.Fatalf("unexpected error calling processUnits: %v", err) | ||||
| 	} | ||||
| 	if len(tum.unmasked) != 1 || tum.unmasked[0] != "locksmithd.service" { | ||||
| 		t.Fatalf("expected locksmithd.service to be unmasked, but got %v", tum.unmasked) | ||||
| 	} | ||||
|  | ||||
| 	tum = &TestUnitManager{} | ||||
| 	units = []system.Unit{ | ||||
| 		system.Unit{Unit: config.Unit{ | ||||
| 			Name:   "woof", | ||||
| 			Enable: true, | ||||
| 		}}, | ||||
| 	} | ||||
| 	if err := processUnits(units, "", tum); err != nil { | ||||
| 		t.Fatalf("unexpected error calling processUnits: %v", err) | ||||
| 	} | ||||
| 	if len(tum.enabled) != 1 || tum.enabled[0] != "woof" { | ||||
| 		t.Fatalf("expected woof to be enabled, but got %v", tum.enabled) | ||||
| 	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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ import ( | ||||
|  | ||||
| // dropinContents generates the contents for a drop-in unit given the config. | ||||
| // The argument must be a struct from the 'config' package. | ||||
| func dropinContents(e interface{}) string { | ||||
| func serviceContents(e interface{}) string { | ||||
| 	et := reflect.TypeOf(e) | ||||
| 	ev := reflect.ValueOf(e) | ||||
|  | ||||
| @@ -42,16 +42,3 @@ func dropinContents(e interface{}) string { | ||||
| 	} | ||||
| 	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, | ||||
| 	}}} | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import ( | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestDropinContents(t *testing.T) { | ||||
| func TestServiceContents(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		Config   interface{} | ||||
| 		Contents string | ||||
| @@ -48,7 +48,7 @@ Environment="D=0.1" | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -28,5 +28,12 @@ type Etcd struct { | ||||
|  | ||||
| // Units creates a Unit file drop-in for etcd, using any configured options. | ||||
| 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), | ||||
| 		}}, | ||||
| 	}}} | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,11 @@ func TestEtcdUnits(t *testing.T) { | ||||
| 	}{ | ||||
| 		{ | ||||
| 			config.Etcd{}, | ||||
| 			nil, | ||||
| 			[]Unit{{config.Unit{ | ||||
| 				Name:    "etcd.service", | ||||
| 				Runtime: true, | ||||
| 				DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}}, | ||||
| 			}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config.Etcd{ | ||||
| @@ -40,11 +44,13 @@ func TestEtcdUnits(t *testing.T) { | ||||
| 			[]Unit{{config.Unit{ | ||||
| 				Name:    "etcd.service", | ||||
| 				Runtime: true, | ||||
| 				DropIn:  true, | ||||
| 				Content: `[Service] | ||||
| 				DropIns: []config.UnitDropIn{{ | ||||
| 					Name: "20-cloudinit.conf", | ||||
| 					Content: `[Service] | ||||
| Environment="ETCD_DISCOVERY=http://disco.example.com/foobar" | ||||
| 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{ | ||||
| 				Name:    "etcd.service", | ||||
| 				Runtime: true, | ||||
| 				DropIn:  true, | ||||
| 				Content: `[Service] | ||||
| 				DropIns: []config.UnitDropIn{{ | ||||
| 					Name: "20-cloudinit.conf", | ||||
| 					Content: `[Service] | ||||
| Environment="ETCD_DISCOVERY=http://disco.example.com/foobar" | ||||
| Environment="ETCD_NAME=node001" | ||||
| Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" | ||||
| `, | ||||
| 				}}, | ||||
| 			}}}, | ||||
| 		}, | ||||
| 	} { | ||||
|   | ||||
| @@ -19,6 +19,7 @@ package system | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path" | ||||
| @@ -47,13 +48,14 @@ func (f *File) Permissions() (os.FileMode, 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 != "" { | ||||
| 		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 { | ||||
| 		return "", err | ||||
| 	} | ||||
| @@ -94,6 +96,7 @@ func WriteFile(f *File, root string) (string, error) { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	log.Printf("Wrote file to %q", fullpath) | ||||
| 	return fullpath, nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -13,5 +13,12 @@ type Flannel struct { | ||||
| // Units generates a Unit file drop-in for flannel, if any flannel options were | ||||
| // configured in cloud-config | ||||
| 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), | ||||
| 		}}, | ||||
| 	}}} | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,11 @@ func TestFlannelUnits(t *testing.T) { | ||||
| 	}{ | ||||
| 		{ | ||||
| 			config.Flannel{}, | ||||
| 			nil, | ||||
| 			[]Unit{{config.Unit{ | ||||
| 				Name:    "flanneld.service", | ||||
| 				Runtime: true, | ||||
| 				DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}}, | ||||
| 			}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config.Flannel{ | ||||
| @@ -22,13 +26,15 @@ func TestFlannelUnits(t *testing.T) { | ||||
| 				EtcdPrefix:   "/coreos.com/network/tenant1", | ||||
| 			}, | ||||
| 			[]Unit{{config.Unit{ | ||||
| 				Name: "flanneld.service", | ||||
| 				Content: `[Service] | ||||
| 				Name:    "flanneld.service", | ||||
| 				Runtime: true, | ||||
| 				DropIns: []config.UnitDropIn{{ | ||||
| 					Name: "20-cloudinit.conf", | ||||
| 					Content: `[Service] | ||||
| Environment="FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001" | ||||
| Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1" | ||||
| `, | ||||
| 				Runtime: true, | ||||
| 				DropIn:  true, | ||||
| 				}}, | ||||
| 			}}}, | ||||
| 		}, | ||||
| 	} { | ||||
|   | ||||
| @@ -29,5 +29,12 @@ type Fleet struct { | ||||
| // Units generates a Unit file drop-in for fleet, if any fleet options were | ||||
| // configured in cloud-config | ||||
| 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), | ||||
| 		}}, | ||||
| 	}}} | ||||
| } | ||||
|   | ||||
| @@ -30,19 +30,25 @@ func TestFleetUnits(t *testing.T) { | ||||
| 	}{ | ||||
| 		{ | ||||
| 			config.Fleet{}, | ||||
| 			nil, | ||||
| 			[]Unit{{config.Unit{ | ||||
| 				Name:    "fleet.service", | ||||
| 				Runtime: true, | ||||
| 				DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}}, | ||||
| 			}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config.Fleet{ | ||||
| 				PublicIP: "12.34.56.78", | ||||
| 			}, | ||||
| 			[]Unit{{config.Unit{ | ||||
| 				Name: "fleet.service", | ||||
| 				Content: `[Service] | ||||
| 				Name:    "fleet.service", | ||||
| 				Runtime: true, | ||||
| 				DropIns: []config.UnitDropIn{{ | ||||
| 					Name: "20-cloudinit.conf", | ||||
| 					Content: `[Service] | ||||
| Environment="FLEET_PUBLIC_IP=12.34.56.78" | ||||
| `, | ||||
| 				Runtime: true, | ||||
| 				DropIn:  true, | ||||
| 				}}, | ||||
| 			}}}, | ||||
| 		}, | ||||
| 	} { | ||||
|   | ||||
| @@ -96,7 +96,8 @@ func maybeProbeBonding(interfaces []network.InterfaceGenerator) error { | ||||
|  | ||||
| func restartNetworkd() error { | ||||
| 	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 | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,6 @@ import ( | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"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 | ||||
| const fakeMachineID = "42000000000000000000000000000042" | ||||
|  | ||||
| // PlaceUnit writes a unit file at the provided destination, creating | ||||
| // parent directories as necessary. | ||||
| func (s *systemd) PlaceUnit(u *Unit, dst string) 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 | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| // PlaceUnit writes a unit file at its desired destination, creating parent | ||||
| // directories as necessary. | ||||
| func (s *systemd) PlaceUnit(u Unit) error { | ||||
| 	file := File{config.File{ | ||||
| 		Path:               filepath.Base(dst), | ||||
| 		Path:               u.Destination(s.root), | ||||
| 		Content:            u.Content, | ||||
| 		RawFilePermissions: "0644", | ||||
| 	}} | ||||
|  | ||||
| 	_, err := WriteFile(&file, dir) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| 	_, err := WriteFile(&file, "/") | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| 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() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	units := []string{unit} | ||||
| 	_, _, err = conn.EnableUnitFiles(units, runtime, true) | ||||
| 	units := []string{u.Name} | ||||
| 	_, _, err = conn.EnableUnitFiles(units, u.Runtime, true) | ||||
| 	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() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	var fn func(string, string) (string, error) | ||||
| 	switch command { | ||||
| 	switch c { | ||||
| 	case "start": | ||||
| 		fn = conn.StartUnit | ||||
| 	case "stop": | ||||
| @@ -100,10 +101,10 @@ func (s *systemd) RunUnitCommand(command, unit string) (string, error) { | ||||
| 	case "reload-or-try-restart": | ||||
| 		fn = conn.ReloadOrTryRestartUnit | ||||
| 	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 { | ||||
| @@ -119,8 +120,8 @@ func (s *systemd) DaemonReload() error { | ||||
| // /dev/null, analogous to `systemctl mask`. | ||||
| // N.B.: Unlike `systemctl mask`, this function will *remove any existing unit | ||||
| // file at the location*, to ensure that the mask will succeed. | ||||
| func (s *systemd) MaskUnit(unit *Unit) error { | ||||
| 	masked := unit.Destination(s.root) | ||||
| func (s *systemd) MaskUnit(u Unit) error { | ||||
| 	masked := u.Destination(s.root) | ||||
| 	if _, err := os.Stat(masked); os.IsNotExist(err) { | ||||
| 		if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil { | ||||
| 			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 | ||||
| // associated with the given Unit is empty or appears to be a symlink to | ||||
| // /dev/null, it is removed. | ||||
| func (s *systemd) UnmaskUnit(unit *Unit) error { | ||||
| 	masked := unit.Destination(s.root) | ||||
| func (s *systemd) UnmaskUnit(u Unit) error { | ||||
| 	masked := u.Destination(s.root) | ||||
| 	ne, err := nullOrEmpty(masked) | ||||
| 	if os.IsNotExist(err) { | ||||
| 		return nil | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
| package system | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path" | ||||
| @@ -25,133 +26,109 @@ import ( | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| ) | ||||
|  | ||||
| func TestPlaceNetworkUnit(t *testing.T) { | ||||
| 	u := Unit{config.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 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) | ||||
| func TestPlaceUnit(t *testing.T) { | ||||
| 	tests := []config.Unit{ | ||||
| 		{ | ||||
| 			Name:    "50-eth0.network", | ||||
| 			Runtime: true, | ||||
| 			Content: "[Match]\nName=eth47\n\n[Network]\nAddress=10.209.171.177/19\n", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:    "media-state.mount", | ||||
| 			Content: "[Mount]\nWhat=/dev/sdb1\nWhere=/media/state\n", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	if err := sd.PlaceUnit(&u, dst); err != nil { | ||||
| 		t.Fatalf("PlaceUnit failed: %v", err) | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") | ||||
| 		if err != nil { | ||||
| 			panic(fmt.Sprintf("Unable to create tempdir: %v", err)) | ||||
| 		} | ||||
|  | ||||
| 	fi, err := os.Stat(dst) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Unable to stat file: %v", err) | ||||
| 	} | ||||
| 		u := Unit{tt} | ||||
| 		sd := &systemd{dir} | ||||
|  | ||||
| 	if fi.Mode() != os.FileMode(0644) { | ||||
| 		t.Errorf("File has incorrect mode: %v", fi.Mode()) | ||||
| 	} | ||||
| 		if err := sd.PlaceUnit(u); err != nil { | ||||
| 			t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err) | ||||
| 		} | ||||
|  | ||||
| 	contents, err := ioutil.ReadFile(dst) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Unable to read expected file: %v", err) | ||||
| 	} | ||||
| 		fi, err := os.Stat(u.Destination(dir)) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err) | ||||
| 		} | ||||
|  | ||||
| 	expectContents := `[Match] | ||||
| Name=eth47 | ||||
| 		if mode := fi.Mode(); mode != os.FileMode(0644) { | ||||
| 			t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode) | ||||
| 		} | ||||
|  | ||||
| [Network] | ||||
| Address=10.209.171.177/19 | ||||
| ` | ||||
| 	if string(contents) != expectContents { | ||||
| 		t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents) | ||||
| 		c, err := ioutil.ReadFile(u.Destination(dir)) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("ReadFile(): bad error (%+v): want nil, got %s", tt, err) | ||||
| 		} | ||||
|  | ||||
| 		if string(c) != tt.Content { | ||||
| 			t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Content, string(c)) | ||||
| 		} | ||||
|  | ||||
| 		os.RemoveAll(dir) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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) | ||||
| 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", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	u.DropIn = true | ||||
| 	for _, tt := range tests { | ||||
| 		dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") | ||||
| 		if err != nil { | ||||
| 			panic(fmt.Sprintf("Unable to create tempdir: %v", err)) | ||||
| 		} | ||||
|  | ||||
| 	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) | ||||
| 	} | ||||
| } | ||||
| 		u := Unit{tt} | ||||
| 		sd := &systemd{dir} | ||||
|  | ||||
| func TestPlaceMountUnit(t *testing.T) { | ||||
| 	u := Unit{config.Unit{ | ||||
| 		Name:    "media-state.mount", | ||||
| 		Runtime: false, | ||||
| 		Content: `[Mount] | ||||
| What=/dev/sdb1 | ||||
| Where=/media/state | ||||
| `, | ||||
| 	}} | ||||
| 		if err := sd.PlaceUnitDropIn(u, u.DropIns[0]); err != nil { | ||||
| 			t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err) | ||||
| 		} | ||||
|  | ||||
| 	dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Unable to create tempdir: %v", err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(dir) | ||||
| 		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) | ||||
| 		} | ||||
|  | ||||
| 	sd := &systemd{dir} | ||||
| 		if mode := fi.Mode(); mode != os.FileMode(0644) { | ||||
| 			t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode) | ||||
| 		} | ||||
|  | ||||
| 	dst := u.Destination(dir) | ||||
| 	expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount") | ||||
| 	if dst != expectDst { | ||||
| 		t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst) | ||||
| 	} | ||||
| 		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 err := sd.PlaceUnit(&u, dst); err != nil { | ||||
| 		t.Fatalf("PlaceUnit failed: %v", err) | ||||
| 	} | ||||
| 		if string(c) != u.DropIns[0].Content { | ||||
| 			t.Errorf("bad contents (%+v): want %q, got %q", tt, u.DropIns[0].Content, string(c)) | ||||
| 		} | ||||
|  | ||||
| 	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 := `[Mount] | ||||
| What=/dev/sdb1 | ||||
| Where=/media/state | ||||
| ` | ||||
| 	if string(contents) != expectContents { | ||||
| 		t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents) | ||||
| 		os.RemoveAll(dir) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -180,7 +157,7 @@ func TestMaskUnit(t *testing.T) { | ||||
| 	sd := &systemd{dir} | ||||
|  | ||||
| 	// 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 { | ||||
| 		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 | ||||
| 	ub := &Unit{config.Unit{Name: "bar.service"}} | ||||
| 	ub := Unit{config.Unit{Name: "bar.service"}} | ||||
| 	barPath := path.Join(dir, "etc", "systemd", "system", "bar.service") | ||||
| 	if _, err := os.Create(barPath); err != nil { | ||||
| 		t.Fatalf("Error creating new unit file: %v", err) | ||||
| @@ -220,12 +197,12 @@ func TestUnmaskUnit(t *testing.T) { | ||||
|  | ||||
| 	sd := &systemd{dir} | ||||
|  | ||||
| 	nilUnit := &Unit{config.Unit{Name: "null.service"}} | ||||
| 	nilUnit := Unit{config.Unit{Name: "null.service"}} | ||||
| 	if err := sd.UnmaskUnit(nilUnit); err != nil { | ||||
| 		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) | ||||
| 	if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil { | ||||
| 		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") | ||||
| 	} | ||||
|  | ||||
| 	ub := &Unit{config.Unit{Name: "bar.service"}} | ||||
| 	ub := Unit{config.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) | ||||
|   | ||||
| @@ -19,40 +19,66 @@ package system | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| ) | ||||
|  | ||||
| // Name for drop-in service configuration files created by cloudconfig | ||||
| const cloudConfigDropIn = "20-cloudinit.conf" | ||||
|  | ||||
| type UnitManager interface { | ||||
| 	PlaceUnit(unit *Unit, dst string) error | ||||
| 	EnableUnitFile(unit string, runtime bool) error | ||||
| 	RunUnitCommand(command, unit string) (string, error) | ||||
| 	PlaceUnit(unit Unit) error | ||||
| 	PlaceUnitDropIn(unit Unit, dropIn config.UnitDropIn) error | ||||
| 	EnableUnitFile(unit Unit) error | ||||
| 	RunUnitCommand(unit Unit, command string) (string, error) | ||||
| 	MaskUnit(unit Unit) error | ||||
| 	UnmaskUnit(unit Unit) error | ||||
| 	DaemonReload() error | ||||
| 	MaskUnit(unit *Unit) error | ||||
| 	UnmaskUnit(unit *Unit) error | ||||
| } | ||||
|  | ||||
| // 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 { | ||||
| 	config.Unit | ||||
| } | ||||
|  | ||||
| // 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 { | ||||
| // Type returns the extension of the unit (everything that follows the final | ||||
| // period). | ||||
| func (u Unit) Type() 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" | ||||
| 	if u.Runtime { | ||||
| 		dir = "run" | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
| 	} | ||||
| 	return path.Join(root, dir, "systemd", u.Group()) | ||||
| } | ||||
|   | ||||
							
								
								
									
										138
									
								
								system/unit_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								system/unit_test.go
									
									
									
									
									
										Normal 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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user