Compare commits

...

33 Commits

Author SHA1 Message Date
Alex Crawford
5d65ca230a coreos-cloudinit: bump to 0.9.1 2014-08-04 14:13:51 -07:00
Alex Crawford
38b3e1213a Merge pull request #188 from crawford/configdrive
configdrive: Use the EC2 metadata over OpenStack
2014-08-04 11:12:06 -07:00
Alex Crawford
4eedca26e9 configdrive: Use the EC2 metadata over OpenStack
Standardize on specific EC2 and OpenStack versions and add tests.
2014-08-04 10:18:29 -07:00
Brian Waldon
f2b342c8be doc: escape user.home example 2014-08-01 13:20:44 -07:00
Michael Marineau
c19d8f6b61 Merge pull request #193 from benjic/cloudconfig_variables
docs(quick-start): Clarified use of fields in cloud config
2014-07-24 11:02:03 -07:00
Benjamin Campbell
7913f74351 docs(quick-start) Enumerated supported platforms
Following suggestion a list of platforms that *do* support cloud config variables. In addition minor mark up formatting is added.
2014-07-24 11:54:31 -06:00
Benjamin Campbell
5593408be8 docs(quick-start): Clarified use of fields in cloud config
Updated the language to illustrate that fields in a cloud config is not
supported in all environments. This is expressed explicitly in PXE and
install to disk pages. The quick start lacked this information and is
inconsistent.
2014-07-24 11:27:35 -06:00
Alex Crawford
7fc67c2acf Merge pull request #191 from crawford/panic
config: Verify that type assertions are valid
2014-07-22 11:51:39 -07:00
Alex Crawford
b093094292 config: Verify that type assertions are valid 2014-07-22 11:39:20 -07:00
Michael Marineau
9a80fd714a Merge pull request #181 from robszumski/docs-startup
fix(docs): clarity around boot behavior and unit usage
2014-07-21 22:12:19 -07:00
Rob Szumski
fef5473881 fix(docs): clarity around boot behavior and unit usage 2014-07-21 21:41:00 -07:00
Alex Crawford
bf5a2b208f coreos-cloudinit: bump to 0.9.0+git 2014-07-21 19:17:14 -07:00
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
Michael Marineau
bf5d3539c9 coreos-cloudinit: bump to 0.8.9 2014-07-11 14:40:34 -07:00
Michael Marineau
5e4cbcd909 Merge pull request #182 from marineam/fix
env_file: fix broken test cases
2014-07-11 14:38:56 -07:00
Michael Marineau
a270c4c737 env_file: fix broken test cases
TestWriteEnvFileDos2Unix had a copy/paste bug, it shouldn't have
asserted that mtime doesn't change because the file is actually being
modified in this test. This didn't come up earlier because the actual
comparison wasn't using Time.Equal as it should have.

Instead switch to comparing inode numbers which is the actual thing I
wanted to test for in the first place, just accessing them is much more
awkard. Now all tests where it is relevant check the inode in addition
to the contents.
2014-07-11 13:35:10 -07:00
Michael Marineau
f356a8a690 coreos-cloudinit: bump to 0.8.8+git 2014-07-11 11:13:01 -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
21 changed files with 751 additions and 245 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...
```
```yaml
#cloud-config
coreos:
oem:
@@ -26,7 +26,7 @@ coreos:
...would be rendered to the following `/etc/oem-release`:
```
```yaml
ID=rackspace
NAME="Rackspace Cloud Servers"
VERSION_ID=168.0.0

View File

@@ -1,6 +1,6 @@
# Using Cloud-Config
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime.
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime. Your cloud-config is processed during each boot.
## Configuration File
@@ -40,9 +40,9 @@ CoreOS tries to conform to each platform's native method to provide user data. E
#### etcd
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...
If the platform environment supports the templating feature of coreos-cloudinit it is possible to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
```
```yaml
#cloud-config
coreos:
@@ -57,7 +57,7 @@ coreos:
...will generate a systemd unit drop-in like this:
```
```yaml
[Service]
Environment="ETCD_NAME=node001"
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
@@ -68,13 +68,15 @@ 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, and Vagrant._
[etcd-config]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md
#### fleet
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
coreos:
@@ -85,7 +87,7 @@ coreos:
...will generate a systemd unit drop-in like this:
```
```yaml
[Service]
Environment="FLEET_PUBLIC_IP=203.0.113.29"
Environment="FLEET_METADATA=region=us-west"
@@ -114,7 +116,7 @@ The `reboot-strategy` parameter also affects the behaviour of [locksmith](https:
##### Example
```
```yaml
#cloud-config
coreos:
update:
@@ -123,7 +125,9 @@ coreos:
#### units
The `coreos.units.*` parameters define a list of arbitrary systemd units to start. Each item is an object with the following fields:
The `coreos.units.*` parameters define a list of arbitrary systemd units to start after booting. This feature is intended to help you start essential services required to mount storage and configure networking in order to join the CoreOS cluster. It is not intended to be a Chef/Puppet replacement.
Each item is an object with the following fields:
- **name**: String representing unit's name. Required.
- **runtime**: Boolean indicating whether or not to persist the unit across reboots. This is analogous to the `--runtime` argument to `systemctl enable`. Default value is false.
@@ -138,7 +142,7 @@ The `coreos.units.*` parameters define a list of arbitrary systemd units to star
Write a unit to disk, automatically starting it.
```
```yaml
#cloud-config
coreos:
@@ -159,7 +163,7 @@ coreos:
Start the built-in `etcd` and `fleet` services:
```
```yaml
#cloud-config
coreos:
@@ -177,7 +181,7 @@ The `ssh_authorized_keys` parameter adds public SSH keys which will be authorize
The keys will be named "coreos-cloudinit" by default.
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
```
```yaml
#cloud-config
ssh_authorized_keys:
@@ -189,7 +193,7 @@ ssh_authorized_keys:
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`).
```
```yaml
#cloud-config
hostname: coreos1
@@ -203,7 +207,7 @@ All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the use
- **name**: Required. Login name of user
- **gecos**: GECOS comment of user
- **passwd**: Hash of the password to use for this user
- **homedir**: User's home directory. Defaults to /home/<name>
- **homedir**: User's home directory. Defaults to /home/\<name\>
- **no-create-home**: Boolean. Skip home directory creation.
- **primary-group**: Default group for the user. Defaults to a new group created named after the user.
- **groups**: Add user to these additional groups
@@ -222,7 +226,7 @@ The following fields are not yet implemented:
- **selinux-user**: Corresponding SELinux user
- **ssh-import-id**: Import SSH keys by ID from Launchpad.
```
```yaml
#cloud-config
users:
@@ -261,7 +265,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.
```
```yaml
#cloud-config
users:
@@ -274,7 +278,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).
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
```
```yaml
#cloud-config
users:
@@ -284,7 +288,7 @@ users:
You can also specify any URL whose response matches the JSON format for public keys:
```
```yaml
#cloud-config
users:
@@ -304,7 +308,7 @@ The `write-file` parameter defines a list of files to create on the local filesy
Explicitly not implemented is the **encoding** attribute.
The **content** field must represent exactly what should be written to disk.
```
```yaml
#cloud-config
write_files:
- path: /etc/fleet/fleet.conf
@@ -321,7 +325,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
infrastructure in place to resolve its own hostname, for example, when using Vagrant.
```
```yaml
#cloud-config
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:
mkdir -p /tmp/new-drive/openstack/latest
cp user_data /tmp/new-drive/openstack/latest/user_data
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
rm -r /tmp/new-drive
```sh
mkdir -p /tmp/new-drive/openstack/latest
cp user_data /tmp/new-drive/openstack/latest/user_data
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
rm -r /tmp/new-drive
```
## QEMU virtfs
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:
qemu-system-x86_64 \
```sh
qemu-system-x86_64 \
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
[usual qemu options here...]
```

View File

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

View File

@@ -8,10 +8,11 @@ import (
type configDrive struct {
root string
readFile func(filename string) ([]byte, error)
}
func NewConfigDrive(root string) *configDrive {
return &configDrive{path.Join(root, "openstack")}
return &configDrive{root, ioutil.ReadFile}
}
func (cd *configDrive) IsAvailable() bool {
@@ -24,23 +25,38 @@ func (cd *configDrive) AvailabilityChanges() bool {
}
func (cd *configDrive) ConfigRoot() string {
return cd.root
return cd.openstackRoot()
}
// FetchMetadata attempts to retrieve metadata from ec2/2009-04-04/meta_data.json.
func (cd *configDrive) FetchMetadata() ([]byte, error) {
return cd.readFile("meta_data.json")
return cd.tryReadFile(path.Join(cd.ec2Root(), "meta_data.json"))
}
// FetchUserdata attempts to retrieve the userdata from ec2/2009-04-04/user_data.
// If no data is found, it will attempt to read from openstack/latest/user_data.
func (cd *configDrive) FetchUserdata() ([]byte, error) {
return cd.readFile("user_data")
bytes, err := cd.tryReadFile(path.Join(cd.ec2Root(), "user_data"))
if bytes == nil && err == nil {
bytes, err = cd.tryReadFile(path.Join(cd.openstackRoot(), "user_data"))
}
return bytes, err
}
func (cd *configDrive) Type() string {
return "cloud-drive"
}
func (cd *configDrive) readFile(filename string) ([]byte, error) {
data, err := ioutil.ReadFile(path.Join(cd.root, "latest", filename))
func (cd *configDrive) ec2Root() string {
return path.Join(cd.root, "ec2", Ec2ApiVersion)
}
func (cd *configDrive) openstackRoot() string {
return path.Join(cd.root, "openstack", "latest")
}
func (cd *configDrive) tryReadFile(filename string) ([]byte, error) {
data, err := cd.readFile(filename)
if os.IsNotExist(err) {
err = nil
}

View File

@@ -0,0 +1,114 @@
package datasource
import (
"os"
"testing"
)
type mockFilesystem []string
func (m mockFilesystem) readFile(filename string) ([]byte, error) {
for _, file := range m {
if file == filename {
return []byte(filename), nil
}
}
return nil, os.ErrNotExist
}
func TestCDFetchMetadata(t *testing.T) {
for _, tt := range []struct {
root string
filename string
files mockFilesystem
}{
{
"/",
"",
mockFilesystem{},
},
{
"/",
"/ec2/2009-04-04/meta_data.json",
mockFilesystem([]string{"/ec2/2009-04-04/meta_data.json"}),
},
{
"/media/configdrive",
"/media/configdrive/ec2/2009-04-04/meta_data.json",
mockFilesystem([]string{"/media/configdrive/ec2/2009-04-04/meta_data.json"}),
},
} {
cd := configDrive{tt.root, tt.files.readFile}
filename, err := cd.FetchMetadata()
if err != nil {
t.Fatalf("bad error for %q: want %q, got %q", tt, nil, err)
}
if string(filename) != tt.filename {
t.Fatalf("bad path for %q: want %q, got %q", tt, tt.filename, filename)
}
}
}
func TestCDFetchUserdata(t *testing.T) {
for _, tt := range []struct {
root string
filename string
files mockFilesystem
}{
{
"/",
"",
mockFilesystem{},
},
{
"/",
"/ec2/2009-04-04/user_data",
mockFilesystem([]string{"/ec2/2009-04-04/user_data"}),
},
{
"/",
"/openstack/latest/user_data",
mockFilesystem([]string{"/openstack/latest/user_data"}),
},
{
"/",
"/ec2/2009-04-04/user_data",
mockFilesystem([]string{"/openstack/latest/user_data", "/ec2/2009-04-04/user_data"}),
},
{
"/media/configdrive",
"/media/configdrive/ec2/2009-04-04/user_data",
mockFilesystem([]string{"/media/configdrive/ec2/2009-04-04/user_data"}),
},
} {
cd := configDrive{tt.root, tt.files.readFile}
filename, err := cd.FetchUserdata()
if err != nil {
t.Fatalf("bad error for %q: want %q, got %q", tt, nil, err)
}
if string(filename) != tt.filename {
t.Fatalf("bad path for %q: want %q, got %q", tt, tt.filename, filename)
}
}
}
func TestCDConfigRoot(t *testing.T) {
for _, tt := range []struct {
root string
configRoot string
}{
{
"/",
"/openstack/latest",
},
{
"/media/configdrive",
"/media/configdrive/openstack/latest",
},
} {
cd := configDrive{tt.root, nil}
if configRoot := cd.ConfigRoot(); configRoot != tt.configRoot {
t.Fatalf("bad config root for %q: want %q, got %q", tt, tt.configRoot, configRoot)
}
}
}

View File

@@ -1,5 +1,10 @@
package datasource
const (
Ec2ApiVersion = "2009-04-04"
OpenstackApiVersion = "2012-08-10"
)
type Datasource interface {
IsAvailable() bool
AvailabilityChanges() bool

View File

@@ -22,11 +22,9 @@ import (
const (
BaseUrl = "http://169.254.169.254/"
Ec2ApiVersion = "2009-04-04"
Ec2UserdataUrl = BaseUrl + Ec2ApiVersion + "/user-data"
Ec2MetadataUrl = BaseUrl + Ec2ApiVersion + "/meta-data"
OpenstackApiVersion = "openstack/2012-08-10"
OpenstackUserdataUrl = BaseUrl + OpenstackApiVersion + "/user_data"
OpenstackUserdataUrl = BaseUrl + "openstack/" + OpenstackApiVersion + "/user_data"
)
type metadataService struct{}

View File

@@ -25,7 +25,7 @@ func (t *TestHttpClient) GetRetry(url string) ([]byte, error) {
}
}
func TestFetchAttributes(t *testing.T) {
func TestMSFetchAttributes(t *testing.T) {
for _, s := range []struct {
metadata map[string]string
err error
@@ -77,7 +77,7 @@ func TestFetchAttributes(t *testing.T) {
}
}
func TestFetchAttribute(t *testing.T) {
func TestMSFetchAttribute(t *testing.T) {
for _, s := range []struct {
metadata map[string]string
err error
@@ -129,7 +129,7 @@ func TestFetchAttribute(t *testing.T) {
}
}
func TestFetchMetadata(t *testing.T) {
func TestMSFetchMetadata(t *testing.T) {
for _, tt := range []struct {
metadata map[string]string
err error

View File

@@ -66,52 +66,63 @@ func warnOnUnrecognizedKeys(contents string, warn warner) {
}
// Check for unrecognized coreos options, if any are set
coreos, ok := c["coreos"]
if ok {
set := coreos.(map[interface{}]interface{})
if coreos, ok := c["coreos"]; ok {
if set, ok := coreos.(map[interface{}]interface{}); ok {
known := cc["coreos"].(map[interface{}]interface{})
for k, _ := range set {
key := k.(string)
if key, ok := k.(string); ok {
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", key)
}
} else {
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", k)
}
}
}
}
// Check for any badly-specified users, if any are set
users, ok := c["users"]
if ok {
if users, ok := c["users"]; ok {
var known map[string]interface{}
b, _ := goyaml.Marshal(&system.User{})
goyaml.Unmarshal(b, &known)
set := users.([]interface{})
if set, ok := users.([]interface{}); ok {
for _, u := range set {
user := u.(map[interface{}]interface{})
if user, ok := u.(map[interface{}]interface{}); ok {
for k, _ := range user {
key := k.(string)
if key, ok := k.(string); ok {
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", key)
}
} else {
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", k)
}
}
}
}
}
}
// Check for any badly-specified files, if any are set
files, ok := c["write_files"]
if ok {
if files, ok := c["write_files"]; ok {
var known map[string]interface{}
b, _ := goyaml.Marshal(&system.File{})
goyaml.Unmarshal(b, &known)
set := files.([]interface{})
if set, ok := files.([]interface{}); ok {
for _, f := range set {
file := f.(map[interface{}]interface{})
if file, ok := f.(map[interface{}]interface{}); ok {
for k, _ := range file {
key := k.(string)
if key, ok := k.(string); ok {
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", key)
}
} else {
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", k)
}
}
}
}
}
}
@@ -272,13 +283,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)
reload := false
for _, unit := range cfg.Coreos.Units {
dst := unit.Destination(env.Root())
for _, unit := range units {
dst := unit.Destination(root)
if unit.Content != "" {
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
}
log.Printf("Placed unit %s at %s", unit.Name, dst)
@@ -287,12 +308,12 @@ func Apply(cfg CloudConfig, env *Environment) error {
if unit.Mask {
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
}
} else if unit.Runtime {
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
}
}
@@ -300,7 +321,7 @@ func Apply(cfg CloudConfig, env *Environment) error {
if unit.Enable {
if unit.Group() != "network" {
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
}
log.Printf("Enabled unit %s", unit.Name)
@@ -317,14 +338,14 @@ func Apply(cfg CloudConfig, env *Environment) error {
}
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))
}
}
for unit, command := range commands {
log.Printf("Calling unit command '%s %s'", command, unit)
res, err := system.RunUnitCommand(command, unit)
res, err := um.RunUnitCommand(command, unit)
if err != nil {
return err
}

View File

@@ -4,8 +4,38 @@ import (
"fmt"
"strings"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestCloudConfigInvalidKeys(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("panic while instantiating CloudConfig with nil keys: %v", r)
}
}()
for _, tt := range []struct {
contents string
}{
{"coreos:"},
{"ssh_authorized_keys:"},
{"ssh_authorized_keys:\n -"},
{"ssh_authorized_keys:\n - 0:"},
{"write_files:"},
{"write_files:\n -"},
{"write_files:\n - 0:"},
{"users:"},
{"users:\n -"},
{"users:\n - 0:"},
} {
_, err := NewCloudConfig(tt.contents)
if err != nil {
t.Fatalf("error instantiating CloudConfig with invalid keys: %v", err)
}
}
}
func TestCloudConfigUnknownKeys(t *testing.T) {
contents := `
coreos:
@@ -332,3 +362,109 @@ users:
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)
sd := system.NewUnitManager(dir)
uu, err := ee.Units(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
@@ -81,7 +83,7 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
dst := u.Destination(dir)
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)
}
@@ -119,6 +121,8 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
}
defer os.RemoveAll(dir)
sd := system.NewUnitManager(dir)
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
if err != nil {
@@ -136,7 +140,7 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
dst := u.Destination(dir)
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)
}

View File

@@ -3,6 +3,7 @@ package network
import (
"fmt"
"strconv"
"strings"
)
type InterfaceGenerator interface {
@@ -11,6 +12,8 @@ type InterfaceGenerator interface {
Netdev() string
Link() string
Network() string
Type() string
ModprobeParams() string
}
type networkInterface interface {
@@ -68,6 +71,10 @@ func (i *logicalInterface) Children() []networkInterface {
return i.children
}
func (i *logicalInterface) ModprobeParams() string {
return ""
}
func (i *logicalInterface) setConfigDepth(depth int) {
i.configDepth = depth
}
@@ -84,9 +91,14 @@ func (p *physicalInterface) Netdev() string {
return ""
}
func (p *physicalInterface) Type() string {
return "physical"
}
type bondInterface struct {
logicalInterface
slaves []string
options map[string]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)
}
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 {
logicalInterface
id int
@@ -123,6 +148,10 @@ func (v *vlanInterface) Netdev() string {
return config
}
func (v *vlanInterface) Type() string {
return "vlan"
}
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
interfaceMap := createInterfaces(stanzas)
linkAncestors(interfaceMap)
@@ -141,15 +170,22 @@ func createInterfaces(stanzas []*stanzaInterface) map[string]networkInterface {
for _, iface := range stanzas {
switch iface.kind {
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{
logicalInterface{
name: iface.name,
config: iface.configMethod,
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 {
interfaceMap[slave] = &physicalInterface{
logicalInterface{
@@ -241,13 +277,17 @@ func markConfigDepths(interfaceMap map[string]networkInterface) {
}
}
for _, iface := range rootInterfaceMap {
setDepth(iface, 0)
setDepth(iface)
}
}
func setDepth(iface networkInterface, depth int) {
iface.setConfigDepth(depth)
func setDepth(iface networkInterface) int {
maxDepth := 0
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",
},
nil,
nil,
},
&vlanInterface{
logicalInterface{
@@ -67,14 +68,14 @@ VLAN=testvlan2
}
func TestBondInterfaceName(t *testing.T) {
b := bondInterface{logicalInterface{name: "testname"}, nil}
b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
if b.Name() != "testname" {
t.FailNow()
}
}
func TestBondInterfaceNetdev(t *testing.T) {
b := bondInterface{logicalInterface{name: "testname"}, nil}
b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
netdev := `[NetDev]
Kind=bond
Name=testname
@@ -102,6 +103,7 @@ func TestBondInterfaceNetwork(t *testing.T) {
name: "testbond1",
},
nil,
nil,
},
&vlanInterface{
logicalInterface{
@@ -120,6 +122,7 @@ func TestBondInterfaceNetwork(t *testing.T) {
},
},
nil,
nil,
}
network := `[Match]
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) {
stanzas := []*stanzaInterface{
&stanzaInterface{
@@ -242,7 +300,7 @@ func TestBuildInterfacesBlindBond(t *testing.T) {
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"slaves": []string{"eth0"},
"bond-slaves": []string{"eth0"},
},
},
}
@@ -252,16 +310,17 @@ func TestBuildInterfacesBlindBond(t *testing.T) {
name: "bond0",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 1,
configDepth: 0,
},
[]string{"eth0"},
map[string]string{},
}
eth0 := &physicalInterface{
logicalInterface{
name: "eth0",
config: configMethodManual{},
children: []networkInterface{bond0},
configDepth: 0,
configDepth: 1,
},
}
expect := []InterfaceGenerator{bond0, eth0}
@@ -289,7 +348,7 @@ func TestBuildInterfacesBlindVLAN(t *testing.T) {
name: "vlan0",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 1,
configDepth: 0,
},
0,
"eth0",
@@ -299,7 +358,7 @@ func TestBuildInterfacesBlindVLAN(t *testing.T) {
name: "eth0",
config: configMethodManual{},
children: []networkInterface{vlan0},
configDepth: 0,
configDepth: 1,
},
}
expect := []InterfaceGenerator{eth0, vlan0}
@@ -323,7 +382,9 @@ func TestBuildInterfaces(t *testing.T) {
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"slaves": []string{"eth0"},
"bond-slaves": []string{"eth0"},
"bond-mode": []string{"4"},
"bond-miimon": []string{"100"},
},
},
&stanzaInterface{
@@ -332,7 +393,7 @@ func TestBuildInterfaces(t *testing.T) {
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"slaves": []string{"bond0"},
"bond-slaves": []string{"bond0"},
},
},
&stanzaInterface{
@@ -362,7 +423,7 @@ func TestBuildInterfaces(t *testing.T) {
name: "vlan1",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 2,
configDepth: 0,
},
1,
"bond0",
@@ -372,7 +433,7 @@ func TestBuildInterfaces(t *testing.T) {
name: "vlan0",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 1,
configDepth: 0,
},
0,
"eth0",
@@ -382,9 +443,10 @@ func TestBuildInterfaces(t *testing.T) {
name: "bond1",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 2,
configDepth: 0,
},
[]string{"bond0"},
map[string]string{},
}
bond0 := &bondInterface{
logicalInterface{
@@ -394,13 +456,17 @@ func TestBuildInterfaces(t *testing.T) {
configDepth: 1,
},
[]string{"eth0"},
map[string]string{
"mode": "4",
"miimon": "100",
},
}
eth0 := &physicalInterface{
logicalInterface{
name: "eth0",
config: configMethodManual{},
children: []networkInterface{bond0, vlan0},
configDepth: 0,
configDepth: 2,
},
}
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) {
options["slaves"] = options["bond-slaves"]
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 {
t.FailNow()
}
if bond.options["slaves"] != nil {
if bond.options["bond-slaves"] != nil {
t.FailNow()
}
}
@@ -152,9 +152,6 @@ func TestParseBondStanza(t *testing.T) {
if bond.configMethod != conf {
t.FailNow()
}
if !reflect.DeepEqual(bond.options["slaves"], options["bond-slaves"]) {
t.FailNow()
}
}
func TestParsePhysicalStanza(t *testing.T) {

View File

@@ -5,6 +5,7 @@ import (
"os"
"path"
"strings"
"syscall"
"testing"
)
@@ -41,6 +42,11 @@ func TestWriteEnvFileUpdate(t *testing.T) {
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(base), 0644)
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
File: &File{
Path: name,
@@ -61,6 +67,15 @@ func TestWriteEnvFileUpdate(t *testing.T) {
if string(contents) != expectUpdate {
t.Fatalf("File has incorrect contents: %q", contents)
}
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was not replaced: %s", fullPath)
}
}
func TestWriteEnvFileUpdateNoNewline(t *testing.T) {
@@ -74,6 +89,11 @@ func TestWriteEnvFileUpdateNoNewline(t *testing.T) {
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(baseNoNewline), 0644)
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
File: &File{
Path: name,
@@ -94,6 +114,15 @@ func TestWriteEnvFileUpdateNoNewline(t *testing.T) {
if string(contents) != expectUpdate {
t.Fatalf("File has incorrect contents: %q", contents)
}
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was not replaced: %s", fullPath)
}
}
func TestWriteEnvFileCreate(t *testing.T) {
@@ -141,7 +170,7 @@ func TestWriteEnvFileNoop(t *testing.T) {
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatal("Unable to stat file: %v", err)
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
@@ -167,11 +196,11 @@ func TestWriteEnvFileNoop(t *testing.T) {
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatal("Unable to stat file: %v", err)
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.ModTime() != newStat.ModTime() {
t.Fatal("File mtime changed.")
if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was replaced: %s", fullPath)
}
}
@@ -186,6 +215,11 @@ func TestWriteEnvFileUpdateDos(t *testing.T) {
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(baseDos), 0644)
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
File: &File{
Path: name,
@@ -206,6 +240,15 @@ func TestWriteEnvFileUpdateDos(t *testing.T) {
if string(contents) != expectUpdate {
t.Fatalf("File has incorrect contents: %q", contents)
}
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was not replaced: %s", fullPath)
}
}
// A middle ground noop, values are unchanged but we did have a value.
@@ -223,7 +266,7 @@ func TestWriteEnvFileDos2Unix(t *testing.T) {
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatal("Unable to stat file: %v", err)
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
@@ -249,11 +292,11 @@ func TestWriteEnvFileDos2Unix(t *testing.T) {
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatal("Unable to stat file: %v", err)
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.ModTime() != newStat.ModTime() {
t.Fatal("File mtime changed.")
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was not replaced: %s", fullPath)
}
}
@@ -271,7 +314,7 @@ func TestWriteEnvFileEmpty(t *testing.T) {
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatal("Unable to stat file: %v", err)
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
@@ -297,11 +340,11 @@ func TestWriteEnvFileEmpty(t *testing.T) {
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatal("Unable to stat file: %v", err)
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.ModTime() != newStat.ModTime() {
t.Fatal("File mtime changed.")
if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was replaced: %s", fullPath)
}
}
@@ -368,8 +411,6 @@ func TestWriteEnvFileNameFailure(t *testing.T) {
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(base), 0000)
ef := EnvFile{
File: &File{

View File

@@ -2,10 +2,11 @@ package system
import (
"fmt"
"io/ioutil"
"log"
"net"
"os/exec"
"path"
"strings"
"time"
"github.com/coreos/coreos-cloudinit/network"
"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) {
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 {
err = e
}
@@ -26,19 +34,18 @@ func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
return
}
if err = probe8012q(); err != nil {
if err = maybeProbe8012q(interfaces); err != nil {
return
}
return
return maybeProbeBonding(interfaces)
}
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
sysInterfaceMap := make(map[string]*net.Interface)
if systemInterfaces, err := net.Interfaces(); err == nil {
for _, iface := range systemInterfaces {
// Need a copy of the interface so we can take the address
temp := iface
sysInterfaceMap[temp.Name] = &temp
iface := iface
sysInterfaceMap[iface.Name] = &iface
}
} else {
return err
@@ -46,6 +53,7 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
for _, iface := range interfaces {
if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok {
log.Printf("Taking down interface %q\n", systemInterface.Name)
if err := netlink.NetworkLinkDown(systemInterface); err != nil {
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
}
func probe8012q() error {
func maybeProbe8012q(interfaces []network.InterfaceGenerator) error {
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 {
_, err := RunUnitCommand("restart", "systemd-networkd.service")
log.Printf("Restarting networkd.service\n")
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service")
return err
}
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
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 {
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 {
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 {
return err
}
@@ -86,6 +113,7 @@ func writeConfig(filename string, config string) error {
if config == "" {
return nil
}
return ioutil.WriteFile(filename, []byte(config), 0444)
log.Printf("Writing networkd unit %q\n", filename)
_, 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"
)
func NewUnitManager(root string) UnitManager {
return &systemd{root}
}
type systemd struct {
root string
}
// fakeMachineID is placed on non-usr CoreOS images and should
// never be used as a true MachineID
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
// parent directories as necessary.
func PlaceUnit(u *Unit, dst string) error {
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 {
@@ -91,7 +49,7 @@ func PlaceUnit(u *Unit, dst string) error {
return nil
}
func EnableUnitFile(unit string, runtime bool) error {
func (s *systemd) EnableUnitFile(unit string, runtime bool) error {
conn, err := dbus.New()
if err != nil {
return err
@@ -102,7 +60,7 @@ func EnableUnitFile(unit string, runtime bool) error {
return err
}
func RunUnitCommand(command, unit string) (string, error) {
func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
conn, err := dbus.New()
if err != nil {
return "", err
@@ -131,7 +89,7 @@ func RunUnitCommand(command, unit string) (string, error) {
return fn(unit, "replace")
}
func DaemonReload() error {
func (s *systemd) DaemonReload() error {
conn, err := dbus.New()
if err != nil {
return err
@@ -140,6 +98,57 @@ func DaemonReload() error {
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) {
props := []dbus.Property{
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
@@ -178,54 +187,3 @@ func MachineID(root string) string {
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)
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)
}
if err := PlaceUnit(&u, dst); err != nil {
if err := sd.PlaceUnit(&u, dst); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
@@ -100,13 +102,15 @@ Where=/media/state
}
defer os.RemoveAll(dir)
sd := &systemd{dir}
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)
}
if err := PlaceUnit(&u, dst); err != nil {
if err := sd.PlaceUnit(&u, dst); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
@@ -155,9 +159,11 @@ func TestMaskUnit(t *testing.T) {
}
defer os.RemoveAll(dir)
sd := &systemd{dir}
// Ensure mask works with units that do not currently exist
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)
}
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 {
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)
}
barTgt, err := os.Readlink(barPath)
@@ -194,8 +200,10 @@ func TestUnmaskUnit(t *testing.T) {
}
defer os.RemoveAll(dir)
sd := &systemd{dir}
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)
}
@@ -211,7 +219,7 @@ func TestUnmaskUnit(t *testing.T) {
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
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)
}
got, _ := ioutil.ReadFile(dst)
@@ -224,7 +232,7 @@ func TestUnmaskUnit(t *testing.T) {
if err := os.Symlink("/dev/null", dst); err != nil {
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)
}
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)
}
}