Compare commits

..

127 Commits

Author SHA1 Message Date
Alex Crawford
eadb6ef42c coreos-cloudinit: bump to 0.9.3 2014-08-15 10:46:46 -07:00
Alex Crawford
7518f0ec93 Merge pull request #204 from crawford/configdrive
configdrive: Remove broken support for ec2 metadata
2014-08-15 10:43:26 -07:00
Alex Crawford
f0b9eaf2fe configdrive: Remove broken support for ec2 metadata
As it turns out, certain metadata is only present in the ec2 flavor
of metadata (e.g. public_ipv4) and other data is only present in
the openstack flavor (e.g. network_config). For now, just read the
openstack metadata.
2014-08-15 10:35:21 -07:00
Jonathan Boulle
24b44e86a6 coreos-cloudinit: bump to 0.9.2+git 2014-08-12 11:38:51 -07:00
Jonathan Boulle
2f52ad4ef8 coreos-cloudinit: bump to 0.9.2 2014-08-12 11:38:12 -07:00
Jonathan Boulle
735d6c6161 Merge pull request #202 from jonboulle/env
environment: write new keys in consistent order
2014-08-11 22:40:42 -07:00
Alex Crawford
1cf275bad6 Merge pull request #201 from crawford/configdrive
configdrive: fix root path
2014-08-11 20:11:17 -07:00
Jonathan Boulle
f1c97cb4d5 environment: write new keys in consistent order 2014-08-11 18:24:58 -07:00
Alex Crawford
d143904aa9 configdrive: fix root path 2014-08-11 17:57:10 -07:00
Jonathan Boulle
c428ce2cc5 Merge pull request #200 from jonboulle/fu
initialize: use correct heuristic to check if etcdenvironment is set
2014-08-11 17:44:44 -07:00
Jonathan Boulle
dfb5b4fc3a initialize: use correct heuristic to check if etcdenvironment is set
In some circumstances (e.g. nova-agent-watcher) cloudconfig files will
be created where the EtcdEnvironment is an empty map, and hence != nil.
If this is the case we should not do anything at all (because the user
hasn't explicitly asked us to configure etcd). This change standardises
behaviour with the check that we already do for FleetEnvironment.
2014-08-11 16:01:08 -07:00
Alex Crawford
97d5538533 Merge pull request #197 from crawford/ec2
datasource: Fix ec2 URLs
2014-08-06 22:45:03 -07:00
Alex Crawford
6b8f82b5d3 datasource: Fix ec2 URLs
_ vs -
2014-08-06 21:31:43 -07:00
Alex Crawford
facde6609f Merge pull request #194 from crawford/metadata
datasource: Refactoring datasources
2014-08-06 15:55:13 -07:00
Alex Crawford
d68ae84b37 metadata: Refactor metadata service into ec2 metadata
Added more testing.
2014-08-05 17:19:43 -07:00
Alex Crawford
54aa39543b timeouts: Use After() instead of Tick() 2014-08-04 15:10:14 -07:00
Alex Crawford
8566a2c118 datasource: Move datasources into their own packages. 2014-08-04 15:10:07 -07:00
Alex Crawford
49ac083af5 coreos-cloudinit: bump to 0.9.1+git 2014-08-04 14:14:24 -07:00
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
Michael Marineau
b1a897d75c coreos-cloudinit: bump to 0.8.8 2014-07-11 11:12:15 -07:00
Jonathan Boulle
be51f4eba0 chore(*): split out unit processing from config.Apply 2014-07-11 10:44:19 -07:00
Michael Marineau
a55e2cd49b Merge pull request #178 from marineam/env
Write /etc/environment
2014-07-11 10:39:33 -07:00
Michael Marineau
983501e43b environment: add support for updating /etc/environment with IP values
To maintain the behavior of the coreos-setup-environment that has
started to move into cloudinit we need to write out /etc/environment
with the public and private addresses, if known. The file is updated so
that other contents are not replaced. This behavior is disabled entirely
if /etc/environment was written by a write_files entry.
2014-07-11 10:34:44 -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
Michael Marineau
81824be3bf system: new file writer for updating env-style files
This can be used to safely update config files cloudinit does not have
exclusive control over. For example update.conf or /etc/environment.
2014-07-10 15:53:32 -07:00
Michael Marineau
98c26440be Merge pull request #176 from jayofdoom/master
Document need for #cloud-config in cloud-config.yml
2014-07-09 16:41:00 -07:00
Jay Faulkner
3b5fcc393b Document need for #cloud-config in cloud-config.yml
- cloud-config.yml does not work if it's missing the #cloud-config
  directive at the top. This is undocumented, except in the examples.
2014-07-09 16:36:11 -07:00
Alex Crawford
9528077340 coreos-cloudinit: bump to 0.8.7+git 2014-07-02 15:20:45 -07:00
Alex Crawford
4355a05d55 coreos-cloudinit: bump to 0.8.7 2014-07-02 15:20:26 -07:00
Alex Crawford
52c44923dd Merge pull request #173 from crawford/metadata
metadata-service: remove check for OpenStack meta_data.json
2014-07-02 15:19:37 -07:00
Alex Crawford
47748ef4b6 metadata-service: remove check for OpenStack meta_data.json
The meta_data.json blob under OpenStack doesn't actually contain all
of the metadata... Fall back to explicitly requesting each attribute.
2014-07-02 14:38:23 -07:00
Alex Crawford
8eca10200e coreos-cloudinit: bump to 0.8.6+git 2014-07-01 16:17:00 -07:00
Alex Crawford
43be8c8996 coreos-cloudinit: bump to 0.8.6 2014-07-01 16:16:41 -07:00
Alex Crawford
19b4b1160e Merge pull request #171 from crawford/err
metadata-service: Handle no user-data
2014-07-01 16:15:32 -07:00
Alex Crawford
ce6fccfb3c metadata-service: Handle no user-data 2014-07-01 16:10:18 -07:00
Alex Crawford
7d89aefb82 coreos-cloudinit: bump to 0.8.5+git 2014-07-01 15:45:49 -07:00
Alex Crawford
2369e2a920 coreos-cloudinit: bump to 0.8.5 2014-07-01 15:45:23 -07:00
Alex Crawford
6d808048d3 Merge pull request #170 from crawford/metadata
metadata: Fetch the public and private IP addresses
2014-07-01 15:44:14 -07:00
Alex Crawford
276f0b5d99 metadata: Fetch the public and private IP addresses 2014-07-01 14:43:19 -07:00
Jonathan Boulle
92bd5ca5d4 coreos-cloudinit: bump to 0.8.4+git 2014-07-01 12:16:09 -07:00
Jonathan Boulle
5b5ffea126 coreos-cloudinit: bump to 0.8.4 2014-07-01 12:15:48 -07:00
Jonathan Boulle
18068e9375 Merge pull request #169 from jonboulle/pebkac
coreos-cloudinit: apply environment to userdata string
2014-07-01 12:15:06 -07:00
Jonathan Boulle
1b3cabb035 coreos-cloudinit: apply environment to userdata string 2014-07-01 12:08:42 -07:00
Jonathan Boulle
1be2bec1c2 coreos-cloudinit: bump to 0.8.3+git 2014-06-30 22:12:13 -07:00
Jonathan Boulle
f3bd5f543e coreos-cloudinit: bump to 0.8.3 2014-06-30 22:11:15 -07:00
Jonathan Boulle
660feb59b9 Merge pull request #168 from jonboulle/foo
fix ordering error in mergeCloudConfig
2014-06-30 22:08:47 -07:00
Jonathan Boulle
9673dbe12b coreos-cloudinit: fix ordering error in merge invocation 2014-06-30 22:07:05 -07:00
Alex Crawford
2be435dd83 coreos-cloudinit: bump to 0.8.2+git 2014-06-30 18:11:14 -07:00
Alex Crawford
2d91369596 coreos-cloudinit: bump to 0.8.2 2014-06-30 18:10:20 -07:00
Alex Crawford
d8d3928978 Merge pull request #167 from crawford/sshkeys
metadata-service: fix ssh key retrieval and application
2014-06-30 18:08:04 -07:00
Alex Crawford
7fcc540154 metadata-service: fix ssh key retrieval and application
The metadata service wasn't properly fetching the ssh keys from metadata.
Drop the key traversal in favor of explict key urls.
2014-06-30 17:45:08 -07:00
Jonathan Boulle
cb7fbd4668 Merge pull request #166 from jonboulle/merge
cloudinit: merge cloudconfig info from user-data and meta-data
2014-06-30 11:27:47 -07:00
Jonathan Boulle
d4e048a1f4 ParseUserData: return nil on empty input string 2014-06-30 11:27:33 -07:00
Jonathan Boulle
231c0fa20b initialize: add tests for ParseMetadata 2014-06-27 23:53:06 -07:00
Jonathan Boulle
1aabacc769 cloudinit: merge cloudconfig info from user-data and meta-data
This attempts to retrieve cloudconfigs from two sources: the meta-data
service, and the user-data service. If only one cloudconfig is found,
that is applied to the system. If both services return a cloudconfig,
the two are merged into a single cloudconfig which is then applied to
the system.

Only a subset of parameters are merged (because the meta-data service
currently only partially populates a cloudconfig). In the event of any
conflicts, parameters in the user-data cloudconfig take precedence over
those in the meta-data cloudconfig.
2014-06-27 23:48:48 -07:00
Alex Crawford
6a2927d701 coreos-cloudinit: bump to 0.8.1+git 2014-06-27 15:00:05 -07:00
Alex Crawford
126188510b coreos-cloudinit: bump to 0.8.1 2014-06-27 14:59:56 -07:00
Alex Crawford
4627ccb444 Merge pull request #165 from crawford/units
units: update dependencies
2014-06-27 14:55:48 -07:00
Alex Crawford
aff372111a units: update dependencies 2014-06-27 14:29:59 -07:00
Alex Crawford
c7081b9918 coreos-cloudinit: bump to 0.8.0+git 2014-06-27 11:33:56 -07:00
Alex Crawford
9ba3b18b59 coreos-cloudinit: bump to 0.8.0 2014-06-27 11:32:52 -07:00
Alex Crawford
099de62e9a Merge pull request #164 from crawford/datasources
datasources: Add support for specifying multiple datasources
2014-06-27 00:25:09 -07:00
Alex Crawford
c089216cb5 datasources: Add support for specifying multiple datasources
If multiple sources are specified, the first available source is used.
2014-06-26 22:32:39 -07:00
Alex Crawford
68dc902ed1 HttpClient: Refactor timeout into two seperate functions 2014-06-26 15:16:22 -07:00
Jonathan Boulle
ad66b1c92f Merge pull request #163 from jonboulle/net
coreos-cloudinit: restrict convert-netconf to configdrive
2014-06-25 14:35:27 -07:00
Jonathan Boulle
fbdece2762 coreos-cloudinit: restrict convert-netconf to configdrive 2014-06-25 14:28:11 -07:00
Jonathan Boulle
f85eafb7ca Merge pull request #162 from jonboulle/fffffffffffffffffff
initialize/env: handle nil substitution maps properly
2014-06-25 12:10:00 -07:00
Jonathan Boulle
f0dba2294e initialize/env: handle nil substitution maps properly 2014-06-25 12:07:48 -07:00
Alex Crawford
bda3948382 Merge pull request #159 from crawford/metadata
metadataService: Check both ec2 and openstack urls more explicitly
2014-06-25 11:26:19 -07:00
Alex Crawford
fae81c78f3 metadataService: Check both ec2 and openstack urls more explicitly
Remove the root url parameter for -from-metadata-service since this
is a guarenteed value. Additionally, check for both ec2 and openstack
urls for the metadata and userdata. Fix a bug with the -from-url
option and a panic on an empty response.
2014-06-25 11:19:11 -07:00
Alex Crawford
a5dec7d7bd cloudconfig: Process metadata before userdata
This gives the options in userdata a higher precedence over metadata.
2014-06-25 10:35:26 -07:00
Jonathan Boulle
e1222c9885 Merge pull request #161 from jonboulle/doc
metadata: add links to metadata source information
2014-06-25 08:55:26 -07:00
Jonathan Boulle
ded3bcf122 metadata: add links to metadata source information 2014-06-24 19:26:07 -07:00
Jonathan Boulle
80d00cde94 Merge pull request #158 from jonboulle/nettttt
cloudinit: retrieve IPv4 addresses from metadata
2014-06-24 19:11:15 -07:00
Jonathan Boulle
2805d70ece initialize/env: add notes about tests 2014-06-24 18:52:08 -07:00
Jonathan Boulle
439b7e8b98 initialize/env: fall back to COREOS_*_IPV4 env variables 2014-06-24 18:49:49 -07:00
Jonathan Boulle
ba1c1e97d0 cloudinit: retrieve IPv4 addresses from metadata
This uses the new MetadataService implementation to retrieve values for
$private_ipv4 and $public_ipv4 substitutions, instead of using
environment variables.
2014-06-24 17:46:06 -07:00
Alex Crawford
8a50fd8595 Merge pull request #154 from crawford/metadata
metadata-service: Add new datasource to download from metadata service
2014-06-24 15:18:27 -07:00
Alex Crawford
465bcce72c metadata_service: Add tests for constructing metadata 2014-06-24 15:08:03 -07:00
Alex Crawford
361edeebc6 metadata-service: Add metadata-service datasource
Move the old metadata-service datasource to url datasource. This new datasource
checks for the existance of meta-data.json and if it doesn't exist, walks the
meta-data directory to build a metadata blob.
2014-06-24 15:08:03 -07:00
Jonathan Boulle
29a7b0e34f Merge pull request #155 from jonboulle/etcd
etcdenvironment: order map keys consistently
2014-06-24 15:02:04 -07:00
Alex Crawford
8496ffb53a HttpClient: Wrap errors with error classes 2014-06-24 15:01:31 -07:00
Jonathan Boulle
2c717a6cd1 Merge pull request #157 from jonboulle/core
coreos-cloudinit: clean up flag handling
2014-06-24 14:57:18 -07:00
Jonathan Boulle
13a91c9181 coreos-cloudinit: clean up flag handling 2014-06-24 14:19:55 -07:00
Jonathan Boulle
338e1b64ab etcdenvironment: order map keys consistently 2014-06-23 15:13:11 -07:00
Alex Crawford
8eb0636034 Merge pull request #149 from crawford/network
feat(network): Add support for blind interfaces, support for hwaddress, and bug fixes
2014-06-20 17:53:37 -07:00
Alex Crawford
f7c25a1b83 doc(debian-interfaces): Add basic docs for convert-netconf 2014-06-20 17:51:57 -07:00
Alex Crawford
d6a0d0908c fix(network): Generate prefixes to ensure proper lexicographical ordering
In order for networkd to properly configure the network interfaces, the configs must be
prefixed to ensure that they load in the correct order (parent interfaces have a lower
prefix than their children).
2014-06-20 17:51:57 -07:00
Alex Crawford
5c89afc18a Merge pull request #152 from crawford/metadata
feat(meta_data): Add partial support for meta_data.json
2014-06-20 17:47:08 -07:00
Michael Marineau
376cc4bcac chore(coreos-cloudinit): bump to 0.7.7+git 2014-06-18 15:01:13 -07:00
Alex Crawford
e6cf83a2e5 refactor(netconf): Move netconf processing and handle metadata 2014-06-18 12:43:41 -07:00
Alex Crawford
840c208b60 feat(metadata): Distinguish between userdata and metadata for datasources 2014-06-18 12:34:31 -07:00
Alex Crawford
29ed6b38bd refactor(env): Add the config root and netconf type to datasource and env 2014-06-18 12:27:15 -07:00
Alex Crawford
259c7e1fe2 fix(sshKeyName): Use the SSH key name provided 2014-06-18 11:47:17 -07:00
Alex Crawford
033c8d352f feat(network): Add support for hwaddress
Currently only supports the ether mode of hwaddress. No immediate plans
to support ax25, ARCnet, or netrom.
2014-06-14 21:30:14 -07:00
Alex Crawford
16d7e8af48 fix(network): Take down all interfaces properly
The map of interfaces wasn't being populated correctly. Also, clean up some prints.
2014-06-13 20:53:59 -07:00
Alex Crawford
159f4a2c7c feat(network): Add support for blind interfaces
It is valid for an interface to reference another, otherwise undeclared,
interface (i.e. a bond enslaves eth0 without eth0 having its own iface stanza).
In order to associate the two interfaces, the undeclared interface needs to be
implicitly created so that it can be referenced by the other. This adds the
capability to forward-declare interfaces in addition to cleaning up the
process a little bit.
2014-06-11 22:07:33 -07:00
43 changed files with 3033 additions and 645 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
@@ -16,7 +16,7 @@ We've designed our implementation to allow the same cloud-config file to work ac
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
A cloud-config file should contain an associative array which has zero or more of the following keys:
A cloud-config file should contain `#cloud-config`, followed by an associative array which has zero or more of the following keys:
- `coreos`
- `ssh_authorized_keys`
@@ -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 \
-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...]
```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

@@ -0,0 +1,27 @@
#Debian Interfaces#
**WARNING**: This option is EXPERIMENTAL and may change or be removed at any
point.
There is basic support for converting from a Debian network configuration to
networkd unit files. The -convert-netconf=debian option is used to activate
this feature.
#convert-netconf#
Default: ""
Read the network config provided in cloud-drive and translate it from the
specified format into networkd unit files (requires the -from-configdrive
flag). Currently only supports "debian" which provides support for a small
subset of the [Debian network configuration]
(https://wiki.debian.org/NetworkConfiguration). These options include:
- interface config methods
- static
- address/netmask
- gateway
- hwaddress
- dns-nameservers
- dhcp
- hwaddress
- manual
- loopback
- vlan_raw_device
- bond-slaves

View File

@@ -1,71 +1,76 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"path"
"sync"
"time"
"github.com/coreos/coreos-cloudinit/datasource"
"github.com/coreos/coreos-cloudinit/datasource/configdrive"
"github.com/coreos/coreos-cloudinit/datasource/file"
"github.com/coreos/coreos-cloudinit/datasource/metadata/ec2"
"github.com/coreos/coreos-cloudinit/datasource/proc_cmdline"
"github.com/coreos/coreos-cloudinit/datasource/url"
"github.com/coreos/coreos-cloudinit/initialize"
"github.com/coreos/coreos-cloudinit/network"
"github.com/coreos/coreos-cloudinit/pkg"
"github.com/coreos/coreos-cloudinit/system"
)
const version = "0.7.7"
const (
version = "0.9.3"
datasourceInterval = 100 * time.Millisecond
datasourceMaxInterval = 30 * time.Second
datasourceTimeout = 5 * time.Minute
)
var (
printVersion bool
ignoreFailure bool
sources struct {
file string
configDrive string
metadataService bool
ec2MetadataService string
url string
procCmdLine bool
}
convertNetconf string
workspace string
sshKeyName string
)
func init() {
flag.BoolVar(&printVersion, "version", false, "Print the version and exit")
flag.BoolVar(&ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")
flag.StringVar(&sources.file, "from-file", "", "Read user-data from provided file")
flag.StringVar(&sources.configDrive, "from-configdrive", "", "Read data from provided cloud-drive directory")
flag.BoolVar(&sources.metadataService, "from-metadata-service", false, "[DEPRECATED - Use -from-ec2-metadata] Download data from metadata service")
flag.StringVar(&sources.ec2MetadataService, "from-ec2-metadata", "", "Download data from the provided metadata service")
flag.StringVar(&sources.url, "from-url", "", "Download user-data from provided url")
flag.BoolVar(&sources.procCmdLine, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=<url>', using the cloud-config served by an HTTP GET to <url>", proc_cmdline.ProcCmdlineLocation, proc_cmdline.ProcCmdlineCloudConfigFlag))
flag.StringVar(&convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files (requires the -from-configdrive flag)")
flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
}
func main() {
var printVersion bool
flag.BoolVar(&printVersion, "version", false, "Print the version and exit")
var ignoreFailure bool
flag.BoolVar(&ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")
var file string
flag.StringVar(&file, "from-file", "", "Read user-data from provided file")
var configdrive string
flag.StringVar(&configdrive, "from-configdrive", "", "Read user-data from provided cloud-drive directory")
var url string
flag.StringVar(&url, "from-url", "", "Download user-data from provided url")
var useProcCmdline bool
flag.BoolVar(&useProcCmdline, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=<url>', using the cloud-config served by an HTTP GET to <url>", datasource.ProcCmdlineLocation, datasource.ProcCmdlineCloudConfigFlag))
var convertNetconf string
flag.StringVar(&convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files (requires the -from-configdrive flag)")
var workspace string
flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
var sshKeyName string
flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
flag.Parse()
die := func() {
if ignoreFailure {
os.Exit(0)
}
os.Exit(1)
}
if printVersion == true {
fmt.Printf("coreos-cloudinit version %s\n", version)
os.Exit(0)
}
var ds datasource.Datasource
if file != "" {
ds = datasource.NewLocalFile(file)
} else if url != "" {
ds = datasource.NewMetadataService(url)
} else if configdrive != "" {
ds = datasource.NewConfigDrive(configdrive)
} else if useProcCmdline {
ds = datasource.NewProcCmdline()
} else {
fmt.Println("Provide one of --from-file, --from-configdrive, --from-url or --from-proc-cmdline")
os.Exit(1)
}
if convertNetconf != "" && configdrive == "" {
if convertNetconf != "" && sources.configDrive == "" {
fmt.Println("-convert-netconf flag requires -from-configdrive")
os.Exit(1)
}
@@ -78,111 +83,209 @@ func main() {
os.Exit(1)
}
dss := getDatasources()
if len(dss) == 0 {
fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --from-url or --from-proc-cmdline")
os.Exit(1)
}
ds := selectDatasource(dss)
if ds == nil {
fmt.Println("No datasources available in time")
die()
}
fmt.Printf("Fetching user-data from datasource of type %q\n", ds.Type())
userdataBytes, err := ds.Fetch()
userdataBytes, err := ds.FetchUserdata()
if err != nil {
fmt.Printf("Failed fetching user-data from datasource: %v\n", err)
if ignoreFailure {
os.Exit(0)
} else {
os.Exit(1)
die()
}
fmt.Printf("Fetching meta-data from datasource of type %q\n", ds.Type())
metadataBytes, err := ds.FetchMetadata()
if err != nil {
fmt.Printf("Failed fetching meta-data from datasource: %v\n", err)
die()
}
// Extract IPv4 addresses from metadata if possible
var subs map[string]string
if len(metadataBytes) > 0 {
subs, err = initialize.ExtractIPsFromMetadata(metadataBytes)
if err != nil {
fmt.Printf("Failed extracting IPs from meta-data: %v\n", err)
die()
}
}
env := initialize.NewEnvironment("/", workspace)
if len(userdataBytes) > 0 {
if err := processUserdata(string(userdataBytes), env); err != nil {
fmt.Printf("Failed resolving user-data: %v\n", err)
if !ignoreFailure {
os.Exit(1)
}
}
// Apply environment to user-data
env := initialize.NewEnvironment("/", ds.ConfigRoot(), workspace, convertNetconf, sshKeyName, subs)
userdata := env.Apply(string(userdataBytes))
var ccm, ccu *initialize.CloudConfig
var script *system.Script
if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil {
fmt.Printf("Failed to parse meta-data: %v\n", err)
die()
}
if ud, err := initialize.ParseUserData(userdata); err != nil {
fmt.Printf("Failed to parse user-data: %v\n", err)
die()
} else {
fmt.Println("No user data to handle.")
switch t := ud.(type) {
case *initialize.CloudConfig:
ccu = t
case system.Script:
script = &t
}
}
if convertNetconf != "" {
if err := processNetconf(convertNetconf, configdrive); err != nil {
fmt.Printf("Failed to process network config: %v\n", err)
if !ignoreFailure {
os.Exit(1)
}
var cc *initialize.CloudConfig
if ccm != nil && ccu != nil {
fmt.Println("Merging cloud-config from meta-data and user-data")
merged := mergeCloudConfig(*ccm, *ccu)
cc = &merged
} else if ccm != nil && ccu == nil {
fmt.Println("Processing cloud-config from meta-data")
cc = ccm
} else if ccm == nil && ccu != nil {
fmt.Println("Processing cloud-config from user-data")
cc = ccu
} else {
fmt.Println("No cloud-config data to handle.")
}
if cc != nil {
if err = initialize.Apply(*cc, env); err != nil {
fmt.Printf("Failed to apply cloud-config: %v\n", err)
die()
}
}
if script != nil {
if err = runScript(*script, env); err != nil {
fmt.Printf("Failed to run script: %v\n", err)
die()
}
}
}
func processUserdata(userdata string, env *initialize.Environment) error {
userdata = env.Apply(userdata)
// mergeCloudConfig merges certain options from mdcc (a CloudConfig derived from
// meta-data) onto udcc (a CloudConfig derived from user-data), if they are
// not already set on udcc (i.e. user-data always takes precedence)
// NB: This needs to be kept in sync with ParseMetadata so that it tracks all
// elements of a CloudConfig which that function can populate.
func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudConfig) {
if mdcc.Hostname != "" {
if udcc.Hostname != "" {
fmt.Printf("Warning: user-data hostname (%s) overrides metadata hostname (%s)\n", udcc.Hostname, mdcc.Hostname)
} else {
udcc.Hostname = mdcc.Hostname
}
parsed, err := initialize.ParseUserData(userdata)
if err != nil {
fmt.Printf("Failed parsing user-data: %v\n", err)
return err
}
for _, key := range mdcc.SSHAuthorizedKeys {
udcc.SSHAuthorizedKeys = append(udcc.SSHAuthorizedKeys, key)
}
if mdcc.NetworkConfigPath != "" {
if udcc.NetworkConfigPath != "" {
fmt.Printf("Warning: user-data NetworkConfigPath %s overrides metadata NetworkConfigPath %s\n", udcc.NetworkConfigPath, mdcc.NetworkConfigPath)
} else {
udcc.NetworkConfigPath = mdcc.NetworkConfigPath
}
}
return udcc
}
// getDatasources creates a slice of possible Datasources for cloudinit based
// on the different source command-line flags.
func getDatasources() []datasource.Datasource {
dss := make([]datasource.Datasource, 0, 5)
if sources.file != "" {
dss = append(dss, file.NewDatasource(sources.file))
}
if sources.url != "" {
dss = append(dss, url.NewDatasource(sources.url))
}
if sources.configDrive != "" {
dss = append(dss, configdrive.NewDatasource(sources.configDrive))
}
if sources.metadataService {
dss = append(dss, ec2.NewDatasource(ec2.DefaultAddress))
}
if sources.ec2MetadataService != "" {
dss = append(dss, ec2.NewDatasource(sources.ec2MetadataService))
}
if sources.procCmdLine {
dss = append(dss, proc_cmdline.NewDatasource())
}
return dss
}
// selectDatasource attempts to choose a valid Datasource to use based on its
// current availability. The first Datasource to report to be available is
// returned. Datasources will be retried if possible if they are not
// immediately available. If all Datasources are permanently unavailable or
// datasourceTimeout is reached before one becomes available, nil is returned.
func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
ds := make(chan datasource.Datasource)
stop := make(chan struct{})
var wg sync.WaitGroup
for _, s := range sources {
wg.Add(1)
go func(s datasource.Datasource) {
defer wg.Done()
duration := datasourceInterval
for {
fmt.Printf("Checking availability of %q\n", s.Type())
if s.IsAvailable() {
ds <- s
return
} else if !s.AvailabilityChanges() {
return
}
select {
case <-stop:
return
case <-time.After(duration):
duration = pkg.ExpBackoff(duration, datasourceMaxInterval)
}
}
}(s)
}
err = initialize.PrepWorkspace(env.Workspace())
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
var s datasource.Datasource
select {
case s = <-ds:
case <-done:
case <-time.After(datasourceTimeout):
}
close(stop)
return s
}
// TODO(jonboulle): this should probably be refactored and moved into a different module
func runScript(script system.Script, env *initialize.Environment) error {
err := initialize.PrepWorkspace(env.Workspace())
if err != nil {
fmt.Printf("Failed preparing workspace: %v\n", err)
return err
}
switch t := parsed.(type) {
case initialize.CloudConfig:
err = initialize.Apply(t, env)
case system.Script:
var path string
path, err = initialize.PersistScriptInWorkspace(t, env.Workspace())
if err == nil {
var name string
name, err = system.ExecuteScript(path)
initialize.PersistUnitNameInWorkspace(name, env.Workspace())
}
path, err := initialize.PersistScriptInWorkspace(script, env.Workspace())
if err == nil {
var name string
name, err = system.ExecuteScript(path)
initialize.PersistUnitNameInWorkspace(name, env.Workspace())
}
return err
}
func processNetconf(convertNetconf, configdrive string) error {
openstackRoot := path.Join(configdrive, "openstack")
metadataFilename := path.Join(openstackRoot, "latest", "meta_data.json")
metadataBytes, err := ioutil.ReadFile(metadataFilename)
if err != nil {
return err
}
var metadata struct {
NetworkConfig struct {
ContentPath string `json:"content_path"`
} `json:"network_config"`
}
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
return err
}
configPath := metadata.NetworkConfig.ContentPath
if configPath == "" {
fmt.Printf("No network config specified in %q.\n", metadataFilename)
return nil
}
netconfBytes, err := ioutil.ReadFile(path.Join(openstackRoot, configPath))
if err != nil {
return err
}
var interfaces []network.InterfaceGenerator
switch convertNetconf {
case "debian":
interfaces, err = network.ProcessDebianNetconf(string(netconfBytes))
default:
return fmt.Errorf("Unsupported network config format %q", convertNetconf)
}
if err != nil {
return err
}
if err := system.WriteNetworkdConfigs(interfaces); err != nil {
return err
}
return system.RestartNetwork(interfaces)
}

110
coreos-cloudinit_test.go Normal file
View File

@@ -0,0 +1,110 @@
package main
import (
"reflect"
"testing"
"github.com/coreos/coreos-cloudinit/initialize"
)
func TestMergeCloudConfig(t *testing.T) {
simplecc := initialize.CloudConfig{
SSHAuthorizedKeys: []string{"abc", "def"},
Hostname: "foobar",
NetworkConfigPath: "/path/somewhere",
}
for i, tt := range []struct {
udcc initialize.CloudConfig
mdcc initialize.CloudConfig
want initialize.CloudConfig
}{
{
// If mdcc is empty, udcc should be returned unchanged
simplecc,
initialize.CloudConfig{},
simplecc,
},
{
// If udcc is empty, mdcc should be returned unchanged(overridden)
initialize.CloudConfig{},
simplecc,
simplecc,
},
{
// user-data should override completely in the case of conflicts
simplecc,
initialize.CloudConfig{
Hostname: "meta-hostname",
NetworkConfigPath: "/path/meta",
},
simplecc,
},
{
// Mixed merge should succeed
initialize.CloudConfig{
SSHAuthorizedKeys: []string{"abc", "def"},
Hostname: "user-hostname",
NetworkConfigPath: "/path/somewhere",
},
initialize.CloudConfig{
SSHAuthorizedKeys: []string{"woof", "qux"},
Hostname: "meta-hostname",
},
initialize.CloudConfig{
SSHAuthorizedKeys: []string{"abc", "def", "woof", "qux"},
Hostname: "user-hostname",
NetworkConfigPath: "/path/somewhere",
},
},
{
// Completely non-conflicting merge should be fine
initialize.CloudConfig{
Hostname: "supercool",
},
initialize.CloudConfig{
SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"},
NetworkConfigPath: "/dev/fun",
},
initialize.CloudConfig{
Hostname: "supercool",
SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"},
NetworkConfigPath: "/dev/fun",
},
},
{
// Non-mergeable settings in user-data should not be affected
initialize.CloudConfig{
Hostname: "mememe",
ManageEtcHosts: initialize.EtcHosts("lolz"),
},
initialize.CloudConfig{
Hostname: "youyouyou",
NetworkConfigPath: "meta-meta-yo",
},
initialize.CloudConfig{
Hostname: "mememe",
ManageEtcHosts: initialize.EtcHosts("lolz"),
NetworkConfigPath: "meta-meta-yo",
},
},
{
// Non-mergeable (unexpected) settings in meta-data are ignored
initialize.CloudConfig{
Hostname: "mememe",
},
initialize.CloudConfig{
ManageEtcHosts: initialize.EtcHosts("lolz"),
NetworkConfigPath: "meta-meta-yo",
},
initialize.CloudConfig{
Hostname: "mememe",
NetworkConfigPath: "meta-meta-yo",
},
},
} {
got := mergeCloudConfig(tt.mdcc, tt.udcc)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("case #%d: mergeCloudConfig mutated CloudConfig unexpectedly:\ngot:\n%s\nwant:\n%s", i, got, tt.want)
}
}
}

View File

@@ -1,27 +0,0 @@
package datasource
import (
"io/ioutil"
"os"
"path"
)
type configDrive struct {
path string
}
func NewConfigDrive(path string) *configDrive {
return &configDrive{path}
}
func (self *configDrive) Fetch() ([]byte, error) {
data, err := ioutil.ReadFile(path.Join(self.path, "openstack", "latest", "user_data"))
if os.IsNotExist(err) {
err = nil
}
return data, err
}
func (self *configDrive) Type() string {
return "cloud-drive"
}

View File

@@ -0,0 +1,63 @@
package configdrive
import (
"fmt"
"io/ioutil"
"os"
"path"
)
const (
openstackApiVersion = "latest"
)
type configDrive struct {
root string
readFile func(filename string) ([]byte, error)
}
func NewDatasource(root string) *configDrive {
return &configDrive{root, ioutil.ReadFile}
}
func (cd *configDrive) IsAvailable() bool {
_, err := os.Stat(cd.root)
return !os.IsNotExist(err)
}
func (cd *configDrive) AvailabilityChanges() bool {
return true
}
func (cd *configDrive) ConfigRoot() string {
return cd.openstackRoot()
}
func (cd *configDrive) FetchMetadata() ([]byte, error) {
return cd.tryReadFile(path.Join(cd.openstackVersionRoot(), "meta_data.json"))
}
func (cd *configDrive) FetchUserdata() ([]byte, error) {
return cd.tryReadFile(path.Join(cd.openstackVersionRoot(), "user_data"))
}
func (cd *configDrive) Type() string {
return "cloud-drive"
}
func (cd *configDrive) openstackRoot() string {
return path.Join(cd.root, "openstack")
}
func (cd *configDrive) openstackVersionRoot() string {
return path.Join(cd.openstackRoot(), openstackApiVersion)
}
func (cd *configDrive) tryReadFile(filename string) ([]byte, error) {
fmt.Printf("Attempting to read from %q\n", filename)
data, err := cd.readFile(filename)
if os.IsNotExist(err) {
err = nil
}
return data, err
}

View File

@@ -0,0 +1,125 @@
package configdrive
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 TestFetchMetadata(t *testing.T) {
for _, tt := range []struct {
root string
filename string
files mockFilesystem
}{
{
"/",
"",
mockFilesystem{},
},
{
"/",
"/openstack/latest/meta_data.json",
mockFilesystem([]string{"/openstack/latest/meta_data.json"}),
},
{
"/media/configdrive",
"/media/configdrive/openstack/latest/meta_data.json",
mockFilesystem([]string{"/media/configdrive/openstack/latest/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 TestFetchUserdata(t *testing.T) {
for _, tt := range []struct {
root string
filename string
files mockFilesystem
}{
{
"/",
"",
mockFilesystem{},
},
{
"/",
"/openstack/latest/user_data",
mockFilesystem([]string{"/openstack/latest/user_data"}),
},
{
"/media/configdrive",
"/media/configdrive/openstack/latest/user_data",
mockFilesystem([]string{"/media/configdrive/openstack/latest/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 TestConfigRoot(t *testing.T) {
for _, tt := range []struct {
root string
configRoot string
}{
{
"/",
"/openstack",
},
{
"/media/configdrive",
"/media/configdrive/openstack",
},
} {
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)
}
}
}
func TestNewDatasource(t *testing.T) {
for _, tt := range []struct {
root string
expectRoot string
}{
{
root: "",
expectRoot: "",
},
{
root: "/media/configdrive",
expectRoot: "/media/configdrive",
},
} {
service := NewDatasource(tt.root)
if service.root != tt.expectRoot {
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
}
}
}

View File

@@ -1,6 +1,15 @@
package datasource
const (
Ec2ApiVersion = "2009-04-04"
OpenstackApiVersion = "2012-08-10"
)
type Datasource interface {
Fetch() ([]byte, error)
IsAvailable() bool
AvailabilityChanges() bool
ConfigRoot() string
FetchMetadata() ([]byte, error)
FetchUserdata() ([]byte, error)
Type() string
}

View File

@@ -1,21 +0,0 @@
package datasource
import (
"io/ioutil"
)
type localFile struct {
path string
}
func NewLocalFile(path string) *localFile {
return &localFile{path}
}
func (self *localFile) Fetch() ([]byte, error) {
return ioutil.ReadFile(self.path)
}
func (self *localFile) Type() string {
return "local-file"
}

39
datasource/file/file.go Normal file
View File

@@ -0,0 +1,39 @@
package file
import (
"io/ioutil"
"os"
)
type localFile struct {
path string
}
func NewDatasource(path string) *localFile {
return &localFile{path}
}
func (f *localFile) IsAvailable() bool {
_, err := os.Stat(f.path)
return !os.IsNotExist(err)
}
func (f *localFile) AvailabilityChanges() bool {
return true
}
func (f *localFile) ConfigRoot() string {
return ""
}
func (f *localFile) FetchMetadata() ([]byte, error) {
return []byte{}, nil
}
func (f *localFile) FetchUserdata() ([]byte, error) {
return ioutil.ReadFile(f.path)
}
func (f *localFile) Type() string {
return "local-file"
}

View File

@@ -0,0 +1,141 @@
package ec2
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/coreos/coreos-cloudinit/pkg"
)
const (
DefaultAddress = "http://169.254.169.254/"
apiVersion = "2009-04-04"
userdataUrl = apiVersion + "/user-data"
metadataUrl = apiVersion + "/meta-data"
)
type metadataService struct {
root string
client pkg.Getter
}
func NewDatasource(root string) *metadataService {
if !strings.HasSuffix(root, "/") {
root += "/"
}
return &metadataService{root, pkg.NewHttpClient()}
}
func (ms metadataService) IsAvailable() bool {
_, err := ms.client.Get(ms.root + apiVersion)
return (err == nil)
}
func (ms metadataService) AvailabilityChanges() bool {
return true
}
func (ms metadataService) ConfigRoot() string {
return ms.root
}
func (ms metadataService) FetchMetadata() ([]byte, error) {
attrs := make(map[string]interface{})
if keynames, err := fetchAttributes(ms.client, fmt.Sprintf("%s/public-keys", ms.metadataUrl())); err == nil {
keyIDs := make(map[string]string)
for _, keyname := range keynames {
tokens := strings.SplitN(keyname, "=", 2)
if len(tokens) != 2 {
return nil, fmt.Errorf("malformed public key: %q", keyname)
}
keyIDs[tokens[1]] = tokens[0]
}
keys := make(map[string]string)
for name, id := range keyIDs {
sshkey, err := fetchAttribute(ms.client, fmt.Sprintf("%s/public-keys/%s/openssh-key", ms.metadataUrl(), id))
if err != nil {
return nil, err
}
keys[name] = sshkey
fmt.Printf("Found SSH key for %q\n", name)
}
attrs["public_keys"] = keys
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
if hostname, err := fetchAttribute(ms.client, fmt.Sprintf("%s/hostname", ms.metadataUrl())); err == nil {
attrs["hostname"] = hostname
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
if localAddr, err := fetchAttribute(ms.client, fmt.Sprintf("%s/local-ipv4", ms.metadataUrl())); err == nil {
attrs["local-ipv4"] = localAddr
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
if publicAddr, err := fetchAttribute(ms.client, fmt.Sprintf("%s/public-ipv4", ms.metadataUrl())); err == nil {
attrs["public-ipv4"] = publicAddr
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
if content_path, err := fetchAttribute(ms.client, fmt.Sprintf("%s/network_config/content_path", ms.metadataUrl())); err == nil {
attrs["network_config"] = map[string]string{
"content_path": content_path,
}
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
return json.Marshal(attrs)
}
func (ms metadataService) FetchUserdata() ([]byte, error) {
if data, err := ms.client.GetRetry(ms.userdataUrl()); err == nil {
return data, err
} else if _, ok := err.(pkg.ErrNotFound); ok {
return []byte{}, nil
} else {
return data, err
}
}
func (ms metadataService) Type() string {
return "ec2-metadata-service"
}
func (ms metadataService) metadataUrl() string {
return (ms.root + metadataUrl)
}
func (ms metadataService) userdataUrl() string {
return (ms.root + userdataUrl)
}
func fetchAttributes(client pkg.Getter, url string) ([]string, error) {
resp, err := client.GetRetry(url)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(bytes.NewBuffer(resp))
data := make([]string, 0)
for scanner.Scan() {
data = append(data, scanner.Text())
}
return data, scanner.Err()
}
func fetchAttribute(client pkg.Getter, url string) (string, error) {
if attrs, err := fetchAttributes(client, url); err == nil && len(attrs) > 0 {
return attrs[0], nil
} else {
return "", err
}
}

View File

@@ -0,0 +1,324 @@
package ec2
import (
"bytes"
"fmt"
"reflect"
"testing"
"github.com/coreos/coreos-cloudinit/pkg"
)
type testHttpClient struct {
resources map[string]string
err error
}
func (t *testHttpClient) GetRetry(url string) ([]byte, error) {
if t.err != nil {
return nil, t.err
}
if val, ok := t.resources[url]; ok {
return []byte(val), nil
} else {
return nil, pkg.ErrNotFound{fmt.Errorf("not found: %q", url)}
}
}
func (t *testHttpClient) Get(url string) ([]byte, error) {
return t.GetRetry(url)
}
func TestAvailabilityChanges(t *testing.T) {
want := true
if ac := (metadataService{}).AvailabilityChanges(); ac != want {
t.Fatalf("bad AvailabilityChanges: want %q, got %q", want, ac)
}
}
func TestType(t *testing.T) {
want := "ec2-metadata-service"
if kind := (metadataService{}).Type(); kind != want {
t.Fatalf("bad type: want %q, got %q", want, kind)
}
}
func TestIsAvailable(t *testing.T) {
for _, tt := range []struct {
root string
resources map[string]string
expect bool
}{
{
root: "/",
resources: map[string]string{
"/2009-04-04": "",
},
expect: true,
},
{
root: "/",
resources: map[string]string{},
expect: false,
},
} {
service := &metadataService{tt.root, &testHttpClient{tt.resources, nil}}
if a := service.IsAvailable(); a != tt.expect {
t.Fatalf("bad isAvailable (%q): want %q, got %q", tt.resources, tt.expect, a)
}
}
}
func TestFetchUserdata(t *testing.T) {
for _, tt := range []struct {
root string
resources map[string]string
userdata []byte
clientErr error
expectErr error
}{
{
root: "/",
resources: map[string]string{
"/2009-04-04/user-data": "hello",
},
userdata: []byte("hello"),
},
{
root: "/",
clientErr: pkg.ErrNotFound{fmt.Errorf("test not found error")},
userdata: []byte{},
},
{
root: "/",
clientErr: pkg.ErrTimeout{fmt.Errorf("test timeout error")},
expectErr: pkg.ErrTimeout{fmt.Errorf("test timeout error")},
},
} {
service := &metadataService{tt.root, &testHttpClient{tt.resources, tt.clientErr}}
data, err := service.FetchUserdata()
if Error(err) != Error(tt.expectErr) {
t.Fatalf("bad error (%q): want %q, got %q", tt.resources, tt.expectErr, err)
}
if !bytes.Equal(data, tt.userdata) {
t.Fatalf("bad userdata (%q): want %q, got %q", tt.resources, tt.userdata, data)
}
}
}
func TestUrls(t *testing.T) {
for _, tt := range []struct {
root string
expectRoot string
userdata string
metadata string
}{
{
root: "/",
expectRoot: "/",
userdata: "/2009-04-04/user-data",
metadata: "/2009-04-04/meta-data",
},
{
root: "http://169.254.169.254/",
expectRoot: "http://169.254.169.254/",
userdata: "http://169.254.169.254/2009-04-04/user-data",
metadata: "http://169.254.169.254/2009-04-04/meta-data",
},
} {
service := &metadataService{tt.root, nil}
if url := service.userdataUrl(); url != tt.userdata {
t.Fatalf("bad url (%q): want %q, got %q", tt.root, tt.userdata, url)
}
if url := service.metadataUrl(); url != tt.metadata {
t.Fatalf("bad url (%q): want %q, got %q", tt.root, tt.metadata, url)
}
if url := service.ConfigRoot(); url != tt.expectRoot {
t.Fatalf("bad url (%q): want %q, got %q", tt.root, tt.expectRoot, url)
}
}
}
func TestFetchAttributes(t *testing.T) {
for _, s := range []struct {
resources map[string]string
err error
tests []struct {
path string
val []string
}
}{
{
resources: map[string]string{
"/": "a\nb\nc/",
"/c/": "d\ne/",
"/c/e/": "f",
"/a": "1",
"/b": "2",
"/c/d": "3",
"/c/e/f": "4",
},
tests: []struct {
path string
val []string
}{
{"/", []string{"a", "b", "c/"}},
{"/b", []string{"2"}},
{"/c/d", []string{"3"}},
{"/c/e/", []string{"f"}},
},
},
{
err: pkg.ErrNotFound{fmt.Errorf("test error")},
tests: []struct {
path string
val []string
}{
{"", nil},
},
},
} {
client := &testHttpClient{s.resources, s.err}
for _, tt := range s.tests {
attrs, err := fetchAttributes(client, tt.path)
if err != s.err {
t.Fatalf("bad error for %q (%q): want %q, got %q", tt.path, s.resources, s.err, err)
}
if !reflect.DeepEqual(attrs, tt.val) {
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.resources, tt.val, attrs)
}
}
}
}
func TestFetchAttribute(t *testing.T) {
for _, s := range []struct {
resources map[string]string
err error
tests []struct {
path string
val string
}
}{
{
resources: map[string]string{
"/": "a\nb\nc/",
"/c/": "d\ne/",
"/c/e/": "f",
"/a": "1",
"/b": "2",
"/c/d": "3",
"/c/e/f": "4",
},
tests: []struct {
path string
val string
}{
{"/a", "1"},
{"/b", "2"},
{"/c/d", "3"},
{"/c/e/f", "4"},
},
},
{
err: pkg.ErrNotFound{fmt.Errorf("test error")},
tests: []struct {
path string
val string
}{
{"", ""},
},
},
} {
client := &testHttpClient{s.resources, s.err}
for _, tt := range s.tests {
attr, err := fetchAttribute(client, tt.path)
if err != s.err {
t.Fatalf("bad error for %q (%q): want %q, got %q", tt.path, s.resources, s.err, err)
}
if attr != tt.val {
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.resources, tt.val, attr)
}
}
}
}
func TestFetchMetadata(t *testing.T) {
for _, tt := range []struct {
root string
resources map[string]string
expect []byte
clientErr error
expectErr error
}{
{
root: "/",
resources: map[string]string{
"/2009-04-04/meta-data/public-keys": "bad\n",
},
expectErr: fmt.Errorf("malformed public key: \"bad\""),
},
{
root: "/",
resources: map[string]string{
"/2009-04-04/meta-data/hostname": "host",
"/2009-04-04/meta-data/local-ipv4": "1.2.3.4",
"/2009-04-04/meta-data/public-ipv4": "5.6.7.8",
"/2009-04-04/meta-data/public-keys": "0=test1\n",
"/2009-04-04/meta-data/public-keys/0": "openssh-key",
"/2009-04-04/meta-data/public-keys/0/openssh-key": "key",
"/2009-04-04/meta-data/network_config/content_path": "path",
},
expect: []byte(`{"hostname":"host","local-ipv4":"1.2.3.4","network_config":{"content_path":"path"},"public-ipv4":"5.6.7.8","public_keys":{"test1":"key"}}`),
},
{
clientErr: pkg.ErrTimeout{fmt.Errorf("test error")},
expectErr: pkg.ErrTimeout{fmt.Errorf("test error")},
},
} {
service := &metadataService{tt.root, &testHttpClient{tt.resources, tt.clientErr}}
metadata, err := service.FetchMetadata()
if Error(err) != Error(tt.expectErr) {
t.Fatalf("bad error (%q): want %q, got %q", tt.resources, tt.expectErr, err)
}
if !bytes.Equal(metadata, tt.expect) {
t.Fatalf("bad fetch (%q): want %q, got %q", tt.resources, tt.expect, metadata)
}
}
}
func TestNewDatasource(t *testing.T) {
for _, tt := range []struct {
root string
expectRoot string
}{
{
root: "",
expectRoot: "/",
},
{
root: "/",
expectRoot: "/",
},
{
root: "http://169.254.169.254",
expectRoot: "http://169.254.169.254/",
},
{
root: "http://169.254.169.254/",
expectRoot: "http://169.254.169.254/",
},
} {
service := NewDatasource(tt.root)
if service.root != tt.expectRoot {
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
}
}
}
func Error(err error) string {
if err != nil {
return err.Error()
}
return ""
}

View File

@@ -1,20 +0,0 @@
package datasource
import "github.com/coreos/coreos-cloudinit/pkg"
type metadataService struct {
url string
}
func NewMetadataService(url string) *metadataService {
return &metadataService{url}
}
func (ms *metadataService) Fetch() ([]byte, error) {
client := pkg.NewHttpClient()
return client.Get(ms.url)
}
func (ms *metadataService) Type() string {
return "metadata-service"
}

View File

@@ -1,4 +1,4 @@
package datasource
package proc_cmdline
import (
"errors"
@@ -14,16 +14,39 @@ const (
ProcCmdlineCloudConfigFlag = "cloud-config-url"
)
type procCmdline struct{
type procCmdline struct {
Location string
}
func NewProcCmdline() *procCmdline {
func NewDatasource() *procCmdline {
return &procCmdline{Location: ProcCmdlineLocation}
}
func (self *procCmdline) Fetch() ([]byte, error) {
contents, err := ioutil.ReadFile(self.Location)
func (c *procCmdline) IsAvailable() bool {
contents, err := ioutil.ReadFile(c.Location)
if err != nil {
return false
}
cmdline := strings.TrimSpace(string(contents))
_, err = findCloudConfigURL(cmdline)
return (err == nil)
}
func (c *procCmdline) AvailabilityChanges() bool {
return false
}
func (c *procCmdline) ConfigRoot() string {
return ""
}
func (c *procCmdline) FetchMetadata() ([]byte, error) {
return []byte{}, nil
}
func (c *procCmdline) FetchUserdata() ([]byte, error) {
contents, err := ioutil.ReadFile(c.Location)
if err != nil {
return nil, err
}
@@ -35,7 +58,7 @@ func (self *procCmdline) Fetch() ([]byte, error) {
}
client := pkg.NewHttpClient()
cfg, err := client.Get(url)
cfg, err := client.GetRetry(url)
if err != nil {
return nil, err
}
@@ -43,7 +66,7 @@ func (self *procCmdline) Fetch() ([]byte, error) {
return cfg, nil
}
func (self *procCmdline) Type() string {
func (c *procCmdline) Type() string {
return "proc-cmdline"
}

View File

@@ -1,4 +1,4 @@
package datasource
package proc_cmdline
import (
"fmt"
@@ -75,9 +75,9 @@ func TestProcCmdlineAndFetchConfig(t *testing.T) {
t.Errorf("Test produced error: %v", err)
}
p := NewProcCmdline()
p := NewDatasource()
p.Location = file.Name()
cfg, err := p.Fetch()
cfg, err := p.FetchUserdata()
if err != nil {
t.Errorf("Test produced error: %v", err)
}

38
datasource/url/url.go Normal file
View File

@@ -0,0 +1,38 @@
package url
import "github.com/coreos/coreos-cloudinit/pkg"
type remoteFile struct {
url string
}
func NewDatasource(url string) *remoteFile {
return &remoteFile{url}
}
func (f *remoteFile) IsAvailable() bool {
client := pkg.NewHttpClient()
_, err := client.Get(f.url)
return (err == nil)
}
func (f *remoteFile) AvailabilityChanges() bool {
return true
}
func (f *remoteFile) ConfigRoot() string {
return ""
}
func (f *remoteFile) FetchMetadata() ([]byte, error) {
return []byte{}, nil
}
func (f *remoteFile) FetchUserdata() ([]byte, error) {
client := pkg.NewHttpClient()
return client.GetRetry(f.url)
}
func (f *remoteFile) Type() string {
return "url"
}

View File

@@ -3,10 +3,13 @@ package initialize
import (
"errors"
"fmt"
"io/ioutil"
"log"
"path"
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
"github.com/coreos/coreos-cloudinit/network"
"github.com/coreos/coreos-cloudinit/system"
)
@@ -34,10 +37,11 @@ type CloudConfig struct {
Update UpdateConfig
Units []system.Unit
}
WriteFiles []system.File `yaml:"write_files"`
Hostname string
Users []system.User
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
WriteFiles []system.File `yaml:"write_files"`
Hostname string
Users []system.User
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
NetworkConfigPath string
}
type warner func(format string, v ...interface{})
@@ -62,51 +66,62 @@ 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{})
known := cc["coreos"].(map[interface{}]interface{})
for k, _ := range set {
key := k.(string)
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", key)
if coreos, ok := c["coreos"]; ok {
if set, ok := coreos.(map[interface{}]interface{}); ok {
known := cc["coreos"].(map[interface{}]interface{})
for k, _ := range set {
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{})
for _, u := range set {
user := u.(map[interface{}]interface{})
for k, _ := range user {
key := k.(string)
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", key)
if set, ok := users.([]interface{}); ok {
for _, u := range set {
if user, ok := u.(map[interface{}]interface{}); ok {
for k, _ := range user {
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{})
for _, f := range set {
file := f.(map[interface{}]interface{})
for k, _ := range file {
key := k.(string)
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", key)
if set, ok := files.([]interface{}); ok {
for _, f := range set {
if file, ok := f.(map[interface{}]interface{}); ok {
for k, _ := range file {
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)
}
}
}
}
}
@@ -219,21 +234,74 @@ func Apply(cfg CloudConfig, env *Environment) error {
cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
}
wroteEnvironment := false
for _, file := range cfg.WriteFiles {
path, err := system.WriteFile(&file, env.Root())
fullPath, err := system.WriteFile(&file, env.Root())
if err != nil {
return err
}
log.Printf("Wrote file %s to filesystem", path)
if path.Clean(file.Path) == "/etc/environment" {
wroteEnvironment = true
}
log.Printf("Wrote file %s to filesystem", fullPath)
}
if !wroteEnvironment {
ef := env.DefaultEnvironmentFile()
if ef != nil {
err := system.WriteEnvFile(ef, env.Root())
if err != nil {
return err
}
log.Printf("Updated /etc/environment")
}
}
if env.NetconfType() != "" {
filename := path.Join(env.ConfigRoot(), cfg.NetworkConfigPath)
log.Printf("Attempting to read config from %q\n", filename)
netconfBytes, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
var interfaces []network.InterfaceGenerator
switch env.NetconfType() {
case "debian":
interfaces, err = network.ProcessDebianNetconf(string(netconfBytes))
default:
return fmt.Errorf("Unsupported network config format %q", env.NetconfType())
}
if err != nil {
return err
}
if err := system.WriteNetworkdConfigs(interfaces); err != nil {
return err
}
if err := system.RestartNetwork(interfaces); err != nil {
return err
}
}
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)
@@ -242,12 +310,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
}
}
@@ -255,7 +323,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)
@@ -272,14 +340,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

@@ -4,48 +4,89 @@ import (
"os"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
const DefaultSSHKeyName = "coreos-cloudinit"
type Environment struct {
root string
configRoot string
workspace string
netconfType string
sshKeyName string
substitutions map[string]string
}
func NewEnvironment(root, workspace string) *Environment {
substitutions := map[string]string{
// TODO(jonboulle): this is getting unwieldy, should be able to simplify the interface somehow
func NewEnvironment(root, configRoot, workspace, netconfType, sshKeyName string, substitutions map[string]string) *Environment {
if substitutions == nil {
substitutions = make(map[string]string)
}
// If certain values are not in the supplied substitution, fall back to retrieving them from the environment
for k, v := range map[string]string{
"$public_ipv4": os.Getenv("COREOS_PUBLIC_IPV4"),
"$private_ipv4": os.Getenv("COREOS_PRIVATE_IPV4"),
} {
if _, ok := substitutions[k]; !ok {
substitutions[k] = v
}
}
return &Environment{root, workspace, DefaultSSHKeyName, substitutions}
return &Environment{root, configRoot, workspace, netconfType, sshKeyName, substitutions}
}
func (self *Environment) Workspace() string {
return path.Join(self.root, self.workspace)
func (e *Environment) Workspace() string {
return path.Join(e.root, e.workspace)
}
func (self *Environment) Root() string {
return self.root
func (e *Environment) Root() string {
return e.root
}
func (self *Environment) SSHKeyName() string {
return self.sshKeyName
func (e *Environment) ConfigRoot() string {
return e.configRoot
}
func (self *Environment) SetSSHKeyName(name string) {
self.sshKeyName = name
func (e *Environment) NetconfType() string {
return e.netconfType
}
func (self *Environment) Apply(data string) string {
for key, val := range self.substitutions {
func (e *Environment) SSHKeyName() string {
return e.sshKeyName
}
func (e *Environment) SetSSHKeyName(name string) {
e.sshKeyName = name
}
func (e *Environment) Apply(data string) string {
for key, val := range e.substitutions {
data = strings.Replace(data, key, val, -1)
}
return data
}
func (e *Environment) DefaultEnvironmentFile() *system.EnvFile {
ef := system.EnvFile{
File: &system.File{
Path: "/etc/environment",
},
Vars: map[string]string{},
}
if ip, ok := e.substitutions["$public_ipv4"]; ok && len(ip) > 0 {
ef.Vars["COREOS_PUBLIC_IPV4"] = ip
}
if ip, ok := e.substitutions["$private_ipv4"]; ok && len(ip) > 0 {
ef.Vars["COREOS_PRIVATE_IPV4"] = ip
}
if len(ef.Vars) == 0 {
return nil
} else {
return &ef
}
}
// normalizeSvcEnv standardizes the keys of the map (environment variables for a service)
// by replacing any dashes with underscores and ensuring they are entirely upper case.
// For example, "some-env" --> "SOME_ENV"

View File

@@ -1,27 +1,106 @@
package initialize
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestEnvironmentApply(t *testing.T) {
os.Setenv("COREOS_PUBLIC_IPV4", "192.0.2.3")
os.Setenv("COREOS_PRIVATE_IPV4", "192.0.2.203")
env := NewEnvironment("./", "./")
input := `[Service]
os.Setenv("COREOS_PUBLIC_IPV4", "1.2.3.4")
os.Setenv("COREOS_PRIVATE_IPV4", "5.6.7.8")
for _, tt := range []struct {
subs map[string]string
input string
out string
}{
{
// Substituting both values directly should always take precedence
// over environment variables
map[string]string{
"$public_ipv4": "192.0.2.3",
"$private_ipv4": "192.0.2.203",
},
`[Service]
ExecStart=/usr/bin/echo "$public_ipv4"
ExecStop=/usr/bin/echo $private_ipv4
ExecStop=/usr/bin/echo $unknown
`
expected := `[Service]
ExecStop=/usr/bin/echo $unknown`,
`[Service]
ExecStart=/usr/bin/echo "192.0.2.3"
ExecStop=/usr/bin/echo 192.0.2.203
ExecStop=/usr/bin/echo $unknown
`
ExecStop=/usr/bin/echo $unknown`,
},
{
// Substituting one value directly while falling back with the other
map[string]string{"$private_ipv4": "127.0.0.1"},
"$private_ipv4\n$public_ipv4",
"127.0.0.1\n1.2.3.4",
},
{
// Falling back to environment variables for both values
map[string]string{"foo": "bar"},
"$private_ipv4\n$public_ipv4",
"5.6.7.8\n1.2.3.4",
},
{
// No substitutions
nil,
"$private_ipv4\nfoobar",
"5.6.7.8\nfoobar",
},
} {
output := env.Apply(input)
if output != expected {
t.Fatalf("Environment incorrectly applied.\nOutput:\n%s\nExpected:\n%s", output, expected)
env := NewEnvironment("./", "./", "./", "", "", tt.subs)
got := env.Apply(tt.input)
if got != tt.out {
t.Fatalf("Environment incorrectly applied.\ngot:\n%s\nwant:\n%s", got, tt.out)
}
}
}
func TestEnvironmentFile(t *testing.T) {
subs := map[string]string{
"$public_ipv4": "1.2.3.4",
"$private_ipv4": "5.6.7.8",
}
expect := "COREOS_PRIVATE_IPV4=5.6.7.8\nCOREOS_PUBLIC_IPV4=1.2.3.4\n"
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
env := NewEnvironment("./", "./", "./", "", "", subs)
ef := env.DefaultEnvironmentFile()
err = system.WriteEnvFile(ef, dir)
if err != nil {
t.Fatalf("WriteEnvFile failed: %v", err)
}
fullPath := path.Join(dir, "etc", "environment")
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != expect {
t.Fatalf("File has incorrect contents: %q", contents)
}
}
func TestEnvironmentFileNil(t *testing.T) {
subs := map[string]string{
"$public_ipv4": "",
"$private_ipv4": "",
}
env := NewEnvironment("./", "./", "./", "", "", subs)
ef := env.DefaultEnvironmentFile()
if ef != nil {
t.Fatalf("Environment file not nil: %v", ef)
}
}

View File

@@ -3,6 +3,7 @@ package initialize
import (
"errors"
"fmt"
"sort"
"github.com/coreos/coreos-cloudinit/system"
)
@@ -19,9 +20,16 @@ func (ee EtcdEnvironment) String() (out string) {
}
}
var sorted sort.StringSlice
for k, _ := range norm {
sorted = append(sorted, k)
}
sorted.Sort()
out += "[Service]\n"
for key, val := range norm {
for _, key := range sorted {
val := norm[key]
out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val)
}
@@ -31,7 +39,7 @@ func (ee EtcdEnvironment) String() (out string) {
// Units creates a Unit file drop-in for etcd, using any configured
// options and adding a default MachineID if unset.
func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
if ee == nil {
if len(ee) < 1 {
return nil, nil
}

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)
}
@@ -102,8 +104,8 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
}
expect := `[Service]
Environment="ETCD_NAME=node001"
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_NAME=node001"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`
if string(contents) != expect {
@@ -111,14 +113,27 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
}
}
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
func TestEtcdEnvironmentEmptyNoOp(t *testing.T) {
ee := EtcdEnvironment{}
uu, err := ee.Units("")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(uu) > 0 {
t.Fatalf("Generated etcd units unexpectedly: %v")
}
}
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
ee := EtcdEnvironment{"foo": "bar"}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
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 +151,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)
}
@@ -148,6 +163,7 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
}
expect := `[Service]
Environment="ETCD_FOO=bar"
Environment="ETCD_NAME=node007"
`
if string(contents) != expect {

52
initialize/meta_data.go Normal file
View File

@@ -0,0 +1,52 @@
package initialize
import "encoding/json"
// ParseMetaData parses a JSON blob in the OpenStack metadata service format, and
// converts it to a partially hydrated CloudConfig
func ParseMetaData(contents string) (*CloudConfig, error) {
if len(contents) == 0 {
return nil, nil
}
var metadata struct {
SSHAuthorizedKeyMap map[string]string `json:"public_keys"`
Hostname string `json:"hostname"`
NetworkConfig struct {
ContentPath string `json:"content_path"`
} `json:"network_config"`
}
if err := json.Unmarshal([]byte(contents), &metadata); err != nil {
return nil, err
}
var cfg CloudConfig
if len(metadata.SSHAuthorizedKeyMap) > 0 {
cfg.SSHAuthorizedKeys = make([]string, 0, len(metadata.SSHAuthorizedKeyMap))
for _, key := range metadata.SSHAuthorizedKeyMap {
cfg.SSHAuthorizedKeys = append(cfg.SSHAuthorizedKeys, key)
}
}
cfg.Hostname = metadata.Hostname
cfg.NetworkConfigPath = metadata.NetworkConfig.ContentPath
return &cfg, nil
}
// ExtractIPsFromMetaData parses a JSON blob in the OpenStack metadata service format,
// and returns a substitution map possibly containing private_ipv4 and public_ipv4 addresses
func ExtractIPsFromMetadata(contents []byte) (map[string]string, error) {
var ips struct {
Public string `json:"public-ipv4"`
Private string `json:"local-ipv4"`
}
if err := json.Unmarshal(contents, &ips); err != nil {
return nil, err
}
m := make(map[string]string)
if ips.Private != "" {
m["$private_ipv4"] = ips.Private
}
if ips.Public != "" {
m["$public_ipv4"] = ips.Public
}
return m, nil
}

View File

@@ -0,0 +1,69 @@
package initialize
import "reflect"
import "testing"
func TestParseMetadata(t *testing.T) {
for i, tt := range []struct {
in string
want *CloudConfig
err bool
}{
{"", nil, false},
{`garbage, invalid json`, nil, true},
{`{"foo": "bar"}`, &CloudConfig{}, false},
{`{"network_config": {"content_path": "asdf"}}`, &CloudConfig{NetworkConfigPath: "asdf"}, false},
{`{"hostname": "turkleton"}`, &CloudConfig{Hostname: "turkleton"}, false},
{`{"public_keys": {"jack": "jill", "bob": "alice"}}`, &CloudConfig{SSHAuthorizedKeys: []string{"jill", "alice"}}, false},
{`{"unknown": "thing", "hostname": "my_host", "public_keys": {"do": "re", "mi": "fa"}, "network_config": {"content_path": "/root", "blah": "zzz"}}`, &CloudConfig{SSHAuthorizedKeys: []string{"re", "fa"}, Hostname: "my_host", NetworkConfigPath: "/root"}, false},
} {
got, err := ParseMetaData(tt.in)
if tt.err != (err != nil) {
t.Errorf("case #%d: bad error state: got %t, want %t (err=%v)", i, (err != nil), tt.err, err)
}
if got == nil {
if tt.want != nil {
t.Errorf("case #%d: unexpected nil output", i)
}
} else if tt.want == nil {
t.Errorf("case #%d: unexpected non-nil output", i)
} else {
if !reflect.DeepEqual(*got, *tt.want) {
t.Errorf("case #%d: bad output:\ngot\n%v\nwant\n%v", i, *got, *tt.want)
}
}
}
}
func TestExtractIPsFromMetadata(t *testing.T) {
for i, tt := range []struct {
in []byte
err bool
out map[string]string
}{
{
[]byte(`{"public-ipv4": "12.34.56.78", "local-ipv4": "1.2.3.4"}`),
false,
map[string]string{"$public_ipv4": "12.34.56.78", "$private_ipv4": "1.2.3.4"},
},
{
[]byte(`{"local-ipv4": "127.0.0.1", "something_else": "don't care"}`),
false,
map[string]string{"$private_ipv4": "127.0.0.1"},
},
{
[]byte(`garbage`),
true,
nil,
},
} {
got, err := ExtractIPsFromMetadata(tt.in)
if (err != nil) != tt.err {
t.Errorf("bad error state (got %t, want %t)", err != nil, tt.err)
}
if !reflect.DeepEqual(got, tt.out) {
t.Errorf("case %d: got %s, want %s", i, got, tt.out)
}
}
}

View File

@@ -25,7 +25,7 @@ func SSHImportKeysFromURL(system_user string, url string) error {
func fetchUserKeys(url string) ([]string, error) {
client := pkg.NewHttpClient()
data, err := client.Get(url)
data, err := client.GetRetry(url)
if err != nil {
return nil, err
}

View File

@@ -9,6 +9,9 @@ import (
)
func ParseUserData(contents string) (interface{}, error) {
if len(contents) == 0 {
return nil, nil
}
header := strings.SplitN(contents, "\n", 2)[0]
// Explicitly trim the header so we can handle user-data from
@@ -19,14 +22,9 @@ func ParseUserData(contents string) (interface{}, error) {
if strings.HasPrefix(header, "#!") {
log.Printf("Parsing user-data as script")
return system.Script(contents), nil
} else if header == "#cloud-config" {
log.Printf("Parsing user-data as cloud-config")
cfg, err := NewCloudConfig(contents)
if err != nil {
log.Fatal(err.Error())
}
return *cfg, nil
return NewCloudConfig(contents)
} else {
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
}

View File

@@ -37,7 +37,7 @@ func TestParseConfigCRLF(t *testing.T) {
t.Fatalf("Failed parsing config: %v", err)
}
cfg := ud.(CloudConfig)
cfg := ud.(*CloudConfig)
if cfg.Hostname != "foo" {
t.Error("Failed parsing hostname from config")
@@ -47,3 +47,12 @@ func TestParseConfigCRLF(t *testing.T) {
t.Error("Parsed incorrect number of SSH keys")
}
}
func TestParseConfigEmpty(t *testing.T) {
i, e := ParseUserData(``)
if i != nil {
t.Error("ParseUserData of empty string returned non-nil unexpectedly")
} else if e != nil {
t.Error("ParseUserData of empty string returned error unexpectedly")
}
}

View File

@@ -3,19 +3,30 @@ package network
import (
"fmt"
"strconv"
"strings"
)
type InterfaceGenerator interface {
Name() string
Filename() string
Netdev() string
Link() string
Network() string
Type() string
ModprobeParams() string
}
type networkInterface interface {
InterfaceGenerator
Children() []networkInterface
setConfigDepth(int)
}
type logicalInterface struct {
name string
config configMethod
children []InterfaceGenerator
name string
config configMethod
children []networkInterface
configDepth int
}
func (i *logicalInterface) Network() string {
@@ -48,6 +59,26 @@ func (i *logicalInterface) Network() string {
return config
}
func (i *logicalInterface) Link() string {
return ""
}
func (i *logicalInterface) Filename() string {
return fmt.Sprintf("%02x-%s", i.configDepth, i.name)
}
func (i *logicalInterface) Children() []networkInterface {
return i.children
}
func (i *logicalInterface) ModprobeParams() string {
return ""
}
func (i *logicalInterface) setConfigDepth(depth int) {
i.configDepth = depth
}
type physicalInterface struct {
logicalInterface
}
@@ -60,13 +91,14 @@ func (p *physicalInterface) Netdev() string {
return ""
}
func (p *physicalInterface) Link() string {
return ""
func (p *physicalInterface) Type() string {
return "physical"
}
type bondInterface struct {
logicalInterface
slaves []string
slaves []string
options map[string]string
}
func (b *bondInterface) Name() string {
@@ -77,8 +109,17 @@ func (b *bondInterface) Netdev() string {
return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name)
}
func (b *bondInterface) Link() string {
return ""
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 {
@@ -92,102 +133,161 @@ func (v *vlanInterface) Name() string {
}
func (v *vlanInterface) Netdev() string {
return fmt.Sprintf("[NetDev]\nKind=vlan\nName=%s\n\n[VLAN]\nId=%d\n", v.name, v.id)
config := fmt.Sprintf("[NetDev]\nKind=vlan\nName=%s\n", v.name)
switch c := v.config.(type) {
case configMethodStatic:
if c.hwaddress != nil {
config += fmt.Sprintf("MACAddress=%s\n", c.hwaddress)
}
case configMethodDHCP:
if c.hwaddress != nil {
config += fmt.Sprintf("MACAddress=%s\n", c.hwaddress)
}
}
config += fmt.Sprintf("\n[VLAN]\nId=%d\n", v.id)
return config
}
func (v *vlanInterface) Link() string {
return ""
func (v *vlanInterface) Type() string {
return "vlan"
}
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
bondStanzas := make(map[string]*stanzaInterface)
physicalStanzas := make(map[string]*stanzaInterface)
vlanStanzas := make(map[string]*stanzaInterface)
for _, iface := range stanzas {
switch iface.kind {
case interfaceBond:
bondStanzas[iface.name] = iface
case interfacePhysical:
physicalStanzas[iface.name] = iface
case interfaceVLAN:
vlanStanzas[iface.name] = iface
}
}
interfaceMap := createInterfaces(stanzas)
linkAncestors(interfaceMap)
markConfigDepths(interfaceMap)
physicals := make(map[string]*physicalInterface)
for _, p := range physicalStanzas {
if _, ok := p.configMethod.(configMethodLoopback); ok {
continue
}
physicals[p.name] = &physicalInterface{
logicalInterface{
name: p.name,
config: p.configMethod,
children: []InterfaceGenerator{},
},
}
}
bonds := make(map[string]*bondInterface)
for _, b := range bondStanzas {
bonds[b.name] = &bondInterface{
logicalInterface{
name: b.name,
config: b.configMethod,
children: []InterfaceGenerator{},
},
b.options["slaves"],
}
}
vlans := make(map[string]*vlanInterface)
for _, v := range vlanStanzas {
var rawDevice string
id, _ := strconv.Atoi(v.options["id"][0])
if device := v.options["raw_device"]; len(device) == 1 {
rawDevice = device[0]
}
vlans[v.name] = &vlanInterface{
logicalInterface{
name: v.name,
config: v.configMethod,
children: []InterfaceGenerator{},
},
id,
rawDevice,
}
}
for _, vlan := range vlans {
if physical, ok := physicals[vlan.rawDevice]; ok {
physical.children = append(physical.children, vlan)
}
if bond, ok := bonds[vlan.rawDevice]; ok {
bond.children = append(bond.children, vlan)
}
}
for _, bond := range bonds {
for _, slave := range bond.slaves {
if physical, ok := physicals[slave]; ok {
physical.children = append(physical.children, bond)
}
if pBond, ok := bonds[slave]; ok {
pBond.children = append(pBond.children, bond)
}
}
}
interfaces := make([]InterfaceGenerator, 0, len(physicals)+len(bonds)+len(vlans))
for _, physical := range physicals {
interfaces = append(interfaces, physical)
}
for _, bond := range bonds {
interfaces = append(interfaces, bond)
}
for _, vlan := range vlans {
interfaces = append(interfaces, vlan)
interfaces := make([]InterfaceGenerator, 0, len(interfaceMap))
for _, iface := range interfaceMap {
interfaces = append(interfaces, iface)
}
return interfaces
}
func createInterfaces(stanzas []*stanzaInterface) map[string]networkInterface {
interfaceMap := make(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["bond-slaves"],
bondOptions,
}
for _, slave := range iface.options["bond-slaves"] {
if _, ok := interfaceMap[slave]; !ok {
interfaceMap[slave] = &physicalInterface{
logicalInterface{
name: slave,
config: configMethodManual{},
children: []networkInterface{},
},
}
}
}
case interfacePhysical:
if _, ok := iface.configMethod.(configMethodLoopback); ok {
continue
}
interfaceMap[iface.name] = &physicalInterface{
logicalInterface{
name: iface.name,
config: iface.configMethod,
children: []networkInterface{},
},
}
case interfaceVLAN:
var rawDevice string
id, _ := strconv.Atoi(iface.options["id"][0])
if device := iface.options["raw_device"]; len(device) == 1 {
rawDevice = device[0]
if _, ok := interfaceMap[rawDevice]; !ok {
interfaceMap[rawDevice] = &physicalInterface{
logicalInterface{
name: rawDevice,
config: configMethodManual{},
children: []networkInterface{},
},
}
}
}
interfaceMap[iface.name] = &vlanInterface{
logicalInterface{
name: iface.name,
config: iface.configMethod,
children: []networkInterface{},
},
id,
rawDevice,
}
}
}
return interfaceMap
}
func linkAncestors(interfaceMap map[string]networkInterface) {
for _, iface := range interfaceMap {
switch i := iface.(type) {
case *vlanInterface:
if parent, ok := interfaceMap[i.rawDevice]; ok {
switch p := parent.(type) {
case *physicalInterface:
p.children = append(p.children, iface)
case *bondInterface:
p.children = append(p.children, iface)
}
}
case *bondInterface:
for _, slave := range i.slaves {
if parent, ok := interfaceMap[slave]; ok {
switch p := parent.(type) {
case *physicalInterface:
p.children = append(p.children, iface)
case *bondInterface:
p.children = append(p.children, iface)
}
}
}
}
}
}
func markConfigDepths(interfaceMap map[string]networkInterface) {
rootInterfaceMap := make(map[string]networkInterface)
for k, v := range interfaceMap {
rootInterfaceMap[k] = v
}
for _, iface := range interfaceMap {
for _, child := range iface.Children() {
delete(rootInterfaceMap, child.Name())
}
}
for _, iface := range rootInterfaceMap {
setDepth(iface)
}
}
func setDepth(iface networkInterface) int {
maxDepth := 0
for _, child := range iface.Children() {
if depth := setDepth(child); depth > maxDepth {
maxDepth = depth
}
}
iface.setConfigDepth(maxDepth)
return (maxDepth + 1)
}

View File

@@ -30,12 +30,13 @@ func TestPhysicalInterfaceLink(t *testing.T) {
func TestPhysicalInterfaceNetwork(t *testing.T) {
p := physicalInterface{logicalInterface{
name: "testname",
children: []InterfaceGenerator{
children: []networkInterface{
&bondInterface{
logicalInterface{
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
@@ -96,12 +97,13 @@ func TestBondInterfaceNetwork(t *testing.T) {
logicalInterface{
name: "testname",
config: configMethodDHCP{},
children: []InterfaceGenerator{
children: []networkInterface{
&bondInterface{
logicalInterface{
name: "testbond1",
},
nil,
nil,
},
&vlanInterface{
logicalInterface{
@@ -120,6 +122,7 @@ func TestBondInterfaceNetwork(t *testing.T) {
},
},
nil,
nil,
}
network := `[Match]
Name=testname
@@ -143,16 +146,26 @@ func TestVLANInterfaceName(t *testing.T) {
}
func TestVLANInterfaceNetdev(t *testing.T) {
v := vlanInterface{logicalInterface{name: "testname"}, 1, ""}
netdev := `[NetDev]
Kind=vlan
Name=testname
[VLAN]
Id=1
`
if v.Netdev() != netdev {
t.FailNow()
for _, tt := range []struct {
i vlanInterface
l string
}{
{
vlanInterface{logicalInterface{name: "testname"}, 1, ""},
"[NetDev]\nKind=vlan\nName=testname\n\n[VLAN]\nId=1\n",
},
{
vlanInterface{logicalInterface{name: "testname", config: configMethodStatic{hwaddress: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5})}}, 1, ""},
"[NetDev]\nKind=vlan\nName=testname\nMACAddress=00:01:02:03:04:05\n\n[VLAN]\nId=1\n",
},
{
vlanInterface{logicalInterface{name: "testname", config: configMethodDHCP{hwaddress: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5})}}, 1, ""},
"[NetDev]\nKind=vlan\nName=testname\nMACAddress=00:01:02:03:04:05\n\n[VLAN]\nId=1\n",
},
} {
if tt.i.Netdev() != tt.l {
t.Fatalf("bad netdev config (%q): got %q, want %q", tt.i, tt.i.Netdev(), tt.l)
}
}
}
@@ -208,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{
@@ -224,6 +292,81 @@ func TestBuildInterfacesLo(t *testing.T) {
}
}
func TestBuildInterfacesBlindBond(t *testing.T) {
stanzas := []*stanzaInterface{
{
name: "bond0",
kind: interfaceBond,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"bond-slaves": []string{"eth0"},
},
},
}
interfaces := buildInterfaces(stanzas)
bond0 := &bondInterface{
logicalInterface{
name: "bond0",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
[]string{"eth0"},
map[string]string{},
}
eth0 := &physicalInterface{
logicalInterface{
name: "eth0",
config: configMethodManual{},
children: []networkInterface{bond0},
configDepth: 1,
},
}
expect := []InterfaceGenerator{bond0, eth0}
if !reflect.DeepEqual(interfaces, expect) {
t.FailNow()
}
}
func TestBuildInterfacesBlindVLAN(t *testing.T) {
stanzas := []*stanzaInterface{
{
name: "vlan0",
kind: interfaceVLAN,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"id": []string{"0"},
"raw_device": []string{"eth0"},
},
},
}
interfaces := buildInterfaces(stanzas)
vlan0 := &vlanInterface{
logicalInterface{
name: "vlan0",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
0,
"eth0",
}
eth0 := &physicalInterface{
logicalInterface{
name: "eth0",
config: configMethodManual{},
children: []networkInterface{vlan0},
configDepth: 1,
},
}
expect := []InterfaceGenerator{eth0, vlan0}
if !reflect.DeepEqual(interfaces, expect) {
t.FailNow()
}
}
func TestBuildInterfaces(t *testing.T) {
stanzas := []*stanzaInterface{
&stanzaInterface{
@@ -239,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{
@@ -248,7 +393,7 @@ func TestBuildInterfaces(t *testing.T) {
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"slaves": []string{"bond0"},
"bond-slaves": []string{"bond0"},
},
},
&stanzaInterface{
@@ -275,43 +420,53 @@ func TestBuildInterfaces(t *testing.T) {
interfaces := buildInterfaces(stanzas)
vlan1 := &vlanInterface{
logicalInterface{
name: "vlan1",
config: configMethodManual{},
children: []InterfaceGenerator{},
name: "vlan1",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
1,
"bond0",
}
vlan0 := &vlanInterface{
logicalInterface{
name: "vlan0",
config: configMethodManual{},
children: []InterfaceGenerator{},
name: "vlan0",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
0,
"eth0",
}
bond1 := &bondInterface{
logicalInterface{
name: "bond1",
config: configMethodManual{},
children: []InterfaceGenerator{},
name: "bond1",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
[]string{"bond0"},
map[string]string{},
}
bond0 := &bondInterface{
logicalInterface{
name: "bond0",
config: configMethodManual{},
children: []InterfaceGenerator{vlan1, bond1},
name: "bond0",
config: configMethodManual{},
children: []networkInterface{bond1, vlan1},
configDepth: 1,
},
[]string{"eth0"},
map[string]string{
"mode": "4",
"miimon": "100",
},
}
eth0 := &physicalInterface{
logicalInterface{
name: "eth0",
config: configMethodManual{},
children: []InterfaceGenerator{vlan0, bond0},
name: "eth0",
config: configMethodManual{},
children: []networkInterface{bond0, vlan0},
configDepth: 2,
},
}
expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1}
@@ -319,3 +474,19 @@ func TestBuildInterfaces(t *testing.T) {
t.FailNow()
}
}
func TestFilename(t *testing.T) {
for _, tt := range []struct {
i logicalInterface
f string
}{
{logicalInterface{name: "iface", configDepth: 0}, "00-iface"},
{logicalInterface{name: "iface", configDepth: 9}, "09-iface"},
{logicalInterface{name: "iface", configDepth: 10}, "0a-iface"},
{logicalInterface{name: "iface", configDepth: 53}, "35-iface"},
} {
if tt.i.Filename() != tt.f {
t.Fatalf("bad filename (%q): got %q, want %q", tt.i, tt.i.Filename(), tt.f)
}
}
}

View File

@@ -40,13 +40,16 @@ type configMethodStatic struct {
address net.IPNet
nameservers []net.IP
routes []route
hwaddress net.HardwareAddr
}
type configMethodLoopback struct{}
type configMethodManual struct{}
type configMethodDHCP struct{}
type configMethodDHCP struct {
hwaddress net.HardwareAddr
}
func parseStanzas(lines []string) (stanzas []stanza, err error) {
rawStanzas, err := splitStanzas(lines)
@@ -96,7 +99,7 @@ func splitStanzas(lines []string) ([][]string, error) {
} else if curStanza != nil {
curStanza = append(curStanza, line)
} else {
return nil, fmt.Errorf("missing stanza start '%s'", line)
return nil, fmt.Errorf("missing stanza start %q", line)
}
}
@@ -142,7 +145,7 @@ func parseStanza(rawStanza []string) (stanza, error) {
case "iface":
return parseInterfaceStanza(attributes, rawStanza[1:])
default:
return nil, fmt.Errorf("unknown stanza '%s'", kind)
return nil, fmt.Errorf("unknown stanza %q", kind)
}
}
@@ -204,7 +207,7 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa
}
}
if config.address.IP == nil || config.address.Mask == nil {
return nil, fmt.Errorf("malformed static network config for '%s'", iface)
return nil, fmt.Errorf("malformed static network config for %q", iface)
}
if gateways, ok := optionMap["gateway"]; ok {
if len(gateways) == 1 {
@@ -217,6 +220,11 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa
})
}
}
if hwaddress, err := parseHwaddress(optionMap, iface); err == nil {
config.hwaddress = hwaddress
} else {
return nil, err
}
for _, nameserver := range optionMap["dns-nameservers"] {
config.nameservers = append(config.nameservers, net.ParseIP(nameserver))
}
@@ -245,9 +253,15 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa
case "manual":
conf = configMethodManual{}
case "dhcp":
conf = configMethodDHCP{}
config := configMethodDHCP{}
if hwaddress, err := parseHwaddress(optionMap, iface); err == nil {
config.hwaddress = hwaddress
} else {
return nil, err
}
conf = config
default:
return nil, fmt.Errorf("invalid config method '%s'", confMethod)
return nil, fmt.Errorf("invalid config method %q", confMethod)
}
if _, ok := optionMap["vlan_raw_device"]; ok {
@@ -265,8 +279,20 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa
return parsePhysicalStanza(iface, conf, attributes, optionMap)
}
func parseHwaddress(options map[string][]string, iface string) (net.HardwareAddr, error) {
if hwaddress, ok := options["hwaddress"]; ok && len(hwaddress) == 2 {
switch hwaddress[0] {
case "ether":
if address, err := net.ParseMAC(hwaddress[1]); err == nil {
return address, nil
}
return nil, fmt.Errorf("malformed hwaddress option for %q", iface)
}
}
return nil, nil
}
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
}
@@ -282,11 +308,11 @@ func parseVLANStanza(iface string, conf configMethod, attributes []string, optio
} else if strings.HasPrefix(iface, "vlan") {
id = strings.TrimPrefix(iface, "vlan")
} else {
return nil, fmt.Errorf("malformed vlan name %s", iface)
return nil, fmt.Errorf("malformed vlan name %q", iface)
}
if _, err := strconv.Atoi(id); err != nil {
return nil, fmt.Errorf("malformed vlan name %s", iface)
return nil, fmt.Errorf("malformed vlan name %q", iface)
}
options["id"] = []string{id}
options["raw_device"] = options["vlan_raw_device"]

View File

@@ -42,6 +42,8 @@ func TestBadParseInterfaceStanza(t *testing.T) {
{[]string{"eth", "inet", "static"}, []string{"netmask 255.255.255.0"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"address invalid", "netmask 255.255.255.0"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100", "netmask invalid"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100", "netmask 255.255.255.0", "hwaddress ether NotAnAddress"}, "malformed hwaddress option"},
{[]string{"eth", "inet", "dhcp"}, []string{"hwaddress ether NotAnAddress"}, "malformed hwaddress option"},
} {
_, err := parseInterfaceStanza(tt.in, tt.opts)
if err == nil || !strings.HasPrefix(err.Error(), tt.e) {
@@ -127,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()
}
}
@@ -150,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) {
@@ -407,7 +406,46 @@ func TestParseInterfaceStanzaOptions(t *testing.T) {
}
}
func TestParseInterfaceStazaBond(t *testing.T) {
func TestParseInterfaceStanzaHwaddress(t *testing.T) {
for _, tt := range []struct {
attr []string
opt []string
hw net.HardwareAddr
}{
{
[]string{"mybond", "inet", "dhcp"},
[]string{},
nil,
},
{
[]string{"mybond", "inet", "dhcp"},
[]string{"hwaddress ether 00:01:02:03:04:05"},
net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}),
},
{
[]string{"mybond", "inet", "static"},
[]string{"hwaddress ether 00:01:02:03:04:05", "address 192.168.1.100", "netmask 255.255.255.0"},
net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}),
},
} {
iface, err := parseInterfaceStanza(tt.attr, tt.opt)
if err != nil {
t.Fatalf("error in parseInterfaceStanza (%q, %q): %q", tt.attr, tt.opt, err)
}
switch c := iface.configMethod.(type) {
case configMethodStatic:
if !reflect.DeepEqual(c.hwaddress, tt.hw) {
t.Fatalf("bad hwaddress (%q, %q): got %q, want %q", tt.attr, tt.opt, c.hwaddress, tt.hw)
}
case configMethodDHCP:
if !reflect.DeepEqual(c.hwaddress, tt.hw) {
t.Fatalf("bad hwaddress (%q, %q): got %q, want %q", tt.attr, tt.opt, c.hwaddress, tt.hw)
}
}
}
}
func TestParseInterfaceStanzaBond(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"mybond", "inet", "manual"}, []string{"bond-slaves eth"})
if err != nil {
t.FailNow()
@@ -417,7 +455,7 @@ func TestParseInterfaceStazaBond(t *testing.T) {
}
}
func TestParseInterfaceStazaVLANName(t *testing.T) {
func TestParseInterfaceStanzaVLANName(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"eth0.1", "inet", "manual"}, nil)
if err != nil {
t.FailNow()
@@ -427,7 +465,7 @@ func TestParseInterfaceStazaVLANName(t *testing.T) {
}
}
func TestParseInterfaceStazaVLANOption(t *testing.T) {
func TestParseInterfaceStanzaVLANOption(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"vlan1", "inet", "manual"}, []string{"vlan_raw_device eth"})
if err != nil {
t.FailNow()

View File

@@ -18,6 +18,28 @@ const (
HTTP_4xx = 4
)
type Err error
type ErrTimeout struct {
Err
}
type ErrNotFound struct {
Err
}
type ErrInvalid struct {
Err
}
type ErrServer struct {
Err
}
type ErrNetwork struct {
Err
}
type HttpClient struct {
// Maximum exp backoff duration. Defaults to 5 seconds
MaxBackoff time.Duration
@@ -31,18 +53,47 @@ type HttpClient struct {
// Whether or not to skip TLS verification. Defaults to false
SkipTLS bool
client *http.Client
}
type Getter interface {
Get(string) ([]byte, error)
GetRetry(string) ([]byte, error)
}
func NewHttpClient() *HttpClient {
return &HttpClient{
hc := &HttpClient{
MaxBackoff: time.Second * 5,
MaxRetries: 15,
Timeout: time.Duration(2) * time.Second,
SkipTLS: false,
}
// We need to create our own client in order to add timeout support.
// TODO(c4milo) Replace it once Go 1.3 is officially used by CoreOS
// More info: https://code.google.com/p/go/source/detail?r=ada6f2d5f99f
hc.client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: hc.SkipTLS,
},
Dial: func(network, addr string) (net.Conn, error) {
deadline := time.Now().Add(hc.Timeout)
c, err := net.DialTimeout(network, addr, hc.Timeout)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
},
},
}
return hc
}
func expBackoff(interval, max time.Duration) time.Duration {
func ExpBackoff(interval, max time.Duration) time.Duration {
interval = interval * 2
if interval > max {
interval = max
@@ -50,74 +101,61 @@ func expBackoff(interval, max time.Duration) time.Duration {
return interval
}
// Fetches a given URL with support for exponential backoff and maximum retries
func (h *HttpClient) Get(rawurl string) ([]byte, error) {
// GetRetry fetches a given URL with support for exponential backoff and maximum retries
func (h *HttpClient) GetRetry(rawurl string) ([]byte, error) {
if rawurl == "" {
return nil, errors.New("URL is empty. Skipping.")
return nil, ErrInvalid{errors.New("URL is empty. Skipping.")}
}
url, err := neturl.Parse(rawurl)
if err != nil {
return nil, err
return nil, ErrInvalid{err}
}
// Unfortunately, url.Parse is too generic to throw errors if a URL does not
// have a valid HTTP scheme. So, we have to do this extra validation
if !strings.HasPrefix(url.Scheme, "http") {
return nil, fmt.Errorf("URL %s does not have a valid HTTP scheme. Skipping.", rawurl)
return nil, ErrInvalid{fmt.Errorf("URL %s does not have a valid HTTP scheme. Skipping.", rawurl)}
}
dataURL := url.String()
// We need to create our own client in order to add timeout support.
// TODO(c4milo) Replace it once Go 1.3 is officially used by CoreOS
// More info: https://code.google.com/p/go/source/detail?r=ada6f2d5f99f
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: h.SkipTLS,
},
Dial: func(network, addr string) (net.Conn, error) {
deadline := time.Now().Add(h.Timeout)
c, err := net.DialTimeout(network, addr, h.Timeout)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
},
}
client := &http.Client{
Transport: transport,
}
duration := 50 * time.Millisecond
for retry := 1; retry <= h.MaxRetries; retry++ {
log.Printf("Fetching data from %s. Attempt #%d", dataURL, retry)
resp, err := client.Get(dataURL)
if err == nil {
defer resp.Body.Close()
status := resp.StatusCode / 100
if status == HTTP_2xx {
return ioutil.ReadAll(resp.Body)
}
if status == HTTP_4xx {
return nil, fmt.Errorf("Not found. HTTP status code: %d", resp.StatusCode)
}
log.Printf("Server error. HTTP status code: %d", resp.StatusCode)
} else {
log.Printf("Unable to fetch data: %s", err.Error())
data, err := h.Get(dataURL)
switch err.(type) {
case ErrNetwork:
log.Printf(err.Error())
case ErrServer:
log.Printf(err.Error())
case ErrNotFound:
return data, err
default:
return data, err
}
duration = expBackoff(duration, h.MaxBackoff)
duration = ExpBackoff(duration, h.MaxBackoff)
log.Printf("Sleeping for %v...", duration)
time.Sleep(duration)
}
return nil, fmt.Errorf("Unable to fetch data. Maximum retries reached: %d", h.MaxRetries)
return nil, ErrTimeout{fmt.Errorf("Unable to fetch data. Maximum retries reached: %d", h.MaxRetries)}
}
func (h *HttpClient) Get(dataURL string) ([]byte, error) {
if resp, err := h.client.Get(dataURL); err == nil {
defer resp.Body.Close()
switch resp.StatusCode / 100 {
case HTTP_2xx:
return ioutil.ReadAll(resp.Body)
case HTTP_4xx:
return nil, ErrNotFound{fmt.Errorf("Not found. HTTP status code: %d", resp.StatusCode)}
default:
return nil, ErrServer{fmt.Errorf("Server error. HTTP status code: %d", resp.StatusCode)}
}
} else {
return nil, ErrNetwork{fmt.Errorf("Unable to fetch data: %s", err.Error())}
}
}

View File

@@ -14,7 +14,7 @@ func TestExpBackoff(t *testing.T) {
duration := time.Millisecond
max := time.Hour
for i := 0; i < math.MaxUint16; i++ {
duration = expBackoff(duration, max)
duration = ExpBackoff(duration, max)
if duration < 0 {
t.Fatalf("duration too small: %v %v", duration, i)
}
@@ -51,7 +51,7 @@ func TestGetURLExpBackOff(t *testing.T) {
ts := httptest.NewServer(mux)
defer ts.Close()
data, err := client.Get(ts.URL)
data, err := client.GetRetry(ts.URL)
if err != nil {
t.Errorf("Test case %d produced error: %v", i, err)
}
@@ -76,7 +76,7 @@ func TestGetURL4xx(t *testing.T) {
}))
defer ts.Close()
_, err := client.Get(ts.URL)
_, err := client.GetRetry(ts.URL)
if err == nil {
t.Errorf("Incorrect result\ngot: %s\nwant: %s", err.Error(), "Not found. HTTP status code: 404")
}
@@ -107,7 +107,7 @@ coreos:
}))
defer ts.Close()
data, err := client.Get(ts.URL)
data, err := client.GetRetry(ts.URL)
if err != nil {
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, nil)
}
@@ -132,7 +132,7 @@ func TestGetMalformedURL(t *testing.T) {
}
for _, test := range tests {
_, err := client.Get(test.url)
_, err := client.GetRetry(test.url)
if err == nil || err.Error() != test.want {
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, test.want)
}

100
system/env_file.go Normal file
View File

@@ -0,0 +1,100 @@
package system
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path"
"regexp"
"sort"
)
type EnvFile struct {
Vars map[string]string
// mask File.Content, it shouldn't be used.
Content interface{} `json:"-" yaml:"-"`
*File
}
// only allow sh compatible identifiers
var validKey = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
// match each line, optionally capturing valid identifiers, discarding dos line endings
var lineLexer = regexp.MustCompile(`(?m)^((?:([a-zA-Z0-9_]+)=)?.*?)\r?\n`)
// mergeEnvContents: Update the existing file contents with new values,
// preserving variable ordering and all content this code doesn't understand.
// All new values are appended to the bottom of the old, sorted by key.
func mergeEnvContents(old []byte, pending map[string]string) []byte {
var buf bytes.Buffer
var match [][]byte
// it is awkward for the regex to handle a missing newline gracefully
if len(old) != 0 && !bytes.HasSuffix(old, []byte{'\n'}) {
old = append(old, byte('\n'))
}
for _, match = range lineLexer.FindAllSubmatch(old, -1) {
key := string(match[2])
if value, ok := pending[key]; ok {
fmt.Fprintf(&buf, "%s=%s\n", key, value)
delete(pending, key)
} else {
fmt.Fprintf(&buf, "%s\n", match[1])
}
}
for _, key := range keys(pending) {
value := pending[key]
fmt.Fprintf(&buf, "%s=%s\n", key, value)
}
return buf.Bytes()
}
// WriteEnvFile updates an existing env `KEY=value` formated file with
// new values provided in EnvFile.Vars; File.Content is ignored.
// Existing ordering and any unknown formatting such as comments are
// preserved. If no changes are required the file is untouched.
func WriteEnvFile(ef *EnvFile, root string) error {
// validate new keys, mergeEnvContents uses pending to track writes
pending := make(map[string]string, len(ef.Vars))
for key, value := range ef.Vars {
if !validKey.MatchString(key) {
return fmt.Errorf("Invalid name %q for %s", key, ef.Path)
}
pending[key] = value
}
if len(pending) == 0 {
return nil
}
oldContent, err := ioutil.ReadFile(path.Join(root, ef.Path))
if err != nil {
if os.IsNotExist(err) {
oldContent = []byte{}
} else {
return err
}
}
newContent := mergeEnvContents(oldContent, pending)
if bytes.Equal(oldContent, newContent) {
return nil
}
ef.File.Content = string(newContent)
_, err = WriteFile(ef.File, root)
return err
}
// keys returns the keys of a map in sorted order
func keys(m map[string]string) (s []string) {
for k, _ := range m {
s = append(s, k)
}
sort.Strings(s)
return
}

426
system/env_file_test.go Normal file
View File

@@ -0,0 +1,426 @@
package system
import (
"io/ioutil"
"os"
"path"
"strings"
"syscall"
"testing"
)
const (
base = "# a file\nFOO=base\n\nBAR= hi there\n"
baseNoNewline = "# a file\nFOO=base\n\nBAR= hi there"
baseDos = "# a file\r\nFOO=base\r\n\r\nBAR= hi there\r\n"
expectUpdate = "# a file\nFOO=test\n\nBAR= hi there\nNEW=a value\n"
expectCreate = "FOO=test\nNEW=a value\n"
)
var (
valueUpdate = map[string]string{
"FOO": "test",
"NEW": "a value",
}
valueNoop = map[string]string{
"FOO": "base",
}
valueEmpty = map[string]string{}
valueInvalid = map[string]string{
"FOO-X": "test",
}
)
func TestWriteEnvFileUpdate(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)
name := "foo.conf"
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,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
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) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
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,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
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) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != expectCreate {
t.Fatalf("File has incorrect contents: %q", contents)
}
}
func TestWriteEnvFileNoop(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)
name := "foo.conf"
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,
},
Vars: valueNoop,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != base {
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 replaced: %s", fullPath)
}
}
func TestWriteEnvFileUpdateDos(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)
name := "foo.conf"
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,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
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.
// Seems reasonable to rewrite the file in Unix format anyway.
func TestWriteEnvFileDos2Unix(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)
name := "foo.conf"
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,
},
Vars: valueNoop,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != base {
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)
}
}
// If it really is a noop (structure is empty) don't even do dos2unix
func TestWriteEnvFileEmpty(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)
name := "foo.conf"
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,
},
Vars: valueEmpty,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != baseDos {
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 replaced: %s", fullPath)
}
}
// no point in creating empty files
func TestWriteEnvFileEmptyNoCreate(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)
name := "foo.conf"
fullPath := path.Join(dir, name)
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueEmpty,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err == nil {
t.Fatalf("File has incorrect contents: %q", contents)
} else if !os.IsNotExist(err) {
t.Fatalf("Unexpected error while reading file: %v", err)
}
}
func TestWriteEnvFilePermFailure(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)
name := "foo.conf"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(base), 0000)
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if !os.IsPermission(err) {
t.Fatalf("Not a pemission denied error: %v", err)
}
}
func TestWriteEnvFileNameFailure(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)
name := "foo.conf"
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueInvalid,
}
err = WriteEnvFile(&ef, dir)
if err == nil || !strings.HasPrefix(err.Error(), "Invalid name") {
t.Fatalf("Not an invalid name error: %v", err)
}
}

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,16 +34,17 @@ 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 {
iface := iface
sysInterfaceMap[iface.Name] = &iface
}
} else {
@@ -44,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)
}
@@ -53,26 +63,45 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
return nil
}
func probe8012q() error {
return exec.Command("modprobe", "8021q").Run()
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.Name()))
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.Name()))
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.Name()))
filename = fmt.Sprintf("%s.network", iface.Filename())
if err := writeConfig(filename, iface.Network()); err != nil {
return err
}
@@ -84,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)
}
}

13
test
View File

@@ -13,12 +13,21 @@ COVER=${COVER:-"-cover"}
source ./build
declare -a TESTPKGS=(initialize system datasource pkg network)
declare -a TESTPKGS=(initialize
system
datasource
datasource/configdrive
datasource/file
datasource/metadata/ec2
datasource/proc_cmdline
datasource/url
pkg
network)
if [ -z "$PKG" ]; then
GOFMTPATH="$TESTPKGS coreos-cloudinit.go"
# prepend repo path to each package
TESTPKGS=${TESTPKGS[@]/#/${REPO_PATH}/}
TESTPKGS="${TESTPKGS[@]/#/${REPO_PATH}/} ./"
else
GOFMTPATH="$TESTPKGS"
# strip out slashes and dots from PKG=./foo/

View File

@@ -1,7 +1,7 @@
[Unit]
Description=Load cloud-config from /media/configdrive
Requires=coreos-setup-environment.service
After=coreos-setup-environment.service
After=coreos-setup-environment.service system-config.target
Before=user-config.target
# HACK: work around ordering between config drive and ec2 metadata It is
@@ -13,7 +13,6 @@ Before=user-config.target
# systemd knows about the ordering as early as possible.
# coreos-cloudinit could implement a simple lock but that cannot be used
# until after the systemd dbus calls are made non-blocking.
After=system-cloudinit@usr-share-oem-cloud\x2dconfig.yml.service
After=ec2-cloudinit.service
[Service]