Compare commits

..

17 Commits

Author SHA1 Message Date
Alex Crawford
364507fb75 coreos-cloudinit: bump to 0.9.0 2014-07-21 19:16:11 -07:00
Alex Crawford
08d4842502 Merge pull request #190 from crawford/logs
Logs
2014-07-21 12:22:41 -07:00
Alex Crawford
21e32e44f8 system: Add more logging for networkd 2014-07-21 11:25:22 -07:00
Alex Crawford
7a06dee16f system: Cleanup redundant code 2014-07-21 11:24:42 -07:00
Alex Crawford
ff9cf5743d Merge pull request #187 from crawford/order
networkd: Reverse lexicographic order of generated unit files
2014-07-18 13:23:58 -07:00
Alex Crawford
1b10a3a187 networkd: Reverse lexicographic order of generated unit files 2014-07-17 20:47:37 -07:00
Michael Marineau
10838e001d Merge pull request #186 from robszumski/add-highlighting
feat(docs): add syntax highlighting
2014-07-15 15:26:33 -07:00
Rob Szumski
96370ac5b9 feat(docs): add syntax highlighting 2014-07-14 16:16:14 -07:00
Michael Marineau
0b82cd074d Merge pull request #180 from marineam/systemd_testing
chore(*): split out unit processing from config.Apply
2014-07-11 20:09:08 -07:00
Alex Crawford
a974e85103 Merge pull request #174 from crawford/teeth
networkd: Fix issues with bonding and VLANs
2014-07-11 15:48:02 -07:00
Michael Marineau
f0450662b0 Merge pull request #183 from marineam/fix
tests: fix error messages, use Fatalf
2014-07-11 15:40:54 -07:00
Michael Marineau
03e29d1291 tests: fix error messages, use Fatalf 2014-07-11 15:38:04 -07:00
Michael Marineau
98ae5d88aa coreos-cloudinit: bump to 0.8.9+git 2014-07-11 14:40:57 -07:00
Jonathan Boulle
be51f4eba0 chore(*): split out unit processing from config.Apply 2014-07-11 10:44:19 -07:00
Alex Crawford
e3037f18a6 networkd: Restart networkd twice to work around race
https://bugs.freedesktop.org/show_bug.cgi?id=76077
2014-07-10 23:40:42 -07:00
Alex Crawford
fe388a3ab6 networkd: Create config directory before writing config 2014-07-10 23:40:42 -07:00
Alex Crawford
c820f2b1cf bonding: Add support for probing the bonding module with parameters
Until support for bonding params is added to networkd, this will be
neccessary in order to use bonding parameters (i.e. miimon, mode).
This also makes it such that the 8012q module will only be loaded if
the network config makes use of VLANs.
2014-07-10 23:40:42 -07:00
16 changed files with 495 additions and 206 deletions

View File

@@ -13,7 +13,7 @@ If no **id** field is provided, coreos-cloudinit will ignore this section.
For example, the following cloud-config document... For example, the following cloud-config document...
``` ```yaml
#cloud-config #cloud-config
coreos: coreos:
oem: oem:
@@ -26,7 +26,7 @@ coreos:
...would be rendered to the following `/etc/oem-release`: ...would be rendered to the following `/etc/oem-release`:
``` ```yaml
ID=rackspace ID=rackspace
NAME="Rackspace Cloud Servers" NAME="Rackspace Cloud Servers"
VERSION_ID=168.0.0 VERSION_ID=168.0.0

View File

@@ -42,7 +42,7 @@ CoreOS tries to conform to each platform's native method to provide user data. E
The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file. The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
We can use the templating feature of coreos-cloudinit to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document... We can use the templating feature of coreos-cloudinit to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
``` ```yaml
#cloud-config #cloud-config
coreos: coreos:
@@ -57,7 +57,7 @@ coreos:
...will generate a systemd unit drop-in like this: ...will generate a systemd unit drop-in like this:
``` ```yaml
[Service] [Service]
Environment="ETCD_NAME=node001" Environment="ETCD_NAME=node001"
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>" Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
@@ -74,7 +74,7 @@ Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allow for the configuration of fleet through environment variables. For example, the following cloud-config document... The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allow for the configuration of fleet through environment variables. For example, the following cloud-config document...
``` ```yaml
#cloud-config #cloud-config
coreos: coreos:
@@ -85,7 +85,7 @@ coreos:
...will generate a systemd unit drop-in like this: ...will generate a systemd unit drop-in like this:
``` ```yaml
[Service] [Service]
Environment="FLEET_PUBLIC_IP=203.0.113.29" Environment="FLEET_PUBLIC_IP=203.0.113.29"
Environment="FLEET_METADATA=region=us-west" Environment="FLEET_METADATA=region=us-west"
@@ -114,7 +114,7 @@ The `reboot-strategy` parameter also affects the behaviour of [locksmith](https:
##### Example ##### Example
``` ```yaml
#cloud-config #cloud-config
coreos: coreos:
update: update:
@@ -138,7 +138,7 @@ The `coreos.units.*` parameters define a list of arbitrary systemd units to star
Write a unit to disk, automatically starting it. Write a unit to disk, automatically starting it.
``` ```yaml
#cloud-config #cloud-config
coreos: coreos:
@@ -159,7 +159,7 @@ coreos:
Start the built-in `etcd` and `fleet` services: Start the built-in `etcd` and `fleet` services:
``` ```yaml
#cloud-config #cloud-config
coreos: coreos:
@@ -177,7 +177,7 @@ The `ssh_authorized_keys` parameter adds public SSH keys which will be authorize
The keys will be named "coreos-cloudinit" by default. The keys will be named "coreos-cloudinit" by default.
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`. Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
``` ```yaml
#cloud-config #cloud-config
ssh_authorized_keys: ssh_authorized_keys:
@@ -189,7 +189,7 @@ ssh_authorized_keys:
The `hostname` parameter defines the system's hostname. The `hostname` parameter defines the system's hostname.
This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`). This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
``` ```yaml
#cloud-config #cloud-config
hostname: coreos1 hostname: coreos1
@@ -222,7 +222,7 @@ The following fields are not yet implemented:
- **selinux-user**: Corresponding SELinux user - **selinux-user**: Corresponding SELinux user
- **ssh-import-id**: Import SSH keys by ID from Launchpad. - **ssh-import-id**: Import SSH keys by ID from Launchpad.
``` ```yaml
#cloud-config #cloud-config
users: users:
@@ -261,7 +261,7 @@ Using a higher number of rounds will help create more secure passwords, but give
Using the `coreos-ssh-import-github` field, we can import public SSH keys from a GitHub user to use as authorized keys to a server. Using the `coreos-ssh-import-github` field, we can import public SSH keys from a GitHub user to use as authorized keys to a server.
``` ```yaml
#cloud-config #cloud-config
users: users:
@@ -274,7 +274,7 @@ users:
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user). We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token: For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
``` ```yaml
#cloud-config #cloud-config
users: users:
@@ -284,7 +284,7 @@ users:
You can also specify any URL whose response matches the JSON format for public keys: You can also specify any URL whose response matches the JSON format for public keys:
``` ```yaml
#cloud-config #cloud-config
users: users:
@@ -304,7 +304,7 @@ The `write-file` parameter defines a list of files to create on the local filesy
Explicitly not implemented is the **encoding** attribute. Explicitly not implemented is the **encoding** attribute.
The **content** field must represent exactly what should be written to disk. The **content** field must represent exactly what should be written to disk.
``` ```yaml
#cloud-config #cloud-config
write_files: write_files:
- path: /etc/fleet/fleet.conf - path: /etc/fleet/fleet.conf
@@ -321,7 +321,7 @@ Currently, the only supported value is "localhost" which will cause your system'
to resolve to "127.0.0.1". This is helpful when the host does not have DNS to resolve to "127.0.0.1". This is helpful when the host does not have DNS
infrastructure in place to resolve its own hostname, for example, when using Vagrant. infrastructure in place to resolve its own hostname, for example, when using Vagrant.
``` ```yaml
#cloud-config #cloud-config
manage_etc_hosts: localhost manage_etc_hosts: localhost

View File

@@ -14,17 +14,21 @@ The image should be a single FAT or ISO9660 file system with the label
For example, to wrap up a config named `user_data` in a config drive image: For example, to wrap up a config named `user_data` in a config drive image:
mkdir -p /tmp/new-drive/openstack/latest ```sh
cp user_data /tmp/new-drive/openstack/latest/user_data mkdir -p /tmp/new-drive/openstack/latest
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive cp user_data /tmp/new-drive/openstack/latest/user_data
rm -r /tmp/new-drive mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
rm -r /tmp/new-drive
```
## QEMU virtfs ## QEMU virtfs
One exception to the above, when using QEMU it is possible to skip creating an One exception to the above, when using QEMU it is possible to skip creating an
image and use a plain directory containing the same contents: image and use a plain directory containing the same contents:
qemu-system-x86_64 \ ```sh
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \ qemu-system-x86_64 \
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \ -fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
[usual qemu options here...] -device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
[usual qemu options here...]
```

View File

@@ -14,7 +14,7 @@ import (
) )
const ( const (
version = "0.8.9" version = "0.9.0"
datasourceInterval = 100 * time.Millisecond datasourceInterval = 100 * time.Millisecond
datasourceMaxInterval = 30 * time.Second datasourceMaxInterval = 30 * time.Second
datasourceTimeout = 5 * time.Minute datasourceTimeout = 5 * time.Minute

View File

@@ -272,13 +272,23 @@ func Apply(cfg CloudConfig, env *Environment) error {
} }
} }
um := system.NewUnitManager(env.Root())
return processUnits(cfg.Coreos.Units, env.Root(), um)
}
// processUnits takes a set of Units and applies them to the given root using
// the given UnitManager. This can involve things like writing unit files to
// disk, masking/unmasking units, or invoking systemd
// commands against units. It returns any error encountered.
func processUnits(units []system.Unit, root string, um system.UnitManager) error {
commands := make(map[string]string, 0) commands := make(map[string]string, 0)
reload := false reload := false
for _, unit := range cfg.Coreos.Units { for _, unit := range units {
dst := unit.Destination(env.Root()) dst := unit.Destination(root)
if unit.Content != "" { if unit.Content != "" {
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
if err := system.PlaceUnit(&unit, dst); err != nil { if err := um.PlaceUnit(&unit, dst); err != nil {
return err return err
} }
log.Printf("Placed unit %s at %s", unit.Name, dst) log.Printf("Placed unit %s at %s", unit.Name, dst)
@@ -287,12 +297,12 @@ func Apply(cfg CloudConfig, env *Environment) error {
if unit.Mask { if unit.Mask {
log.Printf("Masking unit file %s", unit.Name) log.Printf("Masking unit file %s", unit.Name)
if err := system.MaskUnit(&unit, env.Root()); 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 %s is unmasked", unit.Name)
if err := system.UnmaskUnit(&unit, env.Root()); err != nil { if err := um.UnmaskUnit(&unit); err != nil {
return err return err
} }
} }
@@ -300,7 +310,7 @@ func Apply(cfg CloudConfig, env *Environment) error {
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 %s", unit.Name)
if err := system.EnableUnitFile(unit.Name, unit.Runtime); err != nil { if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
return err return err
} }
log.Printf("Enabled unit %s", unit.Name) log.Printf("Enabled unit %s", unit.Name)
@@ -317,14 +327,14 @@ func Apply(cfg CloudConfig, env *Environment) error {
} }
if reload { if reload {
if err := system.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: %v", err))
} }
} }
for unit, command := range commands { for unit, command := range commands {
log.Printf("Calling unit command '%s %s'", command, unit) log.Printf("Calling unit command '%s %s'", command, unit)
res, err := system.RunUnitCommand(command, unit) res, err := um.RunUnitCommand(command, unit)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"github.com/coreos/coreos-cloudinit/system"
) )
func TestCloudConfigUnknownKeys(t *testing.T) { func TestCloudConfigUnknownKeys(t *testing.T) {
@@ -332,3 +334,109 @@ users:
t.Errorf("Failed to parse no-log-init field") t.Errorf("Failed to parse no-log-init field")
} }
} }
type TestUnitManager struct {
placed []string
enabled []string
masked []string
unmasked []string
commands map[string]string
reload bool
}
func (tum *TestUnitManager) PlaceUnit(unit *system.Unit, dst string) error {
tum.placed = append(tum.placed, unit.Name)
return nil
}
func (tum *TestUnitManager) EnableUnitFile(unit string, runtime bool) error {
tum.enabled = append(tum.enabled, unit)
return nil
}
func (tum *TestUnitManager) RunUnitCommand(command, unit string) (string, error) {
tum.commands = make(map[string]string)
tum.commands[unit] = command
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)
return nil
}
func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error {
tum.unmasked = append(tum.unmasked, unit.Name)
return nil
}
func TestProcessUnits(t *testing.T) {
tum := &TestUnitManager{}
units := []system.Unit{
system.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)
}
tum = &TestUnitManager{}
units = []system.Unit{
system.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{
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{
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{
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)
}
}

View File

@@ -70,6 +70,8 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
sd := system.NewUnitManager(dir)
uu, err := ee.Units(dir) uu, err := ee.Units(dir)
if err != nil { if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err) t.Fatalf("Generating etcd unit failed: %v", err)
@@ -81,7 +83,7 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
dst := u.Destination(dir) dst := u.Destination(dir)
os.Stderr.WriteString("writing to " + dir + "\n") os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(&u, dst); err != nil { if err := sd.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err) t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
} }
@@ -119,6 +121,8 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
sd := system.NewUnitManager(dir)
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755)) os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444)) err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
if err != nil { if err != nil {
@@ -136,7 +140,7 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
dst := u.Destination(dir) dst := u.Destination(dir)
os.Stderr.WriteString("writing to " + dir + "\n") os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(&u, dst); err != nil { if err := sd.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err) t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
} }

View File

@@ -3,6 +3,7 @@ package network
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
) )
type InterfaceGenerator interface { type InterfaceGenerator interface {
@@ -11,6 +12,8 @@ type InterfaceGenerator interface {
Netdev() string Netdev() string
Link() string Link() string
Network() string Network() string
Type() string
ModprobeParams() string
} }
type networkInterface interface { type networkInterface interface {
@@ -68,6 +71,10 @@ func (i *logicalInterface) Children() []networkInterface {
return i.children return i.children
} }
func (i *logicalInterface) ModprobeParams() string {
return ""
}
func (i *logicalInterface) setConfigDepth(depth int) { func (i *logicalInterface) setConfigDepth(depth int) {
i.configDepth = depth i.configDepth = depth
} }
@@ -84,9 +91,14 @@ func (p *physicalInterface) Netdev() string {
return "" return ""
} }
func (p *physicalInterface) Type() string {
return "physical"
}
type bondInterface struct { type bondInterface struct {
logicalInterface logicalInterface
slaves []string slaves []string
options map[string]string
} }
func (b *bondInterface) Name() string { func (b *bondInterface) Name() string {
@@ -97,6 +109,19 @@ func (b *bondInterface) Netdev() string {
return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name) return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name)
} }
func (b *bondInterface) Type() string {
return "bond"
}
func (b *bondInterface) ModprobeParams() string {
params := ""
for name, val := range b.options {
params += fmt.Sprintf("%s=%s ", name, val)
}
params = strings.TrimSuffix(params, " ")
return params
}
type vlanInterface struct { type vlanInterface struct {
logicalInterface logicalInterface
id int id int
@@ -123,6 +148,10 @@ func (v *vlanInterface) Netdev() string {
return config return config
} }
func (v *vlanInterface) Type() string {
return "vlan"
}
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator { func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
interfaceMap := createInterfaces(stanzas) interfaceMap := createInterfaces(stanzas)
linkAncestors(interfaceMap) linkAncestors(interfaceMap)
@@ -141,15 +170,22 @@ func createInterfaces(stanzas []*stanzaInterface) map[string]networkInterface {
for _, iface := range stanzas { for _, iface := range stanzas {
switch iface.kind { switch iface.kind {
case interfaceBond: case interfaceBond:
bondOptions := make(map[string]string)
for _, k := range []string{"mode", "miimon", "lacp-rate"} {
if v, ok := iface.options["bond-"+k]; ok && len(v) > 0 {
bondOptions[k] = v[0]
}
}
interfaceMap[iface.name] = &bondInterface{ interfaceMap[iface.name] = &bondInterface{
logicalInterface{ logicalInterface{
name: iface.name, name: iface.name,
config: iface.configMethod, config: iface.configMethod,
children: []networkInterface{}, children: []networkInterface{},
}, },
iface.options["slaves"], iface.options["bond-slaves"],
bondOptions,
} }
for _, slave := range iface.options["slaves"] { for _, slave := range iface.options["bond-slaves"] {
if _, ok := interfaceMap[slave]; !ok { if _, ok := interfaceMap[slave]; !ok {
interfaceMap[slave] = &physicalInterface{ interfaceMap[slave] = &physicalInterface{
logicalInterface{ logicalInterface{
@@ -241,13 +277,17 @@ func markConfigDepths(interfaceMap map[string]networkInterface) {
} }
} }
for _, iface := range rootInterfaceMap { for _, iface := range rootInterfaceMap {
setDepth(iface, 0) setDepth(iface)
} }
} }
func setDepth(iface networkInterface, depth int) { func setDepth(iface networkInterface) int {
iface.setConfigDepth(depth) maxDepth := 0
for _, child := range iface.Children() { for _, child := range iface.Children() {
setDepth(child, depth+1) if depth := setDepth(child); depth > maxDepth {
maxDepth = depth
}
} }
iface.setConfigDepth(maxDepth)
return (maxDepth + 1)
} }

View File

@@ -36,6 +36,7 @@ func TestPhysicalInterfaceNetwork(t *testing.T) {
name: "testbond1", name: "testbond1",
}, },
nil, nil,
nil,
}, },
&vlanInterface{ &vlanInterface{
logicalInterface{ logicalInterface{
@@ -67,14 +68,14 @@ VLAN=testvlan2
} }
func TestBondInterfaceName(t *testing.T) { func TestBondInterfaceName(t *testing.T) {
b := bondInterface{logicalInterface{name: "testname"}, nil} b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
if b.Name() != "testname" { if b.Name() != "testname" {
t.FailNow() t.FailNow()
} }
} }
func TestBondInterfaceNetdev(t *testing.T) { func TestBondInterfaceNetdev(t *testing.T) {
b := bondInterface{logicalInterface{name: "testname"}, nil} b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
netdev := `[NetDev] netdev := `[NetDev]
Kind=bond Kind=bond
Name=testname Name=testname
@@ -102,6 +103,7 @@ func TestBondInterfaceNetwork(t *testing.T) {
name: "testbond1", name: "testbond1",
}, },
nil, nil,
nil,
}, },
&vlanInterface{ &vlanInterface{
logicalInterface{ logicalInterface{
@@ -120,6 +122,7 @@ func TestBondInterfaceNetwork(t *testing.T) {
}, },
}, },
nil, nil,
nil,
} }
network := `[Match] network := `[Match]
Name=testname Name=testname
@@ -218,6 +221,61 @@ Gateway=1.2.3.4
} }
} }
func TestType(t *testing.T) {
for _, tt := range []struct {
i InterfaceGenerator
t string
}{
{
i: &physicalInterface{},
t: "physical",
},
{
i: &vlanInterface{},
t: "vlan",
},
{
i: &bondInterface{},
t: "bond",
},
} {
if tp := tt.i.Type(); tp != tt.t {
t.Fatalf("bad type (%q): got %s, want %s", tt.i, tp, tt.t)
}
}
}
func TestModprobeParams(t *testing.T) {
for _, tt := range []struct {
i InterfaceGenerator
p string
}{
{
i: &physicalInterface{},
p: "",
},
{
i: &vlanInterface{},
p: "",
},
{
i: &bondInterface{
logicalInterface{},
nil,
map[string]string{
"a": "1",
"b": "2",
},
},
p: "a=1 b=2",
},
} {
if p := tt.i.ModprobeParams(); p != tt.p {
t.Fatalf("bad params (%q): got %s, want %s", tt.i, p, tt.p)
}
}
}
func TestBuildInterfacesLo(t *testing.T) { func TestBuildInterfacesLo(t *testing.T) {
stanzas := []*stanzaInterface{ stanzas := []*stanzaInterface{
&stanzaInterface{ &stanzaInterface{
@@ -242,7 +300,7 @@ func TestBuildInterfacesBlindBond(t *testing.T) {
auto: false, auto: false,
configMethod: configMethodManual{}, configMethod: configMethodManual{},
options: map[string][]string{ options: map[string][]string{
"slaves": []string{"eth0"}, "bond-slaves": []string{"eth0"},
}, },
}, },
} }
@@ -252,16 +310,17 @@ func TestBuildInterfacesBlindBond(t *testing.T) {
name: "bond0", name: "bond0",
config: configMethodManual{}, config: configMethodManual{},
children: []networkInterface{}, children: []networkInterface{},
configDepth: 1, configDepth: 0,
}, },
[]string{"eth0"}, []string{"eth0"},
map[string]string{},
} }
eth0 := &physicalInterface{ eth0 := &physicalInterface{
logicalInterface{ logicalInterface{
name: "eth0", name: "eth0",
config: configMethodManual{}, config: configMethodManual{},
children: []networkInterface{bond0}, children: []networkInterface{bond0},
configDepth: 0, configDepth: 1,
}, },
} }
expect := []InterfaceGenerator{bond0, eth0} expect := []InterfaceGenerator{bond0, eth0}
@@ -289,7 +348,7 @@ func TestBuildInterfacesBlindVLAN(t *testing.T) {
name: "vlan0", name: "vlan0",
config: configMethodManual{}, config: configMethodManual{},
children: []networkInterface{}, children: []networkInterface{},
configDepth: 1, configDepth: 0,
}, },
0, 0,
"eth0", "eth0",
@@ -299,7 +358,7 @@ func TestBuildInterfacesBlindVLAN(t *testing.T) {
name: "eth0", name: "eth0",
config: configMethodManual{}, config: configMethodManual{},
children: []networkInterface{vlan0}, children: []networkInterface{vlan0},
configDepth: 0, configDepth: 1,
}, },
} }
expect := []InterfaceGenerator{eth0, vlan0} expect := []InterfaceGenerator{eth0, vlan0}
@@ -323,7 +382,9 @@ func TestBuildInterfaces(t *testing.T) {
auto: false, auto: false,
configMethod: configMethodManual{}, configMethod: configMethodManual{},
options: map[string][]string{ options: map[string][]string{
"slaves": []string{"eth0"}, "bond-slaves": []string{"eth0"},
"bond-mode": []string{"4"},
"bond-miimon": []string{"100"},
}, },
}, },
&stanzaInterface{ &stanzaInterface{
@@ -332,7 +393,7 @@ func TestBuildInterfaces(t *testing.T) {
auto: false, auto: false,
configMethod: configMethodManual{}, configMethod: configMethodManual{},
options: map[string][]string{ options: map[string][]string{
"slaves": []string{"bond0"}, "bond-slaves": []string{"bond0"},
}, },
}, },
&stanzaInterface{ &stanzaInterface{
@@ -362,7 +423,7 @@ func TestBuildInterfaces(t *testing.T) {
name: "vlan1", name: "vlan1",
config: configMethodManual{}, config: configMethodManual{},
children: []networkInterface{}, children: []networkInterface{},
configDepth: 2, configDepth: 0,
}, },
1, 1,
"bond0", "bond0",
@@ -372,7 +433,7 @@ func TestBuildInterfaces(t *testing.T) {
name: "vlan0", name: "vlan0",
config: configMethodManual{}, config: configMethodManual{},
children: []networkInterface{}, children: []networkInterface{},
configDepth: 1, configDepth: 0,
}, },
0, 0,
"eth0", "eth0",
@@ -382,9 +443,10 @@ func TestBuildInterfaces(t *testing.T) {
name: "bond1", name: "bond1",
config: configMethodManual{}, config: configMethodManual{},
children: []networkInterface{}, children: []networkInterface{},
configDepth: 2, configDepth: 0,
}, },
[]string{"bond0"}, []string{"bond0"},
map[string]string{},
} }
bond0 := &bondInterface{ bond0 := &bondInterface{
logicalInterface{ logicalInterface{
@@ -394,13 +456,17 @@ func TestBuildInterfaces(t *testing.T) {
configDepth: 1, configDepth: 1,
}, },
[]string{"eth0"}, []string{"eth0"},
map[string]string{
"mode": "4",
"miimon": "100",
},
} }
eth0 := &physicalInterface{ eth0 := &physicalInterface{
logicalInterface{ logicalInterface{
name: "eth0", name: "eth0",
config: configMethodManual{}, config: configMethodManual{},
children: []networkInterface{bond0, vlan0}, children: []networkInterface{bond0, vlan0},
configDepth: 0, configDepth: 2,
}, },
} }
expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1} expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1}

View File

@@ -293,7 +293,6 @@ func parseHwaddress(options map[string][]string, iface string) (net.HardwareAddr
} }
func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) { func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
options["slaves"] = options["bond-slaves"]
return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
} }

View File

@@ -129,7 +129,7 @@ func TestParseBondStanzaNoSlaves(t *testing.T) {
if err != nil { if err != nil {
t.FailNow() t.FailNow()
} }
if bond.options["slaves"] != nil { if bond.options["bond-slaves"] != nil {
t.FailNow() t.FailNow()
} }
} }
@@ -152,9 +152,6 @@ func TestParseBondStanza(t *testing.T) {
if bond.configMethod != conf { if bond.configMethod != conf {
t.FailNow() t.FailNow()
} }
if !reflect.DeepEqual(bond.options["slaves"], options["bond-slaves"]) {
t.FailNow()
}
} }
func TestParsePhysicalStanza(t *testing.T) { func TestParsePhysicalStanza(t *testing.T) {

View File

@@ -44,7 +44,7 @@ func TestWriteEnvFileUpdate(t *testing.T) {
oldStat, err := os.Stat(fullPath) oldStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
ef := EnvFile{ ef := EnvFile{
@@ -70,11 +70,11 @@ func TestWriteEnvFileUpdate(t *testing.T) {
newStat, err := os.Stat(fullPath) newStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino { if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatal("File was not replaced: %s", fullPath) t.Fatalf("File was not replaced: %s", fullPath)
} }
} }
@@ -91,7 +91,7 @@ func TestWriteEnvFileUpdateNoNewline(t *testing.T) {
oldStat, err := os.Stat(fullPath) oldStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
ef := EnvFile{ ef := EnvFile{
@@ -117,11 +117,11 @@ func TestWriteEnvFileUpdateNoNewline(t *testing.T) {
newStat, err := os.Stat(fullPath) newStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino { if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatal("File was not replaced: %s", fullPath) t.Fatalf("File was not replaced: %s", fullPath)
} }
} }
@@ -170,7 +170,7 @@ func TestWriteEnvFileNoop(t *testing.T) {
oldStat, err := os.Stat(fullPath) oldStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
ef := EnvFile{ ef := EnvFile{
@@ -196,11 +196,11 @@ func TestWriteEnvFileNoop(t *testing.T) {
newStat, err := os.Stat(fullPath) newStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino { if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatal("File was replaced: %s", fullPath) t.Fatalf("File was replaced: %s", fullPath)
} }
} }
@@ -217,7 +217,7 @@ func TestWriteEnvFileUpdateDos(t *testing.T) {
oldStat, err := os.Stat(fullPath) oldStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
ef := EnvFile{ ef := EnvFile{
@@ -243,11 +243,11 @@ func TestWriteEnvFileUpdateDos(t *testing.T) {
newStat, err := os.Stat(fullPath) newStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino { if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatal("File was not replaced: %s", fullPath) t.Fatalf("File was not replaced: %s", fullPath)
} }
} }
@@ -266,7 +266,7 @@ func TestWriteEnvFileDos2Unix(t *testing.T) {
oldStat, err := os.Stat(fullPath) oldStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
ef := EnvFile{ ef := EnvFile{
@@ -292,11 +292,11 @@ func TestWriteEnvFileDos2Unix(t *testing.T) {
newStat, err := os.Stat(fullPath) newStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino { if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatal("File was not replaced: %s", fullPath) t.Fatalf("File was not replaced: %s", fullPath)
} }
} }
@@ -314,7 +314,7 @@ func TestWriteEnvFileEmpty(t *testing.T) {
oldStat, err := os.Stat(fullPath) oldStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
ef := EnvFile{ ef := EnvFile{
@@ -340,11 +340,11 @@ func TestWriteEnvFileEmpty(t *testing.T) {
newStat, err := os.Stat(fullPath) newStat, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatal("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino { if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatal("File was replaced: %s", fullPath) t.Fatalf("File was replaced: %s", fullPath)
} }
} }

View File

@@ -2,10 +2,11 @@ package system
import ( import (
"fmt" "fmt"
"io/ioutil" "log"
"net" "net"
"os/exec" "os/exec"
"path" "strings"
"time"
"github.com/coreos/coreos-cloudinit/network" "github.com/coreos/coreos-cloudinit/network"
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink" "github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
@@ -17,6 +18,13 @@ const (
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) { func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
defer func() { defer func() {
if e := restartNetworkd(); e != nil {
err = e
return
}
// TODO(crawford): Get rid of this once networkd fixes the race
// https://bugs.freedesktop.org/show_bug.cgi?id=76077
time.Sleep(5 * time.Second)
if e := restartNetworkd(); e != nil { if e := restartNetworkd(); e != nil {
err = e err = e
} }
@@ -26,19 +34,18 @@ func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
return return
} }
if err = probe8012q(); err != nil { if err = maybeProbe8012q(interfaces); err != nil {
return return
} }
return return maybeProbeBonding(interfaces)
} }
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error { func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
sysInterfaceMap := make(map[string]*net.Interface) sysInterfaceMap := make(map[string]*net.Interface)
if systemInterfaces, err := net.Interfaces(); err == nil { if systemInterfaces, err := net.Interfaces(); err == nil {
for _, iface := range systemInterfaces { for _, iface := range systemInterfaces {
// Need a copy of the interface so we can take the address iface := iface
temp := iface sysInterfaceMap[iface.Name] = &iface
sysInterfaceMap[temp.Name] = &temp
} }
} else { } else {
return err return err
@@ -46,6 +53,7 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
for _, iface := range interfaces { for _, iface := range interfaces {
if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok { if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok {
log.Printf("Taking down interface %q\n", systemInterface.Name)
if err := netlink.NetworkLinkDown(systemInterface); err != nil { if err := netlink.NetworkLinkDown(systemInterface); err != nil {
fmt.Printf("Error while downing interface %q (%s). Continuing...\n", systemInterface.Name, err) fmt.Printf("Error while downing interface %q (%s). Continuing...\n", systemInterface.Name, err)
} }
@@ -55,26 +63,45 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
return nil return nil
} }
func probe8012q() error { func maybeProbe8012q(interfaces []network.InterfaceGenerator) error {
return exec.Command("modprobe", "8021q").Run() for _, iface := range interfaces {
if iface.Type() == "vlan" {
log.Printf("Probing LKM %q (%q)\n", "8021q", "8021q")
return exec.Command("modprobe", "8021q").Run()
}
}
return nil
}
func maybeProbeBonding(interfaces []network.InterfaceGenerator) error {
args := []string{"bonding"}
for _, iface := range interfaces {
if iface.Type() == "bond" {
args = append(args, strings.Split(iface.ModprobeParams(), " ")...)
break
}
}
log.Printf("Probing LKM %q (%q)\n", "bonding", args)
return exec.Command("modprobe", args...).Run()
} }
func restartNetworkd() error { func restartNetworkd() error {
_, err := RunUnitCommand("restart", "systemd-networkd.service") log.Printf("Restarting networkd.service\n")
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service")
return err return err
} }
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error { func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
for _, iface := range interfaces { for _, iface := range interfaces {
filename := path.Join(runtimeNetworkPath, fmt.Sprintf("%s.netdev", iface.Filename())) filename := fmt.Sprintf("%s.netdev", iface.Filename())
if err := writeConfig(filename, iface.Netdev()); err != nil { if err := writeConfig(filename, iface.Netdev()); err != nil {
return err return err
} }
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.link", iface.Filename())) filename = fmt.Sprintf("%s.link", iface.Filename())
if err := writeConfig(filename, iface.Link()); err != nil { if err := writeConfig(filename, iface.Link()); err != nil {
return err return err
} }
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.network", iface.Filename())) filename = fmt.Sprintf("%s.network", iface.Filename())
if err := writeConfig(filename, iface.Network()); err != nil { if err := writeConfig(filename, iface.Network()); err != nil {
return err return err
} }
@@ -86,6 +113,7 @@ func writeConfig(filename string, config string) error {
if config == "" { if config == "" {
return nil return nil
} }
log.Printf("Writing networkd unit %q\n", filename)
return ioutil.WriteFile(filename, []byte(config), 0444) _, err := WriteFile(&File{Content: config, Path: filename}, runtimeNetworkPath)
return err
} }

View File

@@ -13,63 +13,21 @@ import (
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus" "github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
) )
func NewUnitManager(root string) UnitManager {
return &systemd{root}
}
type systemd struct {
root string
}
// fakeMachineID is placed on non-usr CoreOS images and should // fakeMachineID is placed on non-usr CoreOS images and should
// never be used as a true MachineID // never be used as a true MachineID
const fakeMachineID = "42000000000000000000000000000042" const fakeMachineID = "42000000000000000000000000000042"
// Name for drop-in service configuration files created by cloudconfig
const cloudConfigDropIn = "20-cloudinit.conf"
type Unit struct {
Name string
Mask bool
Enable bool
Runtime bool
Content string
Command string
// 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() (group string) {
t := u.Type()
if t == "network" || t == "netdev" || t == "link" {
group = "network"
} else {
group = "system"
}
return
}
type Script []byte
// 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 {
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)
}
}
// PlaceUnit writes a unit file at the provided destination, creating // PlaceUnit writes a unit file at the provided destination, creating
// parent directories as necessary. // parent directories as necessary.
func PlaceUnit(u *Unit, dst string) error { func (s *systemd) PlaceUnit(u *Unit, dst string) error {
dir := filepath.Dir(dst) dir := filepath.Dir(dst)
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
@@ -91,7 +49,7 @@ func PlaceUnit(u *Unit, dst string) error {
return nil return nil
} }
func EnableUnitFile(unit string, runtime bool) error { func (s *systemd) EnableUnitFile(unit string, runtime bool) error {
conn, err := dbus.New() conn, err := dbus.New()
if err != nil { if err != nil {
return err return err
@@ -102,7 +60,7 @@ func EnableUnitFile(unit string, runtime bool) error {
return err return err
} }
func RunUnitCommand(command, unit string) (string, error) { func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
conn, err := dbus.New() conn, err := dbus.New()
if err != nil { if err != nil {
return "", err return "", err
@@ -131,7 +89,7 @@ func RunUnitCommand(command, unit string) (string, error) {
return fn(unit, "replace") return fn(unit, "replace")
} }
func DaemonReload() error { func (s *systemd) DaemonReload() error {
conn, err := dbus.New() conn, err := dbus.New()
if err != nil { if err != nil {
return err return err
@@ -140,6 +98,57 @@ func DaemonReload() error {
return conn.Reload() return conn.Reload()
} }
// MaskUnit masks the given Unit by symlinking its unit file to
// /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)
if _, err := os.Stat(masked); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
return err
}
} else if err := os.Remove(masked); err != nil {
return err
}
return os.Symlink("/dev/null", masked)
}
// 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)
ne, err := nullOrEmpty(masked)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return err
}
if !ne {
log.Printf("%s is not null or empty, refusing to unmask", masked)
return nil
}
return os.Remove(masked)
}
// nullOrEmpty checks whether a given path appears to be an empty regular file
// or a symlink to /dev/null
func nullOrEmpty(path string) (bool, error) {
fi, err := os.Stat(path)
if err != nil {
return false, err
}
m := fi.Mode()
if m.IsRegular() && fi.Size() <= 0 {
return true, nil
}
if m&os.ModeCharDevice > 0 {
return true, nil
}
return false, nil
}
func ExecuteScript(scriptPath string) (string, error) { func ExecuteScript(scriptPath string) (string, error) {
props := []dbus.Property{ props := []dbus.Property{
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"), dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
@@ -178,54 +187,3 @@ func MachineID(root string) string {
return id return id
} }
// MaskUnit masks the given Unit by symlinking its unit file to
// /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 MaskUnit(unit *Unit, root string) error {
masked := unit.Destination(root)
if _, err := os.Stat(masked); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
return err
}
} else if err := os.Remove(masked); err != nil {
return err
}
return os.Symlink("/dev/null", masked)
}
// 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 UnmaskUnit(unit *Unit, root string) error {
masked := unit.Destination(root)
ne, err := nullOrEmpty(masked)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return err
}
if !ne {
log.Printf("%s is not null or empty, refusing to unmask", masked)
return nil
}
return os.Remove(masked)
}
// nullOrEmpty checks whether a given path appears to be an empty regular file
// or a symlink to /dev/null
func nullOrEmpty(path string) (bool, error) {
fi, err := os.Stat(path)
if err != nil {
return false, err
}
m := fi.Mode()
if m.IsRegular() && fi.Size() <= 0 {
return true, nil
}
if m&os.ModeCharDevice > 0 {
return true, nil
}
return false, nil
}

View File

@@ -25,13 +25,15 @@ Address=10.209.171.177/19
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
sd := &systemd{dir}
dst := u.Destination(dir) dst := u.Destination(dir)
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network") expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
if dst != expectDst { if dst != expectDst {
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst) t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
if err := PlaceUnit(&u, dst); err != nil { if err := sd.PlaceUnit(&u, dst); err != nil {
t.Fatalf("PlaceUnit failed: %v", err) t.Fatalf("PlaceUnit failed: %v", err)
} }
@@ -100,13 +102,15 @@ Where=/media/state
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
sd := &systemd{dir}
dst := u.Destination(dir) dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount") expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
if dst != expectDst { if dst != expectDst {
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst) t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
if err := PlaceUnit(&u, dst); err != nil { if err := sd.PlaceUnit(&u, dst); err != nil {
t.Fatalf("PlaceUnit failed: %v", err) t.Fatalf("PlaceUnit failed: %v", err)
} }
@@ -155,9 +159,11 @@ func TestMaskUnit(t *testing.T) {
} }
defer os.RemoveAll(dir) defer os.RemoveAll(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{Name: "foo.service"} uf := &Unit{Name: "foo.service"}
if err := MaskUnit(uf, dir); 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)
} }
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service") fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
@@ -175,7 +181,7 @@ func TestMaskUnit(t *testing.T) {
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)
} }
if err := MaskUnit(ub, dir); err != nil { if err := sd.MaskUnit(ub); err != nil {
t.Fatalf("Unable to mask existing unit: %v", err) t.Fatalf("Unable to mask existing unit: %v", err)
} }
barTgt, err := os.Readlink(barPath) barTgt, err := os.Readlink(barPath)
@@ -194,8 +200,10 @@ func TestUnmaskUnit(t *testing.T) {
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
sd := &systemd{dir}
nilUnit := &Unit{Name: "null.service"} nilUnit := &Unit{Name: "null.service"}
if err := UnmaskUnit(nilUnit, dir); 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)
} }
@@ -211,7 +219,7 @@ func TestUnmaskUnit(t *testing.T) {
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil { if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
t.Fatalf("Unable to write unit file: %v", err) t.Fatalf("Unable to write unit file: %v", err)
} }
if err := UnmaskUnit(uf, dir); err != nil { if err := sd.UnmaskUnit(uf); err != nil {
t.Errorf("unmask of non-empty unit returned unexpected error: %v", err) t.Errorf("unmask of non-empty unit returned unexpected error: %v", err)
} }
got, _ := ioutil.ReadFile(dst) got, _ := ioutil.ReadFile(dst)
@@ -224,7 +232,7 @@ func TestUnmaskUnit(t *testing.T) {
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)
} }
if err := UnmaskUnit(ub, dir); err != nil { if err := sd.UnmaskUnit(ub); err != nil {
t.Errorf("unmask of unit returned unexpected error: %v", err) t.Errorf("unmask of unit returned unexpected error: %v", err)
} }
if _, err := os.Stat(dst); !os.IsNotExist(err) { if _, err := os.Stat(dst); !os.IsNotExist(err) {

67
system/unit.go Normal file
View File

@@ -0,0 +1,67 @@
package system
import (
"fmt"
"path"
"path/filepath"
"strings"
)
// 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)
DaemonReload() error
MaskUnit(unit *Unit) error
UnmaskUnit(unit *Unit) error
}
type Unit struct {
Name string
Mask bool
Enable bool
Runtime bool
Content string
Command string
// 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() (group string) {
t := u.Type()
if t == "network" || t == "netdev" || t == "link" {
group = "network"
} else {
group = "system"
}
return
}
type Script []byte
// 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 {
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)
}
}