Compare commits

...

49 Commits

Author SHA1 Message Date
Alex Crawford
f3f3af79fd coreos-cloudinit: bump to v1.0.1 2014-12-04 16:34:57 -08:00
Alex Crawford
0e63aa0f6b Merge pull request #276 from crawford/networkd
initialize: restart networkd before other units
2014-12-04 16:33:02 -08:00
Alex Crawford
b254e17e89 Merge pull request #263 from robszumski/docs-validator
docs: link to validator
2014-12-04 16:28:21 -08:00
Alex Crawford
5c059b66f0 initialize: restart networkd before other units 2014-12-04 15:25:44 -08:00
Alex Crawford
c628bef666 Merge pull request #273 from crawford/networkd
initialize: only restart networkd once per config
2014-12-02 12:54:27 -08:00
Alex Crawford
2270db3f7a initialize: only restart networkd once per config
Regression introduced by 9fcf338bf3.

Networkd was erroneously being restarted once per network unit. It
should only restart once for the entire config.
2014-12-02 12:46:35 -08:00
Alex Crawford
d0d467813d Merge pull request #251 from Vladimiroff/master
metadata: Populate CloudSigma's IPs properly
2014-12-01 14:52:11 -08:00
Alex Crawford
123f111efe coreos-cloudinit: bump to 1.0.0+git 2014-11-26 14:19:29 -08:00
Alex Crawford
521ecfdab5 coreos-cloudinit: bump to 1.0.0 2014-11-26 14:19:13 -08:00
Alex Crawford
6d0fdf1a47 Merge pull request #268 from crawford/dropins
drop-in: add support for drop-ins
2014-11-26 14:14:49 -08:00
Alex Crawford
ffc54b028c drop-in: add support for drop-ins
This allows a list of drop-ins for a unit to be declared inline within a
cloud-config. For example:

  #cloud-config
  coreos:
    units:
      - name: docker.service
        drop-ins:
          - name: 50-insecure-registry.conf
            content: |
              [Service]
              Environment=DOCKER_OPTS='--insecure-registry="10.0.1.0/24"'
2014-11-26 14:09:35 -08:00
Alex Crawford
420f7cf202 system: clean up TestPlaceUnit() 2014-11-26 10:32:43 -08:00
Alex Crawford
624df676d0 config/unit: move Type() and Group() out of config 2014-11-26 10:32:43 -08:00
Alex Crawford
75ed8dacf9 initialize: clean up TestProcessUnits() 2014-11-26 10:32:43 -08:00
Alex Crawford
dcaabe4d4a system: clean up UnitManager interface 2014-11-26 10:32:43 -08:00
Alex Crawford
92c57423ba Merge pull request #269 from crawford/valid
validate: promote invalid values to an error
2014-11-26 10:32:27 -08:00
Alex Crawford
7447e133c9 validate: promote invalid values to an error 2014-11-26 10:29:09 -08:00
Eugene Yakubovich
4e466c12da Merge pull request #267 from thommay/flannel_unit
the flannel service is called flanneld
2014-11-25 12:30:58 -08:00
Thom May
333468dba3 the flannel service is called flanneld 2014-11-25 14:00:53 +00:00
Alex Crawford
55c3a793ad coreos-cloudinit: bump to 0.11.4+git 2014-11-21 20:11:54 -08:00
Alex Crawford
eca51031c8 coreos-cloudinit: bump to 0.11.4 2014-11-21 20:11:37 -08:00
Alex Crawford
19522bcb82 Merge pull request #266 from crawford/config
config: update configs to match etcd, fleet, and flannel
2014-11-21 20:10:34 -08:00
Alex Crawford
62248ea33d config/fleet: fix configs
Added EtcdKeyPrefix and fixed the types of EngineReconcileInterval and EtcdRequestTimeout.
2014-11-21 16:57:00 -08:00
Alex Crawford
d2a19cc86d config/flannel: correct - vs _ 2014-11-21 16:57:00 -08:00
Alex Crawford
08131ffab1 config/etcd: fix configs
This new table is pulled from the etcd codebase rather than the docs...

Added:
 GraphiteHost
 PeerElectionTimeout
 PeerHeartbeatInterval
 PeerKeyFile
 RetryInterval
 SnapshotCount
 StrTrace
 VeryVeryVerbose

Fixed types:
 ClusterActiveSize
 ClusterRemoveDelay
 ClusterSyncInterval
 HTTPReadTimeout
 HTTPWriteTimeout
 MaxResultBuffer
 MaxRetryAttempts
 Snapshot
 Verbose
 VeryVerbose

Renamed:
 Cors

Removed:
 MaxClusterSize
 CPUProfileFile
2014-11-21 16:57:00 -08:00
Alex Crawford
4a0019c669 config: add support for float64 2014-11-21 16:13:49 -08:00
Alex Crawford
3275ead1ec coreos-cloudinit: bump to 0.11.3+git 2014-11-21 12:25:26 -08:00
Alex Crawford
32b6a55724 coreos-cloudinit: bump to 0.11.3 2014-11-21 12:25:04 -08:00
Alex Crawford
6c43644369 Merge pull request #265 from crawford/update
config/update: add "off" as a valid strategy
2014-11-21 12:22:45 -08:00
Alex Crawford
e6593d49e6 config/update: add "off" as a valid strategy
It was assumed that the user would specify the reboot strategy as an
unquoted value. In the case that they turn off updates, `off` is
interpreted as a boolean and the normalization pass converts that to
`false`. In the event that the user uses `"off"`, it's interpreted as a
string and not modified.
2014-11-21 10:41:03 -08:00
Alex Crawford
ab752b239f coreos-cloudinit: bump to 0.11.2+git 2014-11-20 11:29:25 -08:00
Alex Crawford
0742e4d357 coreos-cloudinit: bump to 0.11.2 2014-11-20 11:29:12 -08:00
Alex Crawford
78f586ec9e Merge pull request #262 from crawford/permissions
config: fix parsing of file permissions
2014-11-20 11:28:11 -08:00
Alex Crawford
6f91b76d79 docs: correct type of permissions 2014-11-20 11:14:44 -08:00
Alex Crawford
5c80ccacc4 config: fix parsing of file permissions
The file permissions can be specified (unfortunately) as a string or an
octal integer. During the normalization step, every field is
unmarshalled into an interface{}. String types are kept in tact but
integers are converted to decimal integers. If the raw config
represented the permissions as an octal, it would be converted to
decimal _before_ it was saved to RawFilePermissions. Permissions() would
then try to convert it again, assuming it was an octal. The new behavior
doesn't assume the radix of the number, allowing decimal and octal
input.
2014-11-20 11:14:44 -08:00
Rob Szumski
44fdf95d99 docs: mention validate flag 2014-11-20 11:12:31 -08:00
Rob Szumski
0a62614eec docs: link to validator 2014-11-20 10:58:57 -08:00
Alex Crawford
97758b343b coreos-cloudinit: bump to 0.11.1+git 2014-11-18 12:14:34 -08:00
Alex Crawford
fb6f52b360 coreos-cloudinit: bump to 0.11.1 2014-11-18 12:14:29 -08:00
Alex Crawford
786cd2a539 Merge pull request #259 from crawford/hyphen
config/validate: disable - vs _ message for now
2014-11-18 12:12:26 -08:00
Alex Crawford
45793f1254 config/validate: disable - vs _ message for now 2014-11-18 12:11:50 -08:00
Alex Crawford
b621756d92 Merge pull request #258 from crawford/header
config/validate: fix line number for header check
2014-11-18 12:11:35 -08:00
Alex Crawford
a5b5c700a6 config/validate: fix line number for header check 2014-11-18 12:02:23 -08:00
Kiril Vladimirov
ea95920f31 fix(datasource/CloudSigma): Make sure DHCP has run 2014-11-17 15:35:10 +02:00
Alex Crawford
d7602f3c08 Merge pull request #244 from eyakubovich/master
flannel: added flannel support and helper to make dropins
2014-11-14 10:46:19 -08:00
Eugene Yakubovich
a20addd05e flannel: added flannel support and helper to make dropins
fleet, flannel, and etcd all generate dropins from config.
To reduce code duplication, factor out a helper to do that.
2014-11-14 10:45:23 -08:00
Alex Crawford
d9d89a6fa0 coreos-cloudinit: bump to 0.11.0+git 2014-11-14 10:42:00 -08:00
Kiril Vladimirov
b6062f0644 fix(datasource/CloudSigma): Populate local IPv4 address properly 2014-10-23 15:03:23 +03:00
Kiril Vladimirov
c5fada6e69 fix(datasource/CloudSigma): Populate public IPv4 address properly 2014-10-23 13:21:49 +03:00
34 changed files with 1023 additions and 453 deletions

View File

@@ -1,6 +1,8 @@
# Using Cloud-Config # 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. Your cloud-config is processed during each boot. 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. Invalid cloud-config won't be processed but will be logged in the journal. You can validate your cloud-config with the [CoreOS validator]({{site.url}}/validate) or by running `coreos-cloudinit -validate`.
## Configuration File ## Configuration File
@@ -16,7 +18,7 @@ We've designed our implementation to allow the same cloud-config file to work ac
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values. The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
A cloud-config file should contain `#cloud-config`, followed by an associative array which has zero or more of the following keys: A cloud-config file must contain `#cloud-config`, followed by an associative array which has zero or more of the following keys:
- `coreos` - `coreos`
- `ssh_authorized_keys` - `ssh_authorized_keys`
@@ -46,13 +48,13 @@ If the platform environment supports the templating feature of coreos-cloudinit
#cloud-config #cloud-config
coreos: coreos:
etcd: etcd:
name: node001 name: node001
# generate a new token for each unique cluster from https://discovery.etcd.io/new # generate a new token for each unique cluster from https://discovery.etcd.io/new
discovery: https://discovery.etcd.io/<token> discovery: https://discovery.etcd.io/<token>
# multi-region and multi-cloud deployments need to use $public_ipv4 # multi-region and multi-cloud deployments need to use $public_ipv4
addr: $public_ipv4:4001 addr: $public_ipv4:4001
peer-addr: $private_ipv4:7001 peer-addr: $private_ipv4:7001
``` ```
...will generate a systemd unit drop-in like this: ...will generate a systemd unit drop-in like this:
@@ -66,7 +68,6 @@ Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
``` ```
For more information about the available configuration parameters, see the [etcd documentation][etcd-config]. For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
_Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._ _Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._
@@ -80,9 +81,9 @@ The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allo
#cloud-config #cloud-config
coreos: coreos:
fleet: fleet:
public-ip: $public_ipv4 public-ip: $public_ipv4
metadata: region=us-west metadata: region=us-west
``` ```
...will generate a systemd unit drop-in like this: ...will generate a systemd unit drop-in like this:
@@ -97,6 +98,29 @@ For more information on fleet configuration, see the [fleet documentation][fleet
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/deployment-and-configuration.md#configuration [fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/deployment-and-configuration.md#configuration
#### flannel
The `coreos.flannel.*` parameters also work very similarly to `coreos.etcd.*` and `coreos.fleet.*`. They can be used to set enviornment variables for flanneld. Given the following cloud-config...
```yaml
#cloud-config
coreos:
flannel:
etcd-prefix: /coreos.com/network2
```
...will generate systemd unit drop-in like so:
```
[Service]
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network2"
```
For complete list of flannel configuraion parameters, see the [flannel documentation][flannel-readme].
[flannel-readme]: https://github.com/coreos/flannel/blob/master/README.md
#### update #### update
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated. The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
@@ -135,6 +159,10 @@ Each item is an object with the following fields:
- **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already. - **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already.
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands. - **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands.
- **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. The default value is false. - **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. The default value is false.
- **drop-ins**: A list of unit drop-ins with the following fields:
- **name**: String representing unit's name. Required.
- **content**: Plaintext string representing entire file. Required.
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place. **NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
@@ -146,19 +174,34 @@ Write a unit to disk, automatically starting it.
#cloud-config #cloud-config
coreos: coreos:
units: units:
- name: docker-redis.service - name: docker-redis.service
command: start command: start
content: | content: |
[Unit] [Unit]
Description=Redis container Description=Redis container
Author=Me Author=Me
After=docker.service After=docker.service
[Service] [Service]
Restart=always Restart=always
ExecStart=/usr/bin/docker start -a redis_server ExecStart=/usr/bin/docker start -a redis_server
ExecStop=/usr/bin/docker stop -t 2 redis_server ExecStop=/usr/bin/docker stop -t 2 redis_server
```
Add the DOCKER_OPTS environment variable to docker.service.
```yaml
#cloud-config
coreos:
units:
- name: docker.service
drop-ins:
- name: 50-insecure-registry.conf
content: |
[Service]
Environment=DOCKER_OPTS='--insecure-registry="10.0.1.0/24"'
``` ```
Start the built-in `etcd` and `fleet` services: Start the built-in `etcd` and `fleet` services:
@@ -167,11 +210,11 @@ Start the built-in `etcd` and `fleet` services:
#cloud-config #cloud-config
coreos: coreos:
units: units:
- name: etcd.service - name: etcd.service
command: start command: start
- name: fleet.service - name: fleet.service
command: start command: start
``` ```
### ssh_authorized_keys ### ssh_authorized_keys
@@ -303,7 +346,7 @@ Each item in the list may have the following keys:
- **path**: Absolute location on disk where contents should be written - **path**: Absolute location on disk where contents should be written
- **content**: Data to write at the provided `path` - **content**: Data to write at the provided `path`
- **permissions**: String representing file permissions in octal notation (i.e. '0644') - **permissions**: Integer representing file permissions, typically in octal notation (i.e. 0644)
- **owner**: User and group that should own the file written to disk. This is equivalent to the `<user>:<group>` argument to `chown <user>:<group> <path>`. - **owner**: User and group that should own the file written to disk. This is equivalent to the `<user>:<group>` argument to `chown <user>:<group> <path>`.
Explicitly not implemented is the **encoding** attribute. Explicitly not implemented is the **encoding** attribute.

View File

@@ -30,11 +30,12 @@ import (
type CloudConfig struct { type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"` SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct { Coreos struct {
Etcd Etcd `yaml:"etcd"` Etcd Etcd `yaml:"etcd"`
Fleet Fleet `yaml:"fleet"` Flannel Flannel `yaml:"flannel"`
OEM OEM `yaml:"oem"` Fleet Fleet `yaml:"fleet"`
Update Update `yaml:"update"` OEM OEM `yaml:"oem"`
Units []Unit `yaml:"units"` Update Update `yaml:"update"`
Units []Unit `yaml:"units"`
} `yaml:"coreos"` } `yaml:"coreos"`
WriteFiles []File `yaml:"write_files"` WriteFiles []File `yaml:"write_files"`
Hostname string `yaml:"hostname"` Hostname string `yaml:"hostname"`

View File

@@ -261,9 +261,6 @@ Address=10.209.171.177/19
if u.Name != "50-eth0.network" { if u.Name != "50-eth0.network" {
t.Errorf("Unit has incorrect name %s", u.Name) t.Errorf("Unit has incorrect name %s", u.Name)
} }
if u.Type() != "network" {
t.Errorf("Unit has incorrect type '%s'", u.Type())
}
} }
if cfg.Coreos.OEM.ID != "rackspace" { if cfg.Coreos.OEM.ID != "rackspace" {
@@ -276,6 +273,40 @@ Address=10.209.171.177/19
if cfg.Coreos.Update.RebootStrategy != "reboot" { if cfg.Coreos.Update.RebootStrategy != "reboot" {
t.Errorf("Failed to parse locksmith strategy") t.Errorf("Failed to parse locksmith strategy")
} }
contents = `
coreos:
write_files:
- path: /home/me/notes
permissions: 0744
`
cfg, err = NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error :%v", err)
}
if len(cfg.WriteFiles) != 1 {
t.Error("Failed to parse correct number of write_files")
} else {
wf := cfg.WriteFiles[0]
if wf.Content != "" {
t.Errorf("WriteFile has incorrect contents '%s'", wf.Content)
}
if wf.Encoding != "" {
t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding)
}
// Verify that the normalization of the config converted 0744 to its decimal
// representation, 484.
if wf.RawFilePermissions != "484" {
t.Errorf("WriteFile has incorrect permissions %s", wf.RawFilePermissions)
}
if wf.Path != "/home/me/notes" {
t.Errorf("WriteFile has incorrect path %s", wf.Path)
}
if wf.Owner != "" {
t.Errorf("WriteFile has incorrect owner %s", wf.Owner)
}
}
} }
// Assert that our interface conversion doesn't panic // Assert that our interface conversion doesn't panic
@@ -304,26 +335,6 @@ func TestCloudConfigSerializationHeader(t *testing.T) {
} }
} }
// TestDropInIgnored asserts that users are unable to set DropIn=True on units
func TestDropInIgnored(t *testing.T) {
contents := `
coreos:
units:
- name: test
dropin: true
`
cfg, err := NewCloudConfig(contents)
if err != nil || len(cfg.Coreos.Units) != 1 {
t.Fatalf("Encountered unexpected error: %v", err)
}
if len(cfg.Coreos.Units) != 1 || cfg.Coreos.Units[0].Name != "test" {
t.Fatalf("Expected 1 unit, but got %d: %v", len(cfg.Coreos.Units), cfg.Coreos.Units)
}
if cfg.Coreos.Units[0].DropIn {
t.Errorf("dropin option on unit in cloud-config was not ignored!")
}
}
func TestCloudConfigUsers(t *testing.T) { func TestCloudConfigUsers(t *testing.T) {
contents := ` contents := `
users: users:
@@ -479,6 +490,7 @@ func TestNormalizeKeys(t *testing.T) {
{"a:\n b:\n - key-name: the-value\n", "a:\n b:\n - key_name: the-value\n"}, {"a:\n b:\n - key-name: the-value\n", "a:\n b:\n - key_name: the-value\n"},
{"coreos:\n update:\n reboot-strategy: off\n", "coreos:\n update:\n reboot_strategy: false\n"}, {"coreos:\n update:\n reboot-strategy: off\n", "coreos:\n update:\n reboot_strategy: false\n"},
{"coreos:\n update:\n reboot-strategy: 'off'\n", "coreos:\n update:\n reboot_strategy: \"off\"\n"},
} { } {
out, err := normalizeConfig(tt.in) out, err := normalizeConfig(tt.in)
if err != nil { if err != nil {

View File

@@ -17,32 +17,37 @@
package config package config
type Etcd struct { type Etcd struct {
Addr string `yaml:"addr" env:"ETCD_ADDR"` Addr string `yaml:"addr" env:"ETCD_ADDR"`
BindAddr string `yaml:"bind_addr" env:"ETCD_BIND_ADDR"` BindAddr string `yaml:"bind_addr" env:"ETCD_BIND_ADDR"`
CAFile string `yaml:"ca_file" env:"ETCD_CA_FILE"` CAFile string `yaml:"ca_file" env:"ETCD_CA_FILE"`
CertFile string `yaml:"cert_file" env:"ETCD_CERT_FILE"` CertFile string `yaml:"cert_file" env:"ETCD_CERT_FILE"`
ClusterActiveSize string `yaml:"cluster_active_size" env:"ETCD_CLUSTER_ACTIVE_SIZE"` ClusterActiveSize int `yaml:"cluster_active_size" env:"ETCD_CLUSTER_ACTIVE_SIZE"`
ClusterRemoveDelay string `yaml:"cluster_remove_delay" env:"ETCD_CLUSTER_REMOVE_DELAY"` ClusterRemoveDelay float64 `yaml:"cluster_remove_delay" env:"ETCD_CLUSTER_REMOVE_DELAY"`
ClusterSyncInterval string `yaml:"cluster_sync_interval" env:"ETCD_CLUSTER_SYNC_INTERVAL"` ClusterSyncInterval float64 `yaml:"cluster_sync_interval" env:"ETCD_CLUSTER_SYNC_INTERVAL"`
Cors string `yaml:"cors" env:"ETCD_CORS"` CorsOrigins string `yaml:"cors" env:"ETCD_CORS"`
CPUProfileFile string `yaml:"cpu_profile_file" env:"ETCD_CPU_PROFILE_FILE"` DataDir string `yaml:"data_dir" env:"ETCD_DATA_DIR"`
DataDir string `yaml:"data_dir" env:"ETCD_DATA_DIR"` Discovery string `yaml:"discovery" env:"ETCD_DISCOVERY"`
Discovery string `yaml:"discovery" env:"ETCD_DISCOVERY"` GraphiteHost string `yaml:"graphite_host" env:"ETCD_GRAPHITE_HOST"`
HTTPReadTimeout string `yaml:"http_read_timeout" env:"ETCD_HTTP_READ_TIMEOUT"` HTTPReadTimeout float64 `yaml:"http_read_timeout" env:"ETCD_HTTP_READ_TIMEOUT"`
HTTPWriteTimeout string `yaml:"http_write_timeout" env:"ETCD_HTTP_WRITE_TIMEOUT"` HTTPWriteTimeout float64 `yaml:"http_write_timeout" env:"ETCD_HTTP_WRITE_TIMEOUT"`
KeyFile string `yaml:"key_file" env:"ETCD_KEY_FILE"` KeyFile string `yaml:"key_file" env:"ETCD_KEY_FILE"`
MaxClusterSize string `yaml:"max_cluster_size" env:"ETCD_MAX_CLUSTER_SIZE"` MaxResultBuffer int `yaml:"max_result_buffer" env:"ETCD_MAX_RESULT_BUFFER"`
MaxResultBuffer string `yaml:"max_result_buffer" env:"ETCD_MAX_RESULT_BUFFER"` MaxRetryAttempts int `yaml:"max_retry_attempts" env:"ETCD_MAX_RETRY_ATTEMPTS"`
MaxRetryAttempts string `yaml:"max_retry_attempts" env:"ETCD_MAX_RETRY_ATTEMPTS"` Name string `yaml:"name" env:"ETCD_NAME"`
Name string `yaml:"name" env:"ETCD_NAME"` PeerAddr string `yaml:"peer_addr" env:"ETCD_PEER_ADDR"`
PeerAddr string `yaml:"peer_addr" env:"ETCD_PEER_ADDR"` PeerBindAddr string `yaml:"peer_bind_addr" env:"ETCD_PEER_BIND_ADDR"`
PeerBindAddr string `yaml:"peer_bind_addr" env:"ETCD_PEER_BIND_ADDR"` PeerCAFile string `yaml:"peer_ca_file" env:"ETCD_PEER_CA_FILE"`
PeerCAFile string `yaml:"peer_ca_file" env:"ETCD_PEER_CA_FILE"` PeerCertFile string `yaml:"peer_cert_file" env:"ETCD_PEER_CERT_FILE"`
PeerCertFile string `yaml:"peer_cert_file" env:"ETCD_PEER_CERT_FILE"` PeerElectionTimeout int `yaml:"peer_election_timeout" env:"ETCD_PEER_ELECTION_TIMEOUT"`
PeerKeyFile string `yaml:"peer_key_file" env:"ETCD_PEER_KEY_FILE"` PeerHeartbeatInterval int `yaml:"peer_heartbeat_interval" env:"ETCD_PEER_HEARTBEAT_INTERVAL"`
Peers string `yaml:"peers" env:"ETCD_PEERS"` PeerKeyFile string `yaml:"peer_key_file" env:"ETCD_PEER_KEY_FILE"`
PeersFile string `yaml:"peers_file" env:"ETCD_PEERS_FILE"` Peers string `yaml:"peers" env:"ETCD_PEERS"`
Snapshot string `yaml:"snapshot" env:"ETCD_SNAPSHOT"` PeersFile string `yaml:"peers_file" env:"ETCD_PEERS_FILE"`
Verbose string `yaml:"verbose" env:"ETCD_VERBOSE"` RetryInterval float64 `yaml:"retry_interval" env:"ETCD_RETRY_INTERVAL"`
VeryVerbose string `yaml:"very_verbose" env:"ETCD_VERY_VERBOSE"` Snapshot bool `yaml:"snapshot" env:"ETCD_SNAPSHOT"`
SnapshotCount int `yaml:"snapshot_count" env:"ETCD_SNAPSHOTCOUNT"`
StrTrace string `yaml:"trace" env:"ETCD_TRACE"`
Verbose bool `yaml:"verbose" env:"ETCD_VERBOSE"`
VeryVerbose bool `yaml:"very_verbose" env:"ETCD_VERY_VERBOSE"`
VeryVeryVerbose bool `yaml:"very_very_verbose" env:"ETCD_VERY_VERY_VERBOSE"`
} }

9
config/flannel.go Normal file
View File

@@ -0,0 +1,9 @@
package config
type Flannel struct {
EtcdEndpoint string `yaml:"etcd_endpoint" env:"FLANNELD_ETCD_ENDPOINT"`
EtcdPrefix string `yaml:"etcd_prefix" env:"FLANNELD_ETCD_PREFIX"`
IPMasq string `yaml:"ip_masq" env:"FLANNELD_IP_MASQ"`
SubnetFile string `yaml:"subnet_file" env:"FLANNELD_SUBNET_FILE"`
Iface string `yaml:"interface" env:"FLANNELD_IFACE"`
}

View File

@@ -17,14 +17,15 @@
package config package config
type Fleet struct { type Fleet struct {
AgentTTL string `yaml:"agent_ttl" env:"FLEET_AGENT_TTL"` AgentTTL string `yaml:"agent_ttl" env:"FLEET_AGENT_TTL"`
EngineReconcileInterval string `yaml:"engine_reconcile_interval" env:"FLEET_ENGINE_RECONCILE_INTERVAL"` EngineReconcileInterval float64 `yaml:"engine_reconcile_interval" env:"FLEET_ENGINE_RECONCILE_INTERVAL"`
EtcdCAFile string `yaml:"etcd_cafile" env:"FLEET_ETCD_CAFILE"` EtcdCAFile string `yaml:"etcd_cafile" env:"FLEET_ETCD_CAFILE"`
EtcdCertFile string `yaml:"etcd_certfile" env:"FLEET_ETCD_CERTFILE"` EtcdCertFile string `yaml:"etcd_certfile" env:"FLEET_ETCD_CERTFILE"`
EtcdKeyFile string `yaml:"etcd_keyfile" env:"FLEET_ETCD_KEYFILE"` EtcdKeyFile string `yaml:"etcd_keyfile" env:"FLEET_ETCD_KEYFILE"`
EtcdRequestTimeout string `yaml:"etcd_request_timeout" env:"FLEET_ETCD_REQUEST_TIMEOUT"` EtcdKeyPrefix string `yaml:"etcd_key_prefix" env:"FLEET_ETCD_KEY_PREFIX"`
EtcdServers string `yaml:"etcd_servers" env:"FLEET_ETCD_SERVERS"` EtcdRequestTimeout float64 `yaml:"etcd_request_timeout" env:"FLEET_ETCD_REQUEST_TIMEOUT"`
Metadata string `yaml:"metadata" env:"FLEET_METADATA"` EtcdServers string `yaml:"etcd_servers" env:"FLEET_ETCD_SERVERS"`
PublicIP string `yaml:"public_ip" env:"FLEET_PUBLIC_IP"` Metadata string `yaml:"metadata" env:"FLEET_METADATA"`
Verbosity string `yaml:"verbosity" env:"FLEET_VERBOSITY"` PublicIP string `yaml:"public_ip" env:"FLEET_PUBLIC_IP"`
Verbosity int `yaml:"verbosity" env:"FLEET_VERBOSITY"`
} }

View File

@@ -16,35 +16,17 @@
package config package config
import (
"path/filepath"
"strings"
)
type Unit struct { type Unit struct {
Name string `yaml:"name"`
Mask bool `yaml:"mask"`
Enable bool `yaml:"enable"`
Runtime bool `yaml:"runtime"`
Content string `yaml:"content"`
Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"`
DropIns []UnitDropIn `yaml:"drop_ins"`
}
type UnitDropIn struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Mask bool `yaml:"mask"`
Enable bool `yaml:"enable"`
Runtime bool `yaml:"runtime"`
Content string `yaml:"content"` Content string `yaml:"content"`
Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"`
// For drop-in units, a cloudinit.conf is generated.
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
// until the correct behaviour for multiple drop-in units is determined.
DropIn bool `yaml:"-"`
}
func (u *Unit) Type() string {
ext := filepath.Ext(u.Name)
return strings.TrimLeft(ext, ".")
}
func (u *Unit) Group() string {
switch u.Type() {
case "network", "netdev", "link":
return "network"
default:
return "system"
}
} }

View File

@@ -17,7 +17,7 @@
package config package config
type Update struct { type Update struct {
RebootStrategy string `yaml:"reboot_strategy" env:"REBOOT_STRATEGY" valid:"best-effort,etcd-lock,reboot,false"` RebootStrategy string `yaml:"reboot_strategy" env:"REBOOT_STRATEGY" valid:"best-effort,etcd-lock,reboot,off,false"`
Group string `yaml:"group" env:"GROUP"` Group string `yaml:"group" env:"GROUP"`
Server string `yaml:"server" env:"SERVER"` Server string `yaml:"server" env:"SERVER"`
} }

View File

@@ -125,7 +125,7 @@ func toNode(v interface{}, c context, n *node) {
n.children = append(n.children, cn) n.children = append(n.children, cn)
c.Increment() c.Increment()
} }
case reflect.String, reflect.Int, reflect.Bool: case reflect.String, reflect.Int, reflect.Bool, reflect.Float64:
default: default:
panic(fmt.Sprintf("toNode(): unhandled kind %s", vv.Kind())) panic(fmt.Sprintf("toNode(): unhandled kind %s", vv.Kind()))
} }

View File

@@ -61,7 +61,7 @@ func checkNodeStructure(n, g node, r *Report) {
toNode(reflect.New(c).Elem().Interface(), context{}, &cg) toNode(reflect.New(c).Elem().Interface(), context{}, &cg)
checkNodeStructure(cn, cg, r) checkNodeStructure(cn, cg, r)
} }
case reflect.String, reflect.Int, reflect.Bool: case reflect.String, reflect.Int, reflect.Float64, reflect.Bool:
default: default:
panic(fmt.Sprintf("checkNodeStructure(): unhandled kind %s", g.Kind())) panic(fmt.Sprintf("checkNodeStructure(): unhandled kind %s", g.Kind()))
} }
@@ -76,7 +76,7 @@ func checkValidity(cfg node, report *Report) {
func checkNodeValidity(n, g node, r *Report) { func checkNodeValidity(n, g node, r *Report) {
if err := config.AssertValid(n.Value, g.field.Tag.Get("valid")); err != nil { if err := config.AssertValid(n.Value, g.field.Tag.Get("valid")); err != nil {
r.Warning(n.line, fmt.Sprintf("invalid value %v", n.Value)) r.Error(n.line, fmt.Sprintf("invalid value %v", n.Value))
} }
switch g.Kind() { switch g.Kind() {
case reflect.Struct: case reflect.Struct:
@@ -92,7 +92,7 @@ func checkNodeValidity(n, g node, r *Report) {
toNode(reflect.New(c).Elem().Interface(), context{}, &cg) toNode(reflect.New(c).Elem().Interface(), context{}, &cg)
checkNodeValidity(cn, cg, r) checkNodeValidity(cn, cg, r)
} }
case reflect.String, reflect.Int, reflect.Bool: case reflect.String, reflect.Int, reflect.Float64, reflect.Bool:
default: default:
panic(fmt.Sprintf("checkNodeValidity(): unhandled kind %s", g.Kind())) panic(fmt.Sprintf("checkNodeValidity(): unhandled kind %s", g.Kind()))
} }
@@ -104,10 +104,10 @@ func checkNodeValidity(n, g node, r *Report) {
func isCompatible(n, g reflect.Kind) bool { func isCompatible(n, g reflect.Kind) bool {
switch g { switch g {
case reflect.String: case reflect.String:
return n == reflect.String || n == reflect.Int || n == reflect.Bool return n == reflect.String || n == reflect.Int || n == reflect.Float64 || n == reflect.Bool
case reflect.Struct: case reflect.Struct:
return n == reflect.Struct || n == reflect.Map return n == reflect.Struct || n == reflect.Map
case reflect.Bool, reflect.Slice: case reflect.Bool, reflect.Slice, reflect.Int, reflect.Float64:
return n == g return n == g
default: default:
panic(fmt.Sprintf("isCompatible(): unhandled kind %s", g)) panic(fmt.Sprintf("isCompatible(): unhandled kind %s", g))

View File

@@ -218,7 +218,7 @@ func TestCheckValidity(t *testing.T) {
}, },
{ {
config: "coreos:\n units:\n - command: lol", config: "coreos:\n units:\n - command: lol",
entries: []Entry{{entryWarning, "invalid value lol", 3}}, entries: []Entry{{entryError, "invalid value lol", 3}},
}, },
// struct // struct
@@ -227,7 +227,7 @@ func TestCheckValidity(t *testing.T) {
}, },
{ {
config: "coreos:\n update:\n reboot_strategy: always", config: "coreos:\n update:\n reboot_strategy: always",
entries: []Entry{{entryWarning, "invalid value always", 3}}, entries: []Entry{{entryError, "invalid value always", 3}},
}, },
// unknown // unknown

View File

@@ -44,7 +44,7 @@ func Validate(userdataBytes []byte) (Report, error) {
return validateCloudConfig(userdataBytes, Rules) return validateCloudConfig(userdataBytes, Rules)
default: default:
return Report{entries: []Entry{ return Report{entries: []Entry{
Entry{kind: entryError, message: `must be "#cloud-config" or begin with "#!"`}, Entry{kind: entryError, message: `must be "#cloud-config" or begin with "#!"`, line: 1},
}}, nil }}, nil
} }
} }
@@ -103,7 +103,8 @@ func parseCloudConfig(config []byte, report *Report) (n node, err error) {
// and makes a note of each replacement in the report. // and makes a note of each replacement in the report.
func normalizeNodeNames(node node, report *Report) node { func normalizeNodeNames(node node, report *Report) node {
if strings.Contains(node.name, "-") { if strings.Contains(node.name, "-") {
report.Info(node.line, fmt.Sprintf("%q uses '-' instead of '_'", node.name)) // TODO(crawford): Enable this message once the new validator hits stable.
//report.Info(node.line, fmt.Sprintf("%q uses '-' instead of '_'", node.name))
node.name = strings.Replace(node.name, "-", "_", -1) node.name = strings.Replace(node.name, "-", "_", -1)
} }
for i := range node.children { for i := range node.children {

View File

@@ -40,7 +40,7 @@ import (
) )
const ( const (
version = "0.11.0" version = "1.0.1"
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

@@ -17,8 +17,12 @@
package cloudsigma package cloudsigma
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"io/ioutil"
"net"
"os" "os"
"strings" "strings"
@@ -51,7 +55,8 @@ func (_ *serverContextService) IsAvailable() bool {
} }
productName := make([]byte, 10) productName := make([]byte, 10)
_, err = productNameFile.Read(productName) _, err = productNameFile.Read(productName)
return err == nil && string(productName) == "CloudSigma"
return err == nil && string(productName) == "CloudSigma" && hasDHCPLeases()
} }
func (_ *serverContextService) AvailabilityChanges() bool { func (_ *serverContextService) AvailabilityChanges() bool {
@@ -73,12 +78,16 @@ func (scs *serverContextService) FetchMetadata() ([]byte, error) {
UUID string `json:"uuid"` UUID string `json:"uuid"`
Meta map[string]string `json:"meta"` Meta map[string]string `json:"meta"`
Nics []struct { Nics []struct {
Runtime struct { Mac string `json:"mac"`
IPv4Conf struct {
InterfaceType string `json:"interface_type"` InterfaceType string `json:"interface_type"`
IPv4 struct { IP struct {
IP string `json:"uuid"` UUID string `json:"uuid"`
} `json:"ip_v4"` } `json:"ip"`
} `json:"runtime"` } `json:"ip_v4_conf"`
VLAN struct {
UUID string `json:"uuid"`
} `json:"vlan"`
} `json:"nics"` } `json:"nics"`
} }
outputMetadata struct { outputMetadata struct {
@@ -112,11 +121,12 @@ func (scs *serverContextService) FetchMetadata() ([]byte, error) {
} }
for _, nic := range inputMetadata.Nics { for _, nic := range inputMetadata.Nics {
if nic.Runtime.IPv4.IP != "" { if nic.IPv4Conf.IP.UUID != "" {
if nic.Runtime.InterfaceType == "public" { outputMetadata.PublicIPv4 = nic.IPv4Conf.IP.UUID
outputMetadata.PublicIPv4 = nic.Runtime.IPv4.IP }
} else { if nic.VLAN.UUID != "" {
outputMetadata.LocalIPv4 = nic.Runtime.IPv4.IP if localIP, err := scs.findLocalIP(nic.Mac); err == nil {
outputMetadata.LocalIPv4 = localIP
} }
} }
} }
@@ -146,6 +156,36 @@ func (scs *serverContextService) FetchNetworkConfig(a string) ([]byte, error) {
return nil, nil return nil, nil
} }
func (scs *serverContextService) findLocalIP(mac string) (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
ifaceMac, err := net.ParseMAC(mac)
if err != nil {
return "", err
}
for _, iface := range ifaces {
if !bytes.Equal(iface.HardwareAddr, ifaceMac) {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
switch ip := addr.(type) {
case *net.IPNet:
if ip.IP.To4() != nil {
return ip.IP.To4().String(), nil
}
}
}
}
return "", errors.New("Local IP not found")
}
func isBase64Encoded(field string, userdata map[string]string) bool { func isBase64Encoded(field string, userdata map[string]string) bool {
base64Fields, ok := userdata["base64_fields"] base64Fields, ok := userdata["base64_fields"]
if !ok { if !ok {
@@ -159,3 +199,8 @@ func isBase64Encoded(field string, userdata map[string]string) bool {
} }
return false return false
} }
func hasDHCPLeases() bool {
files, err := ioutil.ReadDir("/run/systemd/netif/leases/")
return err == nil && len(files) > 0
}

View File

@@ -74,14 +74,41 @@ func TestServerContextFetchMetadata(t *testing.T) {
"name": "coreos", "name": "coreos",
"nics": [ "nics": [
{ {
"runtime": { "boot_order": null,
"interface_type": "public", "ip_v4_conf": {
"ip_v4": { "conf": "dhcp",
"ip": {
"gateway": "31.171.244.1",
"meta": {},
"nameservers": [
"178.22.66.167",
"178.22.71.56",
"8.8.8.8"
],
"netmask": 22,
"tags": [],
"uuid": "31.171.251.74" "uuid": "31.171.251.74"
}, }
"ip_v6": null
}, },
"ip_v6_conf": null,
"mac": "22:3d:09:6b:90:f3",
"model": "virtio",
"vlan": null "vlan": null
},
{
"boot_order": null,
"ip_v4_conf": null,
"ip_v6_conf": null,
"mac": "22:ae:4a:fb:8f:31",
"model": "virtio",
"vlan": {
"meta": {
"description": "",
"name": "CoreOS"
},
"tags": [],
"uuid": "5dec030e-25b8-4621-a5a4-a3302c9d9619"
}
} }
], ],
"smp": 2, "smp": 2,
@@ -106,12 +133,8 @@ func TestServerContextFetchMetadata(t *testing.T) {
t.Error("Public SSH Keys are not being read properly") t.Error("Public SSH Keys are not being read properly")
} }
if metadata.LocalIPv4 != "" {
t.Errorf("Local IP is not empty but %s instead", metadata.LocalIPv4)
}
if metadata.PublicIPv4 != "31.171.251.74" { if metadata.PublicIPv4 != "31.171.251.74" {
t.Errorf("Local IP is not 31.171.251.74 but %s instead", metadata.PublicIPv4) t.Errorf("Public IP is not 31.171.251.74 but %s instead", metadata.PublicIPv4)
} }
} }

View File

@@ -131,6 +131,7 @@ func Apply(cfg config.CloudConfig, env *Environment) error {
for _, ccu := range []CloudConfigUnit{ for _, ccu := range []CloudConfigUnit{
system.Etcd{Etcd: cfg.Coreos.Etcd}, system.Etcd{Etcd: cfg.Coreos.Etcd},
system.Fleet{Fleet: cfg.Coreos.Fleet}, system.Fleet{Fleet: cfg.Coreos.Fleet},
system.Flannel{Flannel: cfg.Coreos.Flannel},
system.Update{Update: cfg.Coreos.Update, ReadConfig: system.DefaultReadConfig}, system.Update{Update: cfg.Coreos.Update, ReadConfig: system.DefaultReadConfig},
} { } {
units = append(units, ccu.Units()...) units = append(units, ccu.Units()...)
@@ -194,66 +195,92 @@ func Apply(cfg config.CloudConfig, env *Environment) error {
// commands against units. It returns any error encountered. // commands against units. It returns any error encountered.
func processUnits(units []system.Unit, root string, um system.UnitManager) error { func processUnits(units []system.Unit, root string, um system.UnitManager) error {
type action struct { type action struct {
unit string unit system.Unit
command string command string
} }
actions := make([]action, 0, len(units)) actions := make([]action, 0, len(units))
reload := false reload := false
restartNetworkd := false
for _, unit := range units { for _, unit := range units {
dst := unit.Destination(root) if unit.Name == "" {
log.Printf("Skipping unit without name")
continue
}
if unit.Content != "" { if unit.Content != "" {
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) log.Printf("Writing unit %q to filesystem", unit.Name)
if err := um.PlaceUnit(&unit, dst); err != nil { if err := um.PlaceUnit(unit); err != nil {
return err return err
} }
log.Printf("Placed unit %s at %s", unit.Name, dst) log.Printf("Wrote unit %q", unit.Name)
reload = true reload = true
} }
for _, dropin := range unit.DropIns {
if dropin.Name != "" && dropin.Content != "" {
log.Printf("Writing drop-in unit %q to filesystem", dropin.Name)
if err := um.PlaceUnitDropIn(unit, dropin); err != nil {
return err
}
log.Printf("Wrote drop-in unit %q", dropin.Name)
reload = true
}
}
if unit.Mask { if unit.Mask {
log.Printf("Masking unit file %s", unit.Name) log.Printf("Masking unit file %q", unit.Name)
if err := um.MaskUnit(&unit); 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 %q is unmasked", unit.Name)
if err := um.UnmaskUnit(&unit); err != nil { if err := um.UnmaskUnit(unit); err != nil {
return err return err
} }
} }
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 %q", unit.Name)
if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil { if err := um.EnableUnitFile(unit); err != nil {
return err return err
} }
log.Printf("Enabled unit %s", unit.Name) log.Printf("Enabled unit %q", unit.Name)
} else { } else {
log.Printf("Skipping enable for network-like unit %s", unit.Name) log.Printf("Skipping enable for network-like unit %q", unit.Name)
} }
} }
if unit.Group() == "network" { if unit.Group() == "network" {
actions = append(actions, action{"systemd-networkd.service", "restart"}) restartNetworkd = true
} else if unit.Command != "" { } else if unit.Command != "" {
actions = append(actions, action{unit.Name, unit.Command}) actions = append(actions, action{unit, unit.Command})
} }
} }
if reload { if reload {
if err := um.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: %s", err))
} }
} }
for _, action := range actions { if restartNetworkd {
log.Printf("Calling unit command '%s %s'", action.command, action.unit) log.Printf("Restarting systemd-networkd")
res, err := um.RunUnitCommand(action.command, action.unit) networkd := system.Unit{Unit: config.Unit{Name: "systemd-networkd.service"}}
res, err := um.RunUnitCommand(networkd, "restart")
if err != nil { if err != nil {
return err return err
} }
log.Printf("Result of '%s %s': %s", action.command, action.unit, res) log.Printf("Restarted systemd-networkd (%s)", res)
}
for _, action := range actions {
log.Printf("Calling unit command %q on %q'", action.command, action.unit.Name)
res, err := um.RunUnitCommand(action.unit, action.command)
if err != nil {
return err
}
log.Printf("Result of %q on %q: %s", action.command, action.unit.Name, res)
} }
return nil return nil

View File

@@ -17,6 +17,7 @@
package initialize package initialize
import ( import (
"reflect"
"testing" "testing"
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
@@ -28,103 +29,192 @@ type TestUnitManager struct {
enabled []string enabled []string
masked []string masked []string
unmasked []string unmasked []string
commands map[string]string commands []UnitAction
reload bool reload bool
} }
func (tum *TestUnitManager) PlaceUnit(unit *system.Unit, dst string) error { type UnitAction struct {
tum.placed = append(tum.placed, unit.Name) unit string
command string
}
func (tum *TestUnitManager) PlaceUnit(u system.Unit) error {
tum.placed = append(tum.placed, u.Name)
return nil return nil
} }
func (tum *TestUnitManager) EnableUnitFile(unit string, runtime bool) error { func (tum *TestUnitManager) PlaceUnitDropIn(u system.Unit, d config.UnitDropIn) error {
tum.enabled = append(tum.enabled, unit) tum.placed = append(tum.placed, u.Name+".d/"+d.Name)
return nil return nil
} }
func (tum *TestUnitManager) RunUnitCommand(command, unit string) (string, error) { func (tum *TestUnitManager) EnableUnitFile(u system.Unit) error {
tum.commands = make(map[string]string) tum.enabled = append(tum.enabled, u.Name)
tum.commands[unit] = command return nil
}
func (tum *TestUnitManager) RunUnitCommand(u system.Unit, c string) (string, error) {
tum.commands = append(tum.commands, UnitAction{u.Name, c})
return "", nil return "", nil
} }
func (tum *TestUnitManager) DaemonReload() error { func (tum *TestUnitManager) DaemonReload() error {
tum.reload = true tum.reload = true
return nil return nil
} }
func (tum *TestUnitManager) MaskUnit(unit *system.Unit) error { func (tum *TestUnitManager) MaskUnit(u system.Unit) error {
tum.masked = append(tum.masked, unit.Name) tum.masked = append(tum.masked, u.Name)
return nil return nil
} }
func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error { func (tum *TestUnitManager) UnmaskUnit(u system.Unit) error {
tum.unmasked = append(tum.unmasked, unit.Name) tum.unmasked = append(tum.unmasked, u.Name)
return nil return nil
} }
func TestProcessUnits(t *testing.T) { func TestProcessUnits(t *testing.T) {
tum := &TestUnitManager{} tests := []struct {
units := []system.Unit{ units []system.Unit
system.Unit{Unit: config.Unit{
Name: "foo", result TestUnitManager
Mask: true, }{
}}, {
} units: []system.Unit{
if err := processUnits(units, "", tum); err != nil { system.Unit{Unit: config.Unit{
t.Fatalf("unexpected error calling processUnits: %v", err) Name: "foo",
} Mask: true,
if len(tum.masked) != 1 || tum.masked[0] != "foo" { }},
t.Errorf("expected foo to be masked, but found %v", tum.masked) },
result: TestUnitManager{
masked: []string{"foo"},
},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "baz.service",
Content: "[Service]\nExecStart=/bin/baz",
Command: "start",
}},
system.Unit{Unit: config.Unit{
Name: "foo.network",
Content: "[Network]\nFoo=true",
}},
system.Unit{Unit: config.Unit{
Name: "bar.network",
Content: "[Network]\nBar=true",
}},
},
result: TestUnitManager{
placed: []string{"baz.service", "foo.network", "bar.network"},
commands: []UnitAction{
UnitAction{"systemd-networkd.service", "restart"},
UnitAction{"baz.service", "start"},
},
reload: true,
},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "baz.service",
Content: "[Service]\nExecStart=/bin/true",
}},
},
result: TestUnitManager{
placed: []string{"baz.service"},
reload: true,
},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "locksmithd.service",
Runtime: true,
}},
},
result: TestUnitManager{
unmasked: []string{"locksmithd.service"},
},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "woof",
Enable: true,
}},
},
result: TestUnitManager{
enabled: []string{"woof"},
},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "hi.service",
Runtime: true,
Content: "[Service]\nExecStart=/bin/echo hi",
DropIns: []config.UnitDropIn{
{
Name: "lo.conf",
Content: "[Service]\nExecStart=/bin/echo lo",
},
{
Name: "bye.conf",
Content: "[Service]\nExecStart=/bin/echo bye",
},
},
}},
},
result: TestUnitManager{
placed: []string{"hi.service", "hi.service.d/lo.conf", "hi.service.d/bye.conf"},
unmasked: []string{"hi.service"},
reload: true,
},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
DropIns: []config.UnitDropIn{
{
Name: "lo.conf",
Content: "[Service]\nExecStart=/bin/echo lo",
},
},
}},
},
result: TestUnitManager{},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "hi.service",
DropIns: []config.UnitDropIn{
{
Content: "[Service]\nExecStart=/bin/echo lo",
},
},
}},
},
result: TestUnitManager{},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "hi.service",
DropIns: []config.UnitDropIn{
{
Name: "lo.conf",
},
},
}},
},
result: TestUnitManager{},
},
} }
tum = &TestUnitManager{} for _, tt := range tests {
units = []system.Unit{ tum := &TestUnitManager{}
system.Unit{Unit: config.Unit{ if err := processUnits(tt.units, "", tum); err != nil {
Name: "bar.network", t.Errorf("bad error (%+v): want nil, got %s", tt.units, err)
}}, }
} if !reflect.DeepEqual(tt.result, *tum) {
if err := processUnits(units, "", tum); err != nil { t.Errorf("bad result (%+v): want %+v, got %+v", tt.units, tt.result, tum)
t.Fatalf("unexpected error calling processUnits: %v", err) }
}
if _, ok := tum.commands["systemd-networkd.service"]; !ok {
t.Errorf("expected systemd-networkd.service to be reloaded!")
}
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{Unit: config.Unit{
Name: "baz.service",
Content: "[Service]\nExecStart=/bin/true",
}},
}
if err := processUnits(units, "", tum); err != nil {
t.Fatalf("unexpected error calling processUnits: %v", err)
}
if len(tum.placed) != 1 || tum.placed[0] != "baz.service" {
t.Fatalf("expected baz.service to be written, but got %v", tum.placed)
}
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{Unit: config.Unit{
Name: "locksmithd.service",
Runtime: true,
}},
}
if err := processUnits(units, "", tum); err != nil {
t.Fatalf("unexpected error calling processUnits: %v", err)
}
if len(tum.unmasked) != 1 || tum.unmasked[0] != "locksmithd.service" {
t.Fatalf("expected locksmithd.service to be unmasked, but got %v", tum.unmasked)
}
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{Unit: config.Unit{
Name: "woof",
Enable: true,
}},
}
if err := processUnits(units, "", tum); err != nil {
t.Fatalf("unexpected error calling processUnits: %v", err)
}
if len(tum.enabled) != 1 || tum.enabled[0] != "woof" {
t.Fatalf("expected woof to be enabled, but got %v", tum.enabled)
} }
} }

View File

@@ -19,19 +19,21 @@ package system
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/coreos/coreos-cloudinit/config"
) )
// dropinContents generates the contents for a drop-in unit given the config. // dropinContents generates the contents for a drop-in unit given the config.
// The argument must be a struct from the 'config' package. // The argument must be a struct from the 'config' package.
func dropinContents(e interface{}) string { func serviceContents(e interface{}) string {
et := reflect.TypeOf(e) et := reflect.TypeOf(e)
ev := reflect.ValueOf(e) ev := reflect.ValueOf(e)
var out string var out string
for i := 0; i < et.NumField(); i++ { for i := 0; i < et.NumField(); i++ {
if val := ev.Field(i).String(); val != "" { if val := ev.Field(i).Interface(); !config.IsZero(val) {
key := et.Field(i).Tag.Get("env") key := et.Field(i).Tag.Get("env")
out += fmt.Sprintf("Environment=\"%s=%s\"\n", key, val) out += fmt.Sprintf("Environment=\"%s=%v\"\n", key, val)
} }
} }

55
system/env_test.go Normal file
View File

@@ -0,0 +1,55 @@
package system
import (
"testing"
)
func TestServiceContents(t *testing.T) {
tests := []struct {
Config interface{}
Contents string
}{
{
struct{}{},
"",
},
{
struct {
A string `env:"A"`
B int `env:"B"`
C bool `env:"C"`
D float64 `env:"D"`
}{
"hi", 1, true, 0.12345,
},
`[Service]
Environment="A=hi"
Environment="B=1"
Environment="C=true"
Environment="D=0.12345"
`,
},
{
struct {
A float64 `env:"A"`
B float64 `env:"B"`
C float64 `env:"C"`
D float64 `env:"D"`
}{
0.000001, 1, 0.9999999, 0.1,
},
`[Service]
Environment="A=1e-06"
Environment="B=1"
Environment="C=0.9999999"
Environment="D=0.1"
`,
},
}
for _, tt := range tests {
if c := serviceContents(tt.Config); c != tt.Contents {
t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Contents, c)
}
}
}

View File

@@ -28,14 +28,12 @@ type Etcd struct {
// Units creates a Unit file drop-in for etcd, using any configured options. // Units creates a Unit file drop-in for etcd, using any configured options.
func (ee Etcd) Units() []Unit { func (ee Etcd) Units() []Unit {
content := dropinContents(ee.Etcd)
if content == "" {
return nil
}
return []Unit{{config.Unit{ return []Unit{{config.Unit{
Name: "etcd.service", Name: "etcd.service",
Runtime: true, Runtime: true,
DropIn: true, DropIns: []config.UnitDropIn{{
Content: content, Name: "20-cloudinit.conf",
Content: serviceContents(ee.Etcd),
}},
}}} }}}
} }

View File

@@ -30,7 +30,11 @@ func TestEtcdUnits(t *testing.T) {
}{ }{
{ {
config.Etcd{}, config.Etcd{},
nil, []Unit{{config.Unit{
Name: "etcd.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
}, },
{ {
config.Etcd{ config.Etcd{
@@ -40,11 +44,13 @@ func TestEtcdUnits(t *testing.T) {
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "etcd.service", Name: "etcd.service",
Runtime: true, Runtime: true,
DropIn: true, DropIns: []config.UnitDropIn{{
Content: `[Service] Name: "20-cloudinit.conf",
Content: `[Service]
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar" Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`, `,
}},
}}}, }}},
}, },
{ {
@@ -56,18 +62,20 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "etcd.service", Name: "etcd.service",
Runtime: true, Runtime: true,
DropIn: true, DropIns: []config.UnitDropIn{{
Content: `[Service] Name: "20-cloudinit.conf",
Content: `[Service]
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar" Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_NAME=node001" Environment="ETCD_NAME=node001"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`, `,
}},
}}}, }}},
}, },
} { } {
units := Etcd{tt.config}.Units() units := Etcd{tt.config}.Units()
if !reflect.DeepEqual(tt.units, units) { if !reflect.DeepEqual(tt.units, units) {
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.units, units) t.Errorf("bad units (%+v): want %#v, got %#v", tt.config, tt.units, units)
} }
} }
} }

View File

@@ -17,9 +17,9 @@
package system package system
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"os/exec" "os/exec"
"path" "path"
@@ -39,22 +39,23 @@ func (f *File) Permissions() (os.FileMode, error) {
return os.FileMode(0644), nil return os.FileMode(0644), nil
} }
// Parse string representation of file mode as octal // Parse string representation of file mode as integer
perm, err := strconv.ParseInt(f.RawFilePermissions, 8, 32) perm, err := strconv.ParseInt(f.RawFilePermissions, 0, 32)
if err != nil { if err != nil {
return 0, errors.New("Unable to parse file permissions as octal integer") return 0, fmt.Errorf("Unable to parse file permissions %q as integer", f.RawFilePermissions)
} }
return os.FileMode(perm), nil return os.FileMode(perm), nil
} }
func WriteFile(f *File, root string) (string, error) { func WriteFile(f *File, root string) (string, error) {
fullpath := path.Join(root, f.Path)
dir := path.Dir(fullpath)
log.Printf("Writing file to %q", fullpath)
if f.Encoding != "" { if f.Encoding != "" {
return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding) return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
} }
fullpath := path.Join(root, f.Path)
dir := path.Dir(fullpath)
if err := EnsureDirectoryExists(dir); err != nil { if err := EnsureDirectoryExists(dir); err != nil {
return "", err return "", err
} }
@@ -95,6 +96,7 @@ func WriteFile(f *File, root string) (string, error) {
return "", err return "", err
} }
log.Printf("Wrote file to %q", fullpath)
return fullpath, nil return fullpath, nil
} }

View File

@@ -85,6 +85,38 @@ func TestWriteFileInvalidPermission(t *testing.T) {
} }
} }
func TestDecimalFilePermissions(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fn := "foo"
fullPath := path.Join(dir, fn)
wf := File{config.File{
Path: fn,
RawFilePermissions: "484", // Decimal representation of 0744
}}
path, err := WriteFile(&wf, dir)
if err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
} else if path != fullPath {
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
}
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0744) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
}
func TestWriteFilePermissions(t *testing.T) { func TestWriteFilePermissions(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil { if err != nil {

24
system/flannel.go Normal file
View File

@@ -0,0 +1,24 @@
package system
import (
"github.com/coreos/coreos-cloudinit/config"
)
// flannel is a top-level structure which embeds its underlying configuration,
// config.Flannel, and provides the system-specific Unit().
type Flannel struct {
config.Flannel
}
// Units generates a Unit file drop-in for flannel, if any flannel options were
// configured in cloud-config
func (fl Flannel) Units() []Unit {
return []Unit{{config.Unit{
Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: serviceContents(fl.Flannel),
}},
}}}
}

46
system/flannel_test.go Normal file
View File

@@ -0,0 +1,46 @@
package system
import (
"reflect"
"testing"
"github.com/coreos/coreos-cloudinit/config"
)
func TestFlannelUnits(t *testing.T) {
for _, tt := range []struct {
config config.Flannel
units []Unit
}{
{
config.Flannel{},
[]Unit{{config.Unit{
Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
},
{
config.Flannel{
EtcdEndpoint: "http://12.34.56.78:4001",
EtcdPrefix: "/coreos.com/network/tenant1",
},
[]Unit{{config.Unit{
Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service]
Environment="FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001"
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1"
`,
}},
}}},
},
} {
units := Flannel{tt.config}.Units()
if !reflect.DeepEqual(units, tt.units) {
t.Errorf("bad units (%q): want %v, got %v", tt.config, tt.units, units)
}
}
}

View File

@@ -29,14 +29,12 @@ type Fleet struct {
// Units generates a Unit file drop-in for fleet, if any fleet options were // Units generates a Unit file drop-in for fleet, if any fleet options were
// configured in cloud-config // configured in cloud-config
func (fe Fleet) Units() []Unit { func (fe Fleet) Units() []Unit {
content := dropinContents(fe.Fleet)
if content == "" {
return nil
}
return []Unit{{config.Unit{ return []Unit{{config.Unit{
Name: "fleet.service", Name: "fleet.service",
Runtime: true, Runtime: true,
DropIn: true, DropIns: []config.UnitDropIn{{
Content: content, Name: "20-cloudinit.conf",
Content: serviceContents(fe.Fleet),
}},
}}} }}}
} }

View File

@@ -30,25 +30,31 @@ func TestFleetUnits(t *testing.T) {
}{ }{
{ {
config.Fleet{}, config.Fleet{},
nil, []Unit{{config.Unit{
Name: "fleet.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
}, },
{ {
config.Fleet{ config.Fleet{
PublicIP: "12.34.56.78", PublicIP: "12.34.56.78",
}, },
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "fleet.service", Name: "fleet.service",
Content: `[Service] Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service]
Environment="FLEET_PUBLIC_IP=12.34.56.78" Environment="FLEET_PUBLIC_IP=12.34.56.78"
`, `,
Runtime: true, }},
DropIn: true,
}}}, }}},
}, },
} { } {
units := Fleet{tt.config}.Units() units := Fleet{tt.config}.Units()
if !reflect.DeepEqual(units, tt.units) { if !reflect.DeepEqual(units, tt.units) {
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.units, units) t.Errorf("bad units (%+v): want %#v, got %#v", tt.config, tt.units, units)
} }
} }
} }

View File

@@ -96,7 +96,8 @@ func maybeProbeBonding(interfaces []network.InterfaceGenerator) error {
func restartNetworkd() error { func restartNetworkd() error {
log.Printf("Restarting networkd.service\n") log.Printf("Restarting networkd.service\n")
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service") networkd := Unit{config.Unit{Name: "systemd-networkd.service"}}
_, err := NewUnitManager("").RunUnitCommand(networkd, "restart")
return err return err
} }

View File

@@ -23,7 +23,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"strings" "strings"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/go-systemd/dbus" "github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/go-systemd/dbus"
@@ -42,49 +41,51 @@ type systemd struct {
// never be used as a true MachineID // never be used as a true MachineID
const fakeMachineID = "42000000000000000000000000000042" const fakeMachineID = "42000000000000000000000000000042"
// PlaceUnit writes a unit file at the provided destination, creating // PlaceUnit writes a unit file at its desired destination, creating parent
// parent directories as necessary. // directories as necessary.
func (s *systemd) PlaceUnit(u *Unit, dst string) error { func (s *systemd) PlaceUnit(u Unit) error {
dir := filepath.Dir(dst)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
return err
}
}
file := File{config.File{ file := File{config.File{
Path: filepath.Base(dst), Path: u.Destination(s.root),
Content: u.Content, Content: u.Content,
RawFilePermissions: "0644", RawFilePermissions: "0644",
}} }}
_, err := WriteFile(&file, dir) _, err := WriteFile(&file, "/")
if err != nil { return err
return err
}
return nil
} }
func (s *systemd) EnableUnitFile(unit string, runtime bool) error { // PlaceUnitDropIn writes a unit drop-in file at its desired destination,
// creating parent directories as necessary.
func (s *systemd) PlaceUnitDropIn(u Unit, d config.UnitDropIn) error {
file := File{config.File{
Path: u.DropInDestination(s.root, d),
Content: d.Content,
RawFilePermissions: "0644",
}}
_, err := WriteFile(&file, "/")
return err
}
func (s *systemd) EnableUnitFile(u Unit) error {
conn, err := dbus.New() conn, err := dbus.New()
if err != nil { if err != nil {
return err return err
} }
units := []string{unit} units := []string{u.Name}
_, _, err = conn.EnableUnitFiles(units, runtime, true) _, _, err = conn.EnableUnitFiles(units, u.Runtime, true)
return err return err
} }
func (s *systemd) RunUnitCommand(command, unit string) (string, error) { func (s *systemd) RunUnitCommand(u Unit, c string) (string, error) {
conn, err := dbus.New() conn, err := dbus.New()
if err != nil { if err != nil {
return "", err return "", err
} }
var fn func(string, string) (string, error) var fn func(string, string) (string, error)
switch command { switch c {
case "start": case "start":
fn = conn.StartUnit fn = conn.StartUnit
case "stop": case "stop":
@@ -100,10 +101,10 @@ func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
case "reload-or-try-restart": case "reload-or-try-restart":
fn = conn.ReloadOrTryRestartUnit fn = conn.ReloadOrTryRestartUnit
default: default:
return "", fmt.Errorf("Unsupported systemd command %q", command) return "", fmt.Errorf("Unsupported systemd command %q", c)
} }
return fn(unit, "replace") return fn(u.Name, "replace")
} }
func (s *systemd) DaemonReload() error { func (s *systemd) DaemonReload() error {
@@ -119,8 +120,8 @@ func (s *systemd) DaemonReload() error {
// /dev/null, analogous to `systemctl mask`. // /dev/null, analogous to `systemctl mask`.
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit // N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
// file at the location*, to ensure that the mask will succeed. // file at the location*, to ensure that the mask will succeed.
func (s *systemd) MaskUnit(unit *Unit) error { func (s *systemd) MaskUnit(u Unit) error {
masked := unit.Destination(s.root) masked := u.Destination(s.root)
if _, err := os.Stat(masked); os.IsNotExist(err) { if _, err := os.Stat(masked); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil { if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
return err return err
@@ -134,8 +135,8 @@ func (s *systemd) MaskUnit(unit *Unit) error {
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file // 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 // associated with the given Unit is empty or appears to be a symlink to
// /dev/null, it is removed. // /dev/null, it is removed.
func (s *systemd) UnmaskUnit(unit *Unit) error { func (s *systemd) UnmaskUnit(u Unit) error {
masked := unit.Destination(s.root) masked := u.Destination(s.root)
ne, err := nullOrEmpty(masked) ne, err := nullOrEmpty(masked)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil return nil

View File

@@ -17,6 +17,7 @@
package system package system
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
@@ -25,133 +26,109 @@ import (
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
) )
func TestPlaceNetworkUnit(t *testing.T) { func TestPlaceUnit(t *testing.T) {
u := Unit{config.Unit{ tests := []config.Unit{
Name: "50-eth0.network", {
Runtime: true, Name: "50-eth0.network",
Content: `[Match] Runtime: true,
Name=eth47 Content: "[Match]\nName=eth47\n\n[Network]\nAddress=10.209.171.177/19\n",
},
[Network] {
Address=10.209.171.177/19 Name: "media-state.mount",
`, Content: "[Mount]\nWhat=/dev/sdb1\nWhere=/media/state\n",
}} },
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
sd := &systemd{dir}
dst := u.Destination(dir)
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
if dst != expectDst {
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
if err := sd.PlaceUnit(&u, dst); err != nil { for _, tt := range tests {
t.Fatalf("PlaceUnit failed: %v", err) dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
} if err != nil {
panic(fmt.Sprintf("Unable to create tempdir: %v", err))
}
fi, err := os.Stat(dst) u := Unit{tt}
if err != nil { sd := &systemd{dir}
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) { if err := sd.PlaceUnit(u); err != nil {
t.Errorf("File has incorrect mode: %v", fi.Mode()) t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err)
} }
contents, err := ioutil.ReadFile(dst) fi, err := os.Stat(u.Destination(dir))
if err != nil { if err != nil {
t.Fatalf("Unable to read expected file: %v", err) t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err)
} }
expectContents := `[Match] if mode := fi.Mode(); mode != os.FileMode(0644) {
Name=eth47 t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode)
}
[Network] c, err := ioutil.ReadFile(u.Destination(dir))
Address=10.209.171.177/19 if err != nil {
` t.Fatalf("ReadFile(): bad error (%+v): want nil, got %s", tt, err)
if string(contents) != expectContents { }
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
if string(c) != tt.Content {
t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Content, string(c))
}
os.RemoveAll(dir)
} }
} }
func TestUnitDestination(t *testing.T) { func TestPlaceUnitDropIn(t *testing.T) {
dir := "/some/dir" tests := []config.Unit{
name := "foobar.service" {
Name: "false.service",
u := Unit{config.Unit{ Runtime: true,
Name: name, DropIns: []config.UnitDropIn{
DropIn: false, {
}} Name: "00-true.conf",
Content: "[Service]\nExecStart=\nExecStart=/usr/bin/true\n",
dst := u.Destination(dir) },
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service") },
if dst != expectDst { },
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst) {
Name: "true.service",
DropIns: []config.UnitDropIn{
{
Name: "00-false.conf",
Content: "[Service]\nExecStart=\nExecStart=/usr/bin/false\n",
},
},
},
} }
u.DropIn = true for _, tt := range tests {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
panic(fmt.Sprintf("Unable to create tempdir: %v", err))
}
dst = u.Destination(dir) u := Unit{tt}
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn) sd := &systemd{dir}
if dst != expectDst {
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
}
}
func TestPlaceMountUnit(t *testing.T) { if err := sd.PlaceUnitDropIn(u, u.DropIns[0]); err != nil {
u := Unit{config.Unit{ t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err)
Name: "media-state.mount", }
Runtime: false,
Content: `[Mount]
What=/dev/sdb1
Where=/media/state
`,
}}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") fi, err := os.Stat(u.DropInDestination(dir, u.DropIns[0]))
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err)
} }
defer os.RemoveAll(dir)
sd := &systemd{dir} if mode := fi.Mode(); mode != os.FileMode(0644) {
t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode)
}
dst := u.Destination(dir) c, err := ioutil.ReadFile(u.DropInDestination(dir, u.DropIns[0]))
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount") if err != nil {
if dst != expectDst { t.Fatalf("ReadFile(): bad error (%+v): want nil, got %s", tt, err)
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst) }
}
if err := sd.PlaceUnit(&u, dst); err != nil { if string(c) != u.DropIns[0].Content {
t.Fatalf("PlaceUnit failed: %v", err) t.Errorf("bad contents (%+v): want %q, got %q", tt, u.DropIns[0].Content, string(c))
} }
fi, err := os.Stat(dst) os.RemoveAll(dir)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(dst)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expectContents := `[Mount]
What=/dev/sdb1
Where=/media/state
`
if string(contents) != expectContents {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
} }
} }
@@ -180,7 +157,7 @@ func TestMaskUnit(t *testing.T) {
sd := &systemd{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{config.Unit{Name: "foo.service"}} uf := Unit{config.Unit{Name: "foo.service"}}
if err := sd.MaskUnit(uf); 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)
} }
@@ -194,7 +171,7 @@ func TestMaskUnit(t *testing.T) {
} }
// Ensure mask works with unit files that already exist // Ensure mask works with unit files that already exist
ub := &Unit{config.Unit{Name: "bar.service"}} ub := Unit{config.Unit{Name: "bar.service"}}
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service") barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
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)
@@ -220,12 +197,12 @@ func TestUnmaskUnit(t *testing.T) {
sd := &systemd{dir} sd := &systemd{dir}
nilUnit := &Unit{config.Unit{Name: "null.service"}} nilUnit := Unit{config.Unit{Name: "null.service"}}
if err := sd.UnmaskUnit(nilUnit); 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)
} }
uf := &Unit{config.Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}} uf := Unit{config.Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}}
dst := uf.Destination(dir) dst := uf.Destination(dir)
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil { if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
t.Fatalf("Unable to create unit directory: %v", err) t.Fatalf("Unable to create unit directory: %v", err)
@@ -245,7 +222,7 @@ func TestUnmaskUnit(t *testing.T) {
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly") t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
} }
ub := &Unit{config.Unit{Name: "bar.service"}} ub := Unit{config.Unit{Name: "bar.service"}}
dst = ub.Destination(dir) dst = ub.Destination(dir)
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)

View File

@@ -19,40 +19,66 @@ package system
import ( import (
"fmt" "fmt"
"path" "path"
"path/filepath"
"strings"
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
) )
// Name for drop-in service configuration files created by cloudconfig
const cloudConfigDropIn = "20-cloudinit.conf"
type UnitManager interface { type UnitManager interface {
PlaceUnit(unit *Unit, dst string) error PlaceUnit(unit Unit) error
EnableUnitFile(unit string, runtime bool) error PlaceUnitDropIn(unit Unit, dropIn config.UnitDropIn) error
RunUnitCommand(command, unit string) (string, error) EnableUnitFile(unit Unit) error
RunUnitCommand(unit Unit, command string) (string, error)
MaskUnit(unit Unit) error
UnmaskUnit(unit Unit) error
DaemonReload() error DaemonReload() error
MaskUnit(unit *Unit) error
UnmaskUnit(unit *Unit) error
} }
// Unit is a top-level structure which embeds its underlying configuration, // Unit is a top-level structure which embeds its underlying configuration,
// config.Unit, and provides the system-specific Destination(). // config.Unit, and provides the system-specific Destination(), Type(), and
// Group().
type Unit struct { type Unit struct {
config.Unit config.Unit
} }
// Destination builds the appropriate absolute file path for // Type returns the extension of the unit (everything that follows the final
// the Unit. The root argument indicates the effective base // period).
// directory of the system (similar to a chroot). func (u Unit) Type() string {
func (u *Unit) Destination(root string) string { ext := filepath.Ext(u.Name)
return strings.TrimLeft(ext, ".")
}
// Group returns "network" or "system" depending on whether or not the unit is
// a network unit or otherwise.
func (u Unit) Group() string {
switch u.Type() {
case "network", "netdev", "link":
return "network"
default:
return "system"
}
}
// Destination builds the appropriate absolute file path for the Unit. The root
// argument indicates the effective base directory of the system (similar to a
// chroot).
func (u Unit) Destination(root string) string {
return path.Join(u.prefix(root), u.Name)
}
// DropInDestination builds the appropriate absolute file path for the
// UnitDropIn. The root argument indicates the effective base directory of the
// system (similar to a chroot) and the dropIn argument is the UnitDropIn for
// which the destination is being calculated.
func (u Unit) DropInDestination(root string, dropIn config.UnitDropIn) string {
return path.Join(u.prefix(root), fmt.Sprintf("%s.d", u.Name), dropIn.Name)
}
func (u Unit) prefix(root string) string {
dir := "etc" dir := "etc"
if u.Runtime { if u.Runtime {
dir = "run" dir = "run"
} }
return path.Join(root, dir, "systemd", u.Group())
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)
}
} }

138
system/unit_test.go Normal file
View File

@@ -0,0 +1,138 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
"testing"
"github.com/coreos/coreos-cloudinit/config"
)
func TestType(t *testing.T) {
tests := []struct {
name string
typ string
}{
{},
{"test.service", "service"},
{"hello", ""},
{"lots.of.dots", "dots"},
}
for _, tt := range tests {
u := Unit{config.Unit{
Name: tt.name,
}}
if typ := u.Type(); tt.typ != typ {
t.Errorf("bad type (%+v): want %q, got %q", tt, tt.typ, typ)
}
}
}
func TestGroup(t *testing.T) {
tests := []struct {
name string
group string
}{
{"test.service", "system"},
{"test.link", "network"},
{"test.network", "network"},
{"test.netdev", "network"},
{"test.conf", "system"},
}
for _, tt := range tests {
u := Unit{config.Unit{
Name: tt.name,
}}
if group := u.Group(); tt.group != group {
t.Errorf("bad group (%+v): want %q, got %q", tt, tt.group, group)
}
}
}
func TestDestination(t *testing.T) {
tests := []struct {
root string
name string
runtime bool
destination string
}{
{
root: "/some/dir",
name: "foobar.service",
destination: "/some/dir/etc/systemd/system/foobar.service",
},
{
root: "/some/dir",
name: "foobar.service",
runtime: true,
destination: "/some/dir/run/systemd/system/foobar.service",
},
}
for _, tt := range tests {
u := Unit{config.Unit{
Name: tt.name,
Runtime: tt.runtime,
}}
if d := u.Destination(tt.root); tt.destination != d {
t.Errorf("bad destination (%+v): want %q, got %q", tt, tt.destination, d)
}
}
}
func TestDropInDestination(t *testing.T) {
tests := []struct {
root string
unitName string
dropInName string
runtime bool
destination string
}{
{
root: "/some/dir",
unitName: "foo.service",
dropInName: "bar.conf",
destination: "/some/dir/etc/systemd/system/foo.service.d/bar.conf",
},
{
root: "/some/dir",
unitName: "foo.service",
dropInName: "bar.conf",
runtime: true,
destination: "/some/dir/run/systemd/system/foo.service.d/bar.conf",
},
}
for _, tt := range tests {
u := Unit{config.Unit{
Name: tt.unitName,
Runtime: tt.runtime,
DropIns: []config.UnitDropIn{{
Name: tt.dropInName,
}},
}}
if d := u.DropInDestination(tt.root, u.DropIns[0]); tt.destination != d {
t.Errorf("bad destination (%+v): want %q, got %q", tt, tt.destination, d)
}
}
}

View File

@@ -126,7 +126,7 @@ func (uc Update) Units() []Unit {
Runtime: true, Runtime: true,
}} }}
if uc.Update.RebootStrategy == "false" { if uc.Update.RebootStrategy == "false" || uc.Update.RebootStrategy == "off" {
ls.Command = "stop" ls.Command = "stop"
ls.Mask = true ls.Mask = true
} }

View File

@@ -80,6 +80,15 @@ func TestUpdateUnits(t *testing.T) {
Mask: true, Mask: true,
}}}, }}},
}, },
{
config: config.Update{RebootStrategy: "off"},
units: []Unit{{config.Unit{
Name: "locksmithd.service",
Command: "stop",
Runtime: true,
Mask: true,
}}},
},
} { } {
units := Update{Update: tt.config, ReadConfig: testReadConfig("")}.Units() units := Update{Update: tt.config, ReadConfig: testReadConfig("")}.Units()
if !reflect.DeepEqual(tt.units, units) { if !reflect.DeepEqual(tt.units, units) {
@@ -100,7 +109,7 @@ func TestUpdateFile(t *testing.T) {
}, },
{ {
config: config.Update{RebootStrategy: "wizzlewazzle"}, config: config.Update{RebootStrategy: "wizzlewazzle"},
err: &config.ErrorValid{Value: "wizzlewazzle", Field: "RebootStrategy", Valid: []string{"best-effort", "etcd-lock", "reboot", "false"}}, err: &config.ErrorValid{Value: "wizzlewazzle", Field: "RebootStrategy", Valid: []string{"best-effort", "etcd-lock", "reboot", "off", "false"}},
}, },
{ {
config: config.Update{Group: "master", Server: "http://foo.com"}, config: config.Update{Group: "master", Server: "http://foo.com"},
@@ -142,6 +151,14 @@ func TestUpdateFile(t *testing.T) {
RawFilePermissions: "0644", RawFilePermissions: "0644",
}}, }},
}, },
{
config: config.Update{RebootStrategy: "off"},
file: &File{config.File{
Content: "REBOOT_STRATEGY=off\n",
Path: "etc/coreos/update.conf",
RawFilePermissions: "0644",
}},
},
{ {
config: config.Update{RebootStrategy: "etcd-lock"}, config: config.Update{RebootStrategy: "etcd-lock"},
orig: "SERVER=https://example.com\nGROUP=thegroupc\nREBOOT_STRATEGY=awesome", orig: "SERVER=https://example.com\nGROUP=thegroupc\nREBOOT_STRATEGY=awesome",