Compare commits
133 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5d65ca230a | ||
|
38b3e1213a | ||
|
4eedca26e9 | ||
|
f2b342c8be | ||
|
c19d8f6b61 | ||
|
7913f74351 | ||
|
5593408be8 | ||
|
7fc67c2acf | ||
|
b093094292 | ||
|
9a80fd714a | ||
|
fef5473881 | ||
|
bf5a2b208f | ||
|
364507fb75 | ||
|
08d4842502 | ||
|
21e32e44f8 | ||
|
7a06dee16f | ||
|
ff9cf5743d | ||
|
1b10a3a187 | ||
|
10838e001d | ||
|
96370ac5b9 | ||
|
0b82cd074d | ||
|
a974e85103 | ||
|
f0450662b0 | ||
|
03e29d1291 | ||
|
98ae5d88aa | ||
|
bf5d3539c9 | ||
|
5e4cbcd909 | ||
|
a270c4c737 | ||
|
f356a8a690 | ||
|
b1a897d75c | ||
|
be51f4eba0 | ||
|
a55e2cd49b | ||
|
983501e43b | ||
|
e3037f18a6 | ||
|
fe388a3ab6 | ||
|
c820f2b1cf | ||
|
81824be3bf | ||
|
98c26440be | ||
|
3b5fcc393b | ||
|
9528077340 | ||
|
4355a05d55 | ||
|
52c44923dd | ||
|
47748ef4b6 | ||
|
8eca10200e | ||
|
43be8c8996 | ||
|
19b4b1160e | ||
|
ce6fccfb3c | ||
|
7d89aefb82 | ||
|
2369e2a920 | ||
|
6d808048d3 | ||
|
276f0b5d99 | ||
|
92bd5ca5d4 | ||
|
5b5ffea126 | ||
|
18068e9375 | ||
|
1b3cabb035 | ||
|
1be2bec1c2 | ||
|
f3bd5f543e | ||
|
660feb59b9 | ||
|
9673dbe12b | ||
|
2be435dd83 | ||
|
2d91369596 | ||
|
d8d3928978 | ||
|
7fcc540154 | ||
|
cb7fbd4668 | ||
|
d4e048a1f4 | ||
|
231c0fa20b | ||
|
1aabacc769 | ||
|
6a2927d701 | ||
|
126188510b | ||
|
4627ccb444 | ||
|
aff372111a | ||
|
c7081b9918 | ||
|
9ba3b18b59 | ||
|
099de62e9a | ||
|
c089216cb5 | ||
|
68dc902ed1 | ||
|
ad66b1c92f | ||
|
fbdece2762 | ||
|
f85eafb7ca | ||
|
f0dba2294e | ||
|
bda3948382 | ||
|
fae81c78f3 | ||
|
a5dec7d7bd | ||
|
e1222c9885 | ||
|
ded3bcf122 | ||
|
80d00cde94 | ||
|
2805d70ece | ||
|
439b7e8b98 | ||
|
ba1c1e97d0 | ||
|
8a50fd8595 | ||
|
465bcce72c | ||
|
361edeebc6 | ||
|
29a7b0e34f | ||
|
8496ffb53a | ||
|
2c717a6cd1 | ||
|
13a91c9181 | ||
|
338e1b64ab | ||
|
8eb0636034 | ||
|
f7c25a1b83 | ||
|
d6a0d0908c | ||
|
5c89afc18a | ||
|
376cc4bcac | ||
|
d0a6d6f92f | ||
|
2be1e52f32 | ||
|
784a71e2bf | ||
|
e6cf83a2e5 | ||
|
840c208b60 | ||
|
29ed6b38bd | ||
|
259c7e1fe2 | ||
|
033c8d352f | ||
|
16d7e8af48 | ||
|
159f4a2c7c | ||
|
160668284c | ||
|
41b9dfcb1c | ||
|
ef4c3483b6 | ||
|
4bdf633075 | ||
|
c9fc718e18 | ||
|
4461b3d33d | ||
|
c6a1412f6b | ||
|
d0cbbd2007 | ||
|
7b5e542eb4 | ||
|
376d82ba63 | ||
|
a6aa9f82b8 | ||
|
00ee047753 | ||
|
f127406d01 | ||
|
0ddc08d55a | ||
|
56f455f890 | ||
|
dd861b9f88 | ||
|
f7d01da267 | ||
|
fc8f30bf08 | ||
|
075c0557e7 | ||
|
d25e13a2c6 | ||
|
cf1ffad533 |
@@ -13,7 +13,7 @@ If no **id** field is provided, coreos-cloudinit will ignore this section.
|
|||||||
|
|
||||||
For example, the following cloud-config document...
|
For example, the following cloud-config document...
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
coreos:
|
coreos:
|
||||||
oem:
|
oem:
|
||||||
@@ -26,7 +26,7 @@ coreos:
|
|||||||
|
|
||||||
...would be rendered to the following `/etc/oem-release`:
|
...would be rendered to the following `/etc/oem-release`:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
ID=rackspace
|
ID=rackspace
|
||||||
NAME="Rackspace Cloud Servers"
|
NAME="Rackspace Cloud Servers"
|
||||||
VERSION_ID=168.0.0
|
VERSION_ID=168.0.0
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Using Cloud-Config
|
# Using Cloud-Config
|
||||||
|
|
||||||
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime.
|
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
|
## 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.
|
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`
|
- `coreos`
|
||||||
- `ssh_authorized_keys`
|
- `ssh_authorized_keys`
|
||||||
@@ -40,9 +40,9 @@ CoreOS tries to conform to each platform's native method to provide user data. E
|
|||||||
#### etcd
|
#### etcd
|
||||||
|
|
||||||
The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
|
The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
|
||||||
We can use the templating feature of coreos-cloudinit to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
|
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
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
@@ -57,7 +57,7 @@ coreos:
|
|||||||
|
|
||||||
...will generate a systemd unit drop-in like this:
|
...will generate a systemd unit drop-in like this:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
[Service]
|
[Service]
|
||||||
Environment="ETCD_NAME=node001"
|
Environment="ETCD_NAME=node001"
|
||||||
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
|
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
|
||||||
@@ -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].
|
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 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
|
[etcd-config]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md
|
||||||
|
|
||||||
#### fleet
|
#### 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...
|
The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allow for the configuration of fleet through environment variables. For example, the following cloud-config document...
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
@@ -85,7 +87,7 @@ coreos:
|
|||||||
|
|
||||||
...will generate a systemd unit drop-in like this:
|
...will generate a systemd unit drop-in like this:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
[Service]
|
[Service]
|
||||||
Environment="FLEET_PUBLIC_IP=203.0.113.29"
|
Environment="FLEET_PUBLIC_IP=203.0.113.29"
|
||||||
Environment="FLEET_METADATA=region=us-west"
|
Environment="FLEET_METADATA=region=us-west"
|
||||||
@@ -99,7 +101,8 @@ For more information on fleet configuration, see the [fleet documentation][fleet
|
|||||||
|
|
||||||
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
|
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
|
||||||
|
|
||||||
These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field.
|
These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field.
|
||||||
|
The `reboot-strategy` parameter also affects the behaviour of [locksmith](https://github.com/coreos/locksmith).
|
||||||
|
|
||||||
- **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed.
|
- **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed.
|
||||||
- _reboot_: Reboot immediately after an update is applied.
|
- _reboot_: Reboot immediately after an update is applied.
|
||||||
@@ -109,7 +112,11 @@ These fields will be written out to and replace `/etc/coreos/update.conf`. If on
|
|||||||
- **server**: is the omaha endpoint URL which will be queried for updates.
|
- **server**: is the omaha endpoint URL which will be queried for updates.
|
||||||
- **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable")
|
- **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable")
|
||||||
|
|
||||||
```
|
*Note: cloudinit will only manipulate the locksmith unit file in the systemd runtime directory (`/run/systemd/system/locksmithd.service`). If any manual modifications are made to an overriding unit configuration file (e.g. `/etc/systemd/system/locksmithd.service`), cloudinit will no longer be able to control the locksmith service unit.*
|
||||||
|
|
||||||
|
##### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
coreos:
|
coreos:
|
||||||
update:
|
update:
|
||||||
@@ -118,7 +125,9 @@ coreos:
|
|||||||
|
|
||||||
#### units
|
#### 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.
|
- **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.
|
- **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.
|
||||||
@@ -133,7 +142,7 @@ The `coreos.units.*` parameters define a list of arbitrary systemd units to star
|
|||||||
|
|
||||||
Write a unit to disk, automatically starting it.
|
Write a unit to disk, automatically starting it.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
@@ -150,14 +159,11 @@ coreos:
|
|||||||
Restart=always
|
Restart=always
|
||||||
ExecStart=/usr/bin/docker start -a redis_server
|
ExecStart=/usr/bin/docker start -a redis_server
|
||||||
ExecStop=/usr/bin/docker stop -t 2 redis_server
|
ExecStop=/usr/bin/docker stop -t 2 redis_server
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=local.target
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the built-in `etcd` and `fleet` services:
|
Start the built-in `etcd` and `fleet` services:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
@@ -175,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.
|
The keys will be named "coreos-cloudinit" by default.
|
||||||
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
|
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
ssh_authorized_keys:
|
ssh_authorized_keys:
|
||||||
@@ -187,7 +193,7 @@ ssh_authorized_keys:
|
|||||||
The `hostname` parameter defines the system's hostname.
|
The `hostname` parameter defines the system's hostname.
|
||||||
This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
|
This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
hostname: coreos1
|
hostname: coreos1
|
||||||
@@ -201,7 +207,7 @@ All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the use
|
|||||||
- **name**: Required. Login name of user
|
- **name**: Required. Login name of user
|
||||||
- **gecos**: GECOS comment of user
|
- **gecos**: GECOS comment of user
|
||||||
- **passwd**: Hash of the password to use for this 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.
|
- **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.
|
- **primary-group**: Default group for the user. Defaults to a new group created named after the user.
|
||||||
- **groups**: Add user to these additional groups
|
- **groups**: Add user to these additional groups
|
||||||
@@ -220,7 +226,7 @@ The following fields are not yet implemented:
|
|||||||
- **selinux-user**: Corresponding SELinux user
|
- **selinux-user**: Corresponding SELinux user
|
||||||
- **ssh-import-id**: Import SSH keys by ID from Launchpad.
|
- **ssh-import-id**: Import SSH keys by ID from Launchpad.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
users:
|
users:
|
||||||
@@ -259,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.
|
Using the `coreos-ssh-import-github` field, we can import public SSH keys from a GitHub user to use as authorized keys to a server.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
users:
|
users:
|
||||||
@@ -272,17 +278,17 @@ users:
|
|||||||
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
|
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
|
||||||
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
|
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
users:
|
users:
|
||||||
- name: elroy
|
- name: elroy
|
||||||
coreos-ssh-import-url: https://token:<OAUTH-TOKEN>@github-enterprise.example.com/users/elroy/keys
|
coreos-ssh-import-url: https://github-enterprise.example.com/api/v3/users/elroy/keys?access_token=<TOKEN>
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also specify any URL whose response matches the JSON format for public keys:
|
You can also specify any URL whose response matches the JSON format for public keys:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
users:
|
users:
|
||||||
@@ -302,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.
|
Explicitly not implemented is the **encoding** attribute.
|
||||||
The **content** field must represent exactly what should be written to disk.
|
The **content** field must represent exactly what should be written to disk.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
write_files:
|
write_files:
|
||||||
- path: /etc/fleet/fleet.conf
|
- path: /etc/fleet/fleet.conf
|
||||||
@@ -319,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
|
to resolve to "127.0.0.1". This is helpful when the host does not have DNS
|
||||||
infrastructure in place to resolve its own hostname, for example, when using Vagrant.
|
infrastructure in place to resolve its own hostname, for example, when using Vagrant.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
manage_etc_hosts: localhost
|
manage_etc_hosts: localhost
|
||||||
|
@@ -14,17 +14,21 @@ The image should be a single FAT or ISO9660 file system with the label
|
|||||||
|
|
||||||
For example, to wrap up a config named `user_data` in a config drive image:
|
For example, to wrap up a config named `user_data` in a config drive image:
|
||||||
|
|
||||||
mkdir -p /tmp/new-drive/openstack/latest
|
```sh
|
||||||
cp user_data /tmp/new-drive/openstack/latest/user_data
|
mkdir -p /tmp/new-drive/openstack/latest
|
||||||
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
|
cp user_data /tmp/new-drive/openstack/latest/user_data
|
||||||
rm -r /tmp/new-drive
|
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
|
||||||
|
rm -r /tmp/new-drive
|
||||||
|
```
|
||||||
|
|
||||||
## QEMU virtfs
|
## QEMU virtfs
|
||||||
|
|
||||||
One exception to the above, when using QEMU it is possible to skip creating an
|
One exception to the above, when using QEMU it is possible to skip creating an
|
||||||
image and use a plain directory containing the same contents:
|
image and use a plain directory containing the same contents:
|
||||||
|
|
||||||
qemu-system-x86_64 \
|
```sh
|
||||||
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
|
qemu-system-x86_64 \
|
||||||
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
|
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
|
||||||
[usual qemu options here...]
|
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
|
||||||
|
[usual qemu options here...]
|
||||||
|
```
|
||||||
|
27
Documentation/debian-interfaces.md
Normal file
27
Documentation/debian-interfaces.md
Normal 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
|
@@ -1,71 +1,69 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/datasource"
|
"github.com/coreos/coreos-cloudinit/datasource"
|
||||||
"github.com/coreos/coreos-cloudinit/initialize"
|
"github.com/coreos/coreos-cloudinit/initialize"
|
||||||
"github.com/coreos/coreos-cloudinit/network"
|
"github.com/coreos/coreos-cloudinit/pkg"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "0.7.4"
|
const (
|
||||||
|
version = "0.9.1"
|
||||||
|
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
|
||||||
|
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, "Download data from 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>", datasource.ProcCmdlineLocation, datasource.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() {
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
|
die := func() {
|
||||||
|
if ignoreFailure {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
if printVersion == true {
|
if printVersion == true {
|
||||||
fmt.Printf("coreos-cloudinit version %s\n", version)
|
fmt.Printf("coreos-cloudinit version %s\n", version)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ds datasource.Datasource
|
if convertNetconf != "" && sources.configDrive == "" {
|
||||||
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 == "" {
|
|
||||||
fmt.Println("-convert-netconf flag requires -from-configdrive")
|
fmt.Println("-convert-netconf flag requires -from-configdrive")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -78,111 +76,206 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dss := getDatasources()
|
||||||
|
if len(dss) == 0 {
|
||||||
|
fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-metadata-service, --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())
|
fmt.Printf("Fetching user-data from datasource of type %q\n", ds.Type())
|
||||||
userdataBytes, err := ds.Fetch()
|
userdataBytes, err := ds.FetchUserdata()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed fetching user-data from datasource: %v\n", err)
|
fmt.Printf("Failed fetching user-data from datasource: %v\n", err)
|
||||||
if ignoreFailure {
|
die()
|
||||||
os.Exit(0)
|
}
|
||||||
} else {
|
|
||||||
os.Exit(1)
|
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)
|
// Apply environment to user-data
|
||||||
if len(userdataBytes) > 0 {
|
env := initialize.NewEnvironment("/", ds.ConfigRoot(), workspace, convertNetconf, sshKeyName, subs)
|
||||||
if err := processUserdata(string(userdataBytes), env); err != nil {
|
userdata := env.Apply(string(userdataBytes))
|
||||||
fmt.Printf("Failed resolving user-data: %v\n", err)
|
|
||||||
if !ignoreFailure {
|
var ccm, ccu *initialize.CloudConfig
|
||||||
os.Exit(1)
|
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 {
|
} else {
|
||||||
fmt.Println("No user data to handle.")
|
switch t := ud.(type) {
|
||||||
|
case *initialize.CloudConfig:
|
||||||
|
ccu = t
|
||||||
|
case system.Script:
|
||||||
|
script = &t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if convertNetconf != "" {
|
var cc *initialize.CloudConfig
|
||||||
if err := processNetconf(convertNetconf, configdrive); err != nil {
|
if ccm != nil && ccu != nil {
|
||||||
fmt.Printf("Failed to process network config: %v\n", err)
|
fmt.Println("Merging cloud-config from meta-data and user-data")
|
||||||
if !ignoreFailure {
|
merged := mergeCloudConfig(*ccm, *ccu)
|
||||||
os.Exit(1)
|
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 {
|
// mergeCloudConfig merges certain options from mdcc (a CloudConfig derived from
|
||||||
userdata = env.Apply(userdata)
|
// 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)", udcc.Hostname, mdcc.Hostname)
|
||||||
|
} else {
|
||||||
|
udcc.Hostname = mdcc.Hostname
|
||||||
|
}
|
||||||
|
|
||||||
parsed, err := initialize.ParseUserData(userdata)
|
}
|
||||||
if err != nil {
|
for _, key := range mdcc.SSHAuthorizedKeys {
|
||||||
fmt.Printf("Failed parsing user-data: %v\n", err)
|
udcc.SSHAuthorizedKeys = append(udcc.SSHAuthorizedKeys, key)
|
||||||
return err
|
}
|
||||||
|
if mdcc.NetworkConfigPath != "" {
|
||||||
|
if udcc.NetworkConfigPath != "" {
|
||||||
|
fmt.Printf("Warning: user-data NetworkConfigPath %s overrides metadata NetworkConfigPath %s", 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, datasource.NewLocalFile(sources.file))
|
||||||
|
}
|
||||||
|
if sources.url != "" {
|
||||||
|
dss = append(dss, datasource.NewRemoteFile(sources.url))
|
||||||
|
}
|
||||||
|
if sources.configDrive != "" {
|
||||||
|
dss = append(dss, datasource.NewConfigDrive(sources.configDrive))
|
||||||
|
}
|
||||||
|
if sources.metadataService {
|
||||||
|
dss = append(dss, datasource.NewMetadataService())
|
||||||
|
}
|
||||||
|
if sources.procCmdLine {
|
||||||
|
dss = append(dss, datasource.NewProcCmdline())
|
||||||
|
}
|
||||||
|
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.Tick(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.Tick(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 {
|
if err != nil {
|
||||||
fmt.Printf("Failed preparing workspace: %v\n", err)
|
fmt.Printf("Failed preparing workspace: %v\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
path, err := initialize.PersistScriptInWorkspace(script, env.Workspace())
|
||||||
switch t := parsed.(type) {
|
if err == nil {
|
||||||
case initialize.CloudConfig:
|
var name string
|
||||||
err = initialize.Apply(t, env)
|
name, err = system.ExecuteScript(path)
|
||||||
case system.Script:
|
initialize.PersistUnitNameInWorkspace(name, env.Workspace())
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
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
110
coreos-cloudinit_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,21 +7,58 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type configDrive struct {
|
type configDrive struct {
|
||||||
path string
|
root string
|
||||||
|
readFile func(filename string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigDrive(path string) *configDrive {
|
func NewConfigDrive(root string) *configDrive {
|
||||||
return &configDrive{path}
|
return &configDrive{root, ioutil.ReadFile}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *configDrive) Fetch() ([]byte, error) {
|
func (cd *configDrive) IsAvailable() bool {
|
||||||
data, err := ioutil.ReadFile(path.Join(self.path, "openstack", "latest", "user_data"))
|
_, err := os.Stat(cd.root)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) AvailabilityChanges() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) ConfigRoot() string {
|
||||||
|
return cd.openstackRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchMetadata attempts to retrieve metadata from ec2/2009-04-04/meta_data.json.
|
||||||
|
func (cd *configDrive) FetchMetadata() ([]byte, error) {
|
||||||
|
return cd.tryReadFile(path.Join(cd.ec2Root(), "meta_data.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchUserdata attempts to retrieve the userdata from ec2/2009-04-04/user_data.
|
||||||
|
// If no data is found, it will attempt to read from openstack/latest/user_data.
|
||||||
|
func (cd *configDrive) FetchUserdata() ([]byte, error) {
|
||||||
|
bytes, err := cd.tryReadFile(path.Join(cd.ec2Root(), "user_data"))
|
||||||
|
if bytes == nil && err == nil {
|
||||||
|
bytes, err = cd.tryReadFile(path.Join(cd.openstackRoot(), "user_data"))
|
||||||
|
}
|
||||||
|
return bytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) Type() string {
|
||||||
|
return "cloud-drive"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) ec2Root() string {
|
||||||
|
return path.Join(cd.root, "ec2", Ec2ApiVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) openstackRoot() string {
|
||||||
|
return path.Join(cd.root, "openstack", "latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) tryReadFile(filename string) ([]byte, error) {
|
||||||
|
data, err := cd.readFile(filename)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *configDrive) Type() string {
|
|
||||||
return "cloud-drive"
|
|
||||||
}
|
|
||||||
|
114
datasource/configdrive_test.go
Normal file
114
datasource/configdrive_test.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package datasource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockFilesystem []string
|
||||||
|
|
||||||
|
func (m mockFilesystem) readFile(filename string) ([]byte, error) {
|
||||||
|
for _, file := range m {
|
||||||
|
if file == filename {
|
||||||
|
return []byte(filename), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCDFetchMetadata(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
root string
|
||||||
|
filename string
|
||||||
|
files mockFilesystem
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"/",
|
||||||
|
"",
|
||||||
|
mockFilesystem{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/",
|
||||||
|
"/ec2/2009-04-04/meta_data.json",
|
||||||
|
mockFilesystem([]string{"/ec2/2009-04-04/meta_data.json"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/media/configdrive",
|
||||||
|
"/media/configdrive/ec2/2009-04-04/meta_data.json",
|
||||||
|
mockFilesystem([]string{"/media/configdrive/ec2/2009-04-04/meta_data.json"}),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
cd := configDrive{tt.root, tt.files.readFile}
|
||||||
|
filename, err := cd.FetchMetadata()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad error for %q: want %q, got %q", tt, nil, err)
|
||||||
|
}
|
||||||
|
if string(filename) != tt.filename {
|
||||||
|
t.Fatalf("bad path for %q: want %q, got %q", tt, tt.filename, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCDFetchUserdata(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
root string
|
||||||
|
filename string
|
||||||
|
files mockFilesystem
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"/",
|
||||||
|
"",
|
||||||
|
mockFilesystem{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/",
|
||||||
|
"/ec2/2009-04-04/user_data",
|
||||||
|
mockFilesystem([]string{"/ec2/2009-04-04/user_data"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/",
|
||||||
|
"/openstack/latest/user_data",
|
||||||
|
mockFilesystem([]string{"/openstack/latest/user_data"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/",
|
||||||
|
"/ec2/2009-04-04/user_data",
|
||||||
|
mockFilesystem([]string{"/openstack/latest/user_data", "/ec2/2009-04-04/user_data"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/media/configdrive",
|
||||||
|
"/media/configdrive/ec2/2009-04-04/user_data",
|
||||||
|
mockFilesystem([]string{"/media/configdrive/ec2/2009-04-04/user_data"}),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
cd := configDrive{tt.root, tt.files.readFile}
|
||||||
|
filename, err := cd.FetchUserdata()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad error for %q: want %q, got %q", tt, nil, err)
|
||||||
|
}
|
||||||
|
if string(filename) != tt.filename {
|
||||||
|
t.Fatalf("bad path for %q: want %q, got %q", tt, tt.filename, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCDConfigRoot(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
root string
|
||||||
|
configRoot string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"/",
|
||||||
|
"/openstack/latest",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/media/configdrive",
|
||||||
|
"/media/configdrive/openstack/latest",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
cd := configDrive{tt.root, nil}
|
||||||
|
if configRoot := cd.ConfigRoot(); configRoot != tt.configRoot {
|
||||||
|
t.Fatalf("bad config root for %q: want %q, got %q", tt, tt.configRoot, configRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,15 @@
|
|||||||
package datasource
|
package datasource
|
||||||
|
|
||||||
|
const (
|
||||||
|
Ec2ApiVersion = "2009-04-04"
|
||||||
|
OpenstackApiVersion = "2012-08-10"
|
||||||
|
)
|
||||||
|
|
||||||
type Datasource interface {
|
type Datasource interface {
|
||||||
Fetch() ([]byte, error)
|
IsAvailable() bool
|
||||||
|
AvailabilityChanges() bool
|
||||||
|
ConfigRoot() string
|
||||||
|
FetchMetadata() ([]byte, error)
|
||||||
|
FetchUserdata() ([]byte, error)
|
||||||
Type() string
|
Type() string
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package datasource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type localFile struct {
|
type localFile struct {
|
||||||
@@ -12,10 +13,27 @@ func NewLocalFile(path string) *localFile {
|
|||||||
return &localFile{path}
|
return &localFile{path}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *localFile) Fetch() ([]byte, error) {
|
func (f *localFile) IsAvailable() bool {
|
||||||
return ioutil.ReadFile(self.path)
|
_, err := os.Stat(f.path)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *localFile) Type() string {
|
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"
|
return "local-file"
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +1,153 @@
|
|||||||
package datasource
|
package datasource
|
||||||
|
|
||||||
import "github.com/coreos/coreos-cloudinit/pkg"
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
type metadataService struct {
|
"github.com/coreos/coreos-cloudinit/pkg"
|
||||||
url string
|
)
|
||||||
|
|
||||||
|
// metadataService retrieves metadata from either an OpenStack[1] (2012-08-10)
|
||||||
|
// or EC2[2] (2009-04-04) compatible endpoint. It will first attempt to
|
||||||
|
// directly retrieve a JSON blob from the OpenStack endpoint. If that fails
|
||||||
|
// with a 404, it then attempts to retrieve metadata bit-by-bit from the EC2
|
||||||
|
// endpoint, and populates that into an equivalent JSON blob. metadataService
|
||||||
|
// also checks for userdata from EC2 and, if that fails with a 404, OpenStack.
|
||||||
|
//
|
||||||
|
// [1] http://docs.openstack.org/grizzly/openstack-compute/admin/content/metadata-service.html
|
||||||
|
// [2] http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html#instancedata-data-categories
|
||||||
|
|
||||||
|
const (
|
||||||
|
BaseUrl = "http://169.254.169.254/"
|
||||||
|
Ec2UserdataUrl = BaseUrl + Ec2ApiVersion + "/user-data"
|
||||||
|
Ec2MetadataUrl = BaseUrl + Ec2ApiVersion + "/meta-data"
|
||||||
|
OpenstackUserdataUrl = BaseUrl + "openstack/" + OpenstackApiVersion + "/user_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type metadataService struct{}
|
||||||
|
|
||||||
|
type getter interface {
|
||||||
|
GetRetry(string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMetadataService(url string) *metadataService {
|
func NewMetadataService() *metadataService {
|
||||||
return &metadataService{url}
|
return &metadataService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *metadataService) Fetch() ([]byte, error) {
|
func (ms *metadataService) IsAvailable() bool {
|
||||||
client := pkg.NewHttpClient()
|
client := pkg.NewHttpClient()
|
||||||
return client.Get(ms.url)
|
_, err := client.Get(BaseUrl)
|
||||||
|
return (err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *metadataService) AvailabilityChanges() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *metadataService) ConfigRoot() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *metadataService) FetchMetadata() ([]byte, error) {
|
||||||
|
return fetchMetadata(pkg.NewHttpClient())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *metadataService) FetchUserdata() ([]byte, error) {
|
||||||
|
client := pkg.NewHttpClient()
|
||||||
|
if data, err := client.GetRetry(Ec2UserdataUrl); err == nil {
|
||||||
|
return data, err
|
||||||
|
} else if _, ok := err.(pkg.ErrTimeout); ok {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, err := client.GetRetry(OpenstackUserdataUrl); err == nil {
|
||||||
|
return data, err
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); ok {
|
||||||
|
return []byte{}, nil
|
||||||
|
} else {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *metadataService) Type() string {
|
func (ms *metadataService) Type() string {
|
||||||
return "metadata-service"
|
return "metadata-service"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchMetadata(client getter) ([]byte, error) {
|
||||||
|
attrs := make(map[string]interface{})
|
||||||
|
if keynames, err := fetchAttributes(client, fmt.Sprintf("%s/public-keys", Ec2MetadataUrl)); 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\n", keyname)
|
||||||
|
}
|
||||||
|
keyIDs[tokens[1]] = tokens[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make(map[string]string)
|
||||||
|
for name, id := range keyIDs {
|
||||||
|
sshkey, err := fetchAttribute(client, fmt.Sprintf("%s/public-keys/%s/openssh-key", Ec2MetadataUrl, 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(client, fmt.Sprintf("%s/hostname", Ec2MetadataUrl)); err == nil {
|
||||||
|
attrs["hostname"] = hostname
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localAddr, err := fetchAttribute(client, fmt.Sprintf("%s/local-ipv4", Ec2MetadataUrl)); err == nil {
|
||||||
|
attrs["local-ipv4"] = localAddr
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if publicAddr, err := fetchAttribute(client, fmt.Sprintf("%s/public-ipv4", Ec2MetadataUrl)); err == nil {
|
||||||
|
attrs["public-ipv4"] = publicAddr
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if content_path, err := fetchAttribute(client, fmt.Sprintf("%s/network_config/content_path", Ec2MetadataUrl)); 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 fetchAttributes(client 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 getter, url string) (string, error) {
|
||||||
|
if attrs, err := fetchAttributes(client, url); err == nil && len(attrs) > 0 {
|
||||||
|
return attrs[0], nil
|
||||||
|
} else {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
159
datasource/metadata_service_test.go
Normal file
159
datasource/metadata_service_test.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package datasource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestHttpClient struct {
|
||||||
|
metadata 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.metadata[url]; ok {
|
||||||
|
return []byte(val), nil
|
||||||
|
} else {
|
||||||
|
return nil, pkg.ErrNotFound{fmt.Errorf("not found: %q", url)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMSFetchAttributes(t *testing.T) {
|
||||||
|
for _, s := range []struct {
|
||||||
|
metadata map[string]string
|
||||||
|
err error
|
||||||
|
tests []struct {
|
||||||
|
path string
|
||||||
|
val []string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
metadata: 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.metadata, 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.metadata, s.err, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(attrs, tt.val) {
|
||||||
|
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.metadata, tt.val, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMSFetchAttribute(t *testing.T) {
|
||||||
|
for _, s := range []struct {
|
||||||
|
metadata map[string]string
|
||||||
|
err error
|
||||||
|
tests []struct {
|
||||||
|
path string
|
||||||
|
val string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
metadata: 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.metadata, 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.metadata, s.err, err)
|
||||||
|
}
|
||||||
|
if attr != tt.val {
|
||||||
|
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.metadata, tt.val, attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMSFetchMetadata(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
metadata map[string]string
|
||||||
|
err error
|
||||||
|
expect []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
metadata: map[string]string{
|
||||||
|
"http://169.254.169.254/2009-04-04/meta-data/hostname": "host",
|
||||||
|
"http://169.254.169.254/2009-04-04/meta-data/public-keys": "0=test1\n",
|
||||||
|
"http://169.254.169.254/2009-04-04/meta-data/public-keys/0": "openssh-key",
|
||||||
|
"http://169.254.169.254/2009-04-04/meta-data/public-keys/0/openssh-key": "key",
|
||||||
|
"http://169.254.169.254/2009-04-04/meta-data/network_config/content_path": "path",
|
||||||
|
},
|
||||||
|
expect: []byte(`{"hostname":"host","network_config":{"content_path":"path"},"public_keys":{"test1":"key"}}`),
|
||||||
|
},
|
||||||
|
{err: pkg.ErrTimeout{fmt.Errorf("test error")}},
|
||||||
|
} {
|
||||||
|
client := &TestHttpClient{tt.metadata, tt.err}
|
||||||
|
metadata, err := fetchMetadata(client)
|
||||||
|
if err != tt.err {
|
||||||
|
t.Fatalf("bad error (%q): want %q, got %q", tt.metadata, tt.err, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(metadata, tt.expect) {
|
||||||
|
t.Fatalf("bad fetch (%q): want %q, got %q", tt.metadata, tt.expect, metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -14,7 +14,7 @@ const (
|
|||||||
ProcCmdlineCloudConfigFlag = "cloud-config-url"
|
ProcCmdlineCloudConfigFlag = "cloud-config-url"
|
||||||
)
|
)
|
||||||
|
|
||||||
type procCmdline struct{
|
type procCmdline struct {
|
||||||
Location string
|
Location string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,8 +22,31 @@ func NewProcCmdline() *procCmdline {
|
|||||||
return &procCmdline{Location: ProcCmdlineLocation}
|
return &procCmdline{Location: ProcCmdlineLocation}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *procCmdline) Fetch() ([]byte, error) {
|
func (c *procCmdline) IsAvailable() bool {
|
||||||
contents, err := ioutil.ReadFile(self.Location)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -35,7 +58,7 @@ func (self *procCmdline) Fetch() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client := pkg.NewHttpClient()
|
client := pkg.NewHttpClient()
|
||||||
cfg, err := client.Get(url)
|
cfg, err := client.GetRetry(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -43,7 +66,7 @@ func (self *procCmdline) Fetch() ([]byte, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *procCmdline) Type() string {
|
func (c *procCmdline) Type() string {
|
||||||
return "proc-cmdline"
|
return "proc-cmdline"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -77,7 +77,7 @@ func TestProcCmdlineAndFetchConfig(t *testing.T) {
|
|||||||
|
|
||||||
p := NewProcCmdline()
|
p := NewProcCmdline()
|
||||||
p.Location = file.Name()
|
p.Location = file.Name()
|
||||||
cfg, err := p.Fetch()
|
cfg, err := p.FetchUserdata()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Test produced error: %v", err)
|
t.Errorf("Test produced error: %v", err)
|
||||||
}
|
}
|
||||||
|
38
datasource/url.go
Normal file
38
datasource/url.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package datasource
|
||||||
|
|
||||||
|
import "github.com/coreos/coreos-cloudinit/pkg"
|
||||||
|
|
||||||
|
type remoteFile struct {
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoteFile(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"
|
||||||
|
}
|
@@ -3,11 +3,13 @@ package initialize
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
|
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/network"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,11 +22,9 @@ type CloudConfigFile interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CloudConfigUnit represents a CoreOS specific configuration option that can generate
|
// CloudConfigUnit represents a CoreOS specific configuration option that can generate
|
||||||
// an associated system.Unit to be created/enabled appropriately
|
// associated system.Units to be created/enabled appropriately
|
||||||
type CloudConfigUnit interface {
|
type CloudConfigUnit interface {
|
||||||
// Unit should either return (*system.Unit, error), or (nil, nil) if nothing
|
Units(root string) ([]system.Unit, error)
|
||||||
// needs to be done for this configuration option.
|
|
||||||
Unit(root string) (*system.Unit, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
|
// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
|
||||||
@@ -37,10 +37,11 @@ type CloudConfig struct {
|
|||||||
Update UpdateConfig
|
Update UpdateConfig
|
||||||
Units []system.Unit
|
Units []system.Unit
|
||||||
}
|
}
|
||||||
WriteFiles []system.File `yaml:"write_files"`
|
WriteFiles []system.File `yaml:"write_files"`
|
||||||
Hostname string
|
Hostname string
|
||||||
Users []system.User
|
Users []system.User
|
||||||
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
|
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
|
||||||
|
NetworkConfigPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
type warner func(format string, v ...interface{})
|
type warner func(format string, v ...interface{})
|
||||||
@@ -65,51 +66,62 @@ func warnOnUnrecognizedKeys(contents string, warn warner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for unrecognized coreos options, if any are set
|
// Check for unrecognized coreos options, if any are set
|
||||||
coreos, ok := c["coreos"]
|
if coreos, ok := c["coreos"]; ok {
|
||||||
if ok {
|
if set, ok := coreos.(map[interface{}]interface{}); ok {
|
||||||
set := coreos.(map[interface{}]interface{})
|
known := cc["coreos"].(map[interface{}]interface{})
|
||||||
known := cc["coreos"].(map[interface{}]interface{})
|
for k, _ := range set {
|
||||||
for k, _ := range set {
|
if key, ok := k.(string); ok {
|
||||||
key := k.(string)
|
if _, ok := known[key]; !ok {
|
||||||
if _, ok := known[key]; !ok {
|
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", key)
|
||||||
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
|
// Check for any badly-specified users, if any are set
|
||||||
users, ok := c["users"]
|
if users, ok := c["users"]; ok {
|
||||||
if ok {
|
|
||||||
var known map[string]interface{}
|
var known map[string]interface{}
|
||||||
b, _ := goyaml.Marshal(&system.User{})
|
b, _ := goyaml.Marshal(&system.User{})
|
||||||
goyaml.Unmarshal(b, &known)
|
goyaml.Unmarshal(b, &known)
|
||||||
|
|
||||||
set := users.([]interface{})
|
if set, ok := users.([]interface{}); ok {
|
||||||
for _, u := range set {
|
for _, u := range set {
|
||||||
user := u.(map[interface{}]interface{})
|
if user, ok := u.(map[interface{}]interface{}); ok {
|
||||||
for k, _ := range user {
|
for k, _ := range user {
|
||||||
key := k.(string)
|
if key, ok := k.(string); ok {
|
||||||
if _, ok := known[key]; !ok {
|
if _, ok := known[key]; !ok {
|
||||||
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", key)
|
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
|
// Check for any badly-specified files, if any are set
|
||||||
files, ok := c["write_files"]
|
if files, ok := c["write_files"]; ok {
|
||||||
if ok {
|
|
||||||
var known map[string]interface{}
|
var known map[string]interface{}
|
||||||
b, _ := goyaml.Marshal(&system.File{})
|
b, _ := goyaml.Marshal(&system.File{})
|
||||||
goyaml.Unmarshal(b, &known)
|
goyaml.Unmarshal(b, &known)
|
||||||
|
|
||||||
set := files.([]interface{})
|
if set, ok := files.([]interface{}); ok {
|
||||||
for _, f := range set {
|
for _, f := range set {
|
||||||
file := f.(map[interface{}]interface{})
|
if file, ok := f.(map[interface{}]interface{}); ok {
|
||||||
for k, _ := range file {
|
for k, _ := range file {
|
||||||
key := k.(string)
|
if key, ok := k.(string); ok {
|
||||||
if _, ok := known[key]; !ok {
|
if _, ok := known[key]; !ok {
|
||||||
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", key)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,30 +227,79 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
|
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
|
||||||
u, err := ccu.Unit(env.Root())
|
u, err := ccu.Units(env.Root())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if u != nil {
|
cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
|
||||||
cfg.Coreos.Units = append(cfg.Coreos.Units, *u)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wroteEnvironment := false
|
||||||
for _, file := range cfg.WriteFiles {
|
for _, file := range cfg.WriteFiles {
|
||||||
file.Path = path.Join(env.Root(), file.Path)
|
fullPath, err := system.WriteFile(&file, env.Root())
|
||||||
if err := system.WriteFile(&file); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Wrote file %s to filesystem", file.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() != "" {
|
||||||
|
netconfBytes, err := ioutil.ReadFile(path.Join(env.ConfigRoot(), cfg.NetworkConfigPath))
|
||||||
|
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)
|
commands := make(map[string]string, 0)
|
||||||
reload := false
|
reload := false
|
||||||
for _, unit := range cfg.Coreos.Units {
|
for _, unit := range units {
|
||||||
dst := system.UnitDestination(&unit, env.Root())
|
dst := unit.Destination(root)
|
||||||
if unit.Content != "" {
|
if unit.Content != "" {
|
||||||
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
|
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
|
||||||
if err := system.PlaceUnit(&unit, dst); err != nil {
|
if err := um.PlaceUnit(&unit, dst); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Placed unit %s at %s", unit.Name, dst)
|
log.Printf("Placed unit %s at %s", unit.Name, dst)
|
||||||
@@ -247,7 +308,12 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
|
|
||||||
if unit.Mask {
|
if unit.Mask {
|
||||||
log.Printf("Masking unit file %s", unit.Name)
|
log.Printf("Masking unit file %s", unit.Name)
|
||||||
if err := system.MaskUnit(unit.Name, 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 := um.UnmaskUnit(&unit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +321,7 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
if unit.Enable {
|
if unit.Enable {
|
||||||
if unit.Group() != "network" {
|
if unit.Group() != "network" {
|
||||||
log.Printf("Enabling unit file %s", unit.Name)
|
log.Printf("Enabling unit file %s", unit.Name)
|
||||||
if err := system.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
|
if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Enabled unit %s", unit.Name)
|
log.Printf("Enabled unit %s", unit.Name)
|
||||||
@@ -272,14 +338,14 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if reload {
|
if reload {
|
||||||
if err := system.DaemonReload(); err != nil {
|
if err := um.DaemonReload(); err != nil {
|
||||||
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
|
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for unit, command := range commands {
|
for unit, command := range commands {
|
||||||
log.Printf("Calling unit command '%s %s'", command, unit)
|
log.Printf("Calling unit command '%s %s'", command, unit)
|
||||||
res, err := system.RunUnitCommand(command, unit)
|
res, err := um.RunUnitCommand(command, unit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,38 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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) {
|
func TestCloudConfigUnknownKeys(t *testing.T) {
|
||||||
contents := `
|
contents := `
|
||||||
coreos:
|
coreos:
|
||||||
@@ -332,3 +362,109 @@ users:
|
|||||||
t.Errorf("Failed to parse no-log-init field")
|
t.Errorf("Failed to parse no-log-init field")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TestUnitManager struct {
|
||||||
|
placed []string
|
||||||
|
enabled []string
|
||||||
|
masked []string
|
||||||
|
unmasked []string
|
||||||
|
commands map[string]string
|
||||||
|
reload bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tum *TestUnitManager) PlaceUnit(unit *system.Unit, dst string) error {
|
||||||
|
tum.placed = append(tum.placed, unit.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (tum *TestUnitManager) EnableUnitFile(unit string, runtime bool) error {
|
||||||
|
tum.enabled = append(tum.enabled, unit)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (tum *TestUnitManager) RunUnitCommand(command, unit string) (string, error) {
|
||||||
|
tum.commands = make(map[string]string)
|
||||||
|
tum.commands[unit] = command
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
func (tum *TestUnitManager) DaemonReload() error {
|
||||||
|
tum.reload = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (tum *TestUnitManager) MaskUnit(unit *system.Unit) error {
|
||||||
|
tum.masked = append(tum.masked, unit.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error {
|
||||||
|
tum.unmasked = append(tum.unmasked, unit.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessUnits(t *testing.T) {
|
||||||
|
tum := &TestUnitManager{}
|
||||||
|
units := []system.Unit{
|
||||||
|
system.Unit{
|
||||||
|
Name: "foo",
|
||||||
|
Mask: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
|
}
|
||||||
|
if len(tum.masked) != 1 || tum.masked[0] != "foo" {
|
||||||
|
t.Errorf("expected foo to be masked, but found %v", tum.masked)
|
||||||
|
}
|
||||||
|
|
||||||
|
tum = &TestUnitManager{}
|
||||||
|
units = []system.Unit{
|
||||||
|
system.Unit{
|
||||||
|
Name: "bar.network",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := tum.commands["systemd-networkd.service"]; !ok {
|
||||||
|
t.Errorf("expected systemd-networkd.service to be reloaded!")
|
||||||
|
}
|
||||||
|
|
||||||
|
tum = &TestUnitManager{}
|
||||||
|
units = []system.Unit{
|
||||||
|
system.Unit{
|
||||||
|
Name: "baz.service",
|
||||||
|
Content: "[Service]\nExecStart=/bin/true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
|
}
|
||||||
|
if len(tum.placed) != 1 || tum.placed[0] != "baz.service" {
|
||||||
|
t.Fatalf("expected baz.service to be written, but got %v", tum.placed)
|
||||||
|
}
|
||||||
|
|
||||||
|
tum = &TestUnitManager{}
|
||||||
|
units = []system.Unit{
|
||||||
|
system.Unit{
|
||||||
|
Name: "locksmithd.service",
|
||||||
|
Runtime: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
|
}
|
||||||
|
if len(tum.unmasked) != 1 || tum.unmasked[0] != "locksmithd.service" {
|
||||||
|
t.Fatalf("expected locksmithd.service to be unmasked, but got %v", tum.unmasked)
|
||||||
|
}
|
||||||
|
|
||||||
|
tum = &TestUnitManager{}
|
||||||
|
units = []system.Unit{
|
||||||
|
system.Unit{
|
||||||
|
Name: "woof",
|
||||||
|
Enable: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
|
}
|
||||||
|
if len(tum.enabled) != 1 || tum.enabled[0] != "woof" {
|
||||||
|
t.Fatalf("expected woof to be enabled, but got %v", tum.enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -4,48 +4,89 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultSSHKeyName = "coreos-cloudinit"
|
const DefaultSSHKeyName = "coreos-cloudinit"
|
||||||
|
|
||||||
type Environment struct {
|
type Environment struct {
|
||||||
root string
|
root string
|
||||||
|
configRoot string
|
||||||
workspace string
|
workspace string
|
||||||
|
netconfType string
|
||||||
sshKeyName string
|
sshKeyName string
|
||||||
substitutions map[string]string
|
substitutions map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEnvironment(root, workspace string) *Environment {
|
// TODO(jonboulle): this is getting unwieldy, should be able to simplify the interface somehow
|
||||||
substitutions := map[string]string{
|
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"),
|
"$public_ipv4": os.Getenv("COREOS_PUBLIC_IPV4"),
|
||||||
"$private_ipv4": os.Getenv("COREOS_PRIVATE_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 {
|
func (e *Environment) Workspace() string {
|
||||||
return path.Join(self.root, self.workspace)
|
return path.Join(e.root, e.workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Environment) Root() string {
|
func (e *Environment) Root() string {
|
||||||
return self.root
|
return e.root
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Environment) SSHKeyName() string {
|
func (e *Environment) ConfigRoot() string {
|
||||||
return self.sshKeyName
|
return e.configRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Environment) SetSSHKeyName(name string) {
|
func (e *Environment) NetconfType() string {
|
||||||
self.sshKeyName = name
|
return e.netconfType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Environment) Apply(data string) string {
|
func (e *Environment) SSHKeyName() string {
|
||||||
for key, val := range self.substitutions {
|
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)
|
data = strings.Replace(data, key, val, -1)
|
||||||
}
|
}
|
||||||
return data
|
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)
|
// 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.
|
// by replacing any dashes with underscores and ensuring they are entirely upper case.
|
||||||
// For example, "some-env" --> "SOME_ENV"
|
// For example, "some-env" --> "SOME_ENV"
|
||||||
|
@@ -1,27 +1,106 @@
|
|||||||
package initialize
|
package initialize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEnvironmentApply(t *testing.T) {
|
func TestEnvironmentApply(t *testing.T) {
|
||||||
os.Setenv("COREOS_PUBLIC_IPV4", "192.0.2.3")
|
os.Setenv("COREOS_PUBLIC_IPV4", "1.2.3.4")
|
||||||
os.Setenv("COREOS_PRIVATE_IPV4", "192.0.2.203")
|
os.Setenv("COREOS_PRIVATE_IPV4", "5.6.7.8")
|
||||||
env := NewEnvironment("./", "./")
|
for _, tt := range []struct {
|
||||||
input := `[Service]
|
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"
|
ExecStart=/usr/bin/echo "$public_ipv4"
|
||||||
ExecStop=/usr/bin/echo $private_ipv4
|
ExecStop=/usr/bin/echo $private_ipv4
|
||||||
ExecStop=/usr/bin/echo $unknown
|
ExecStop=/usr/bin/echo $unknown`,
|
||||||
`
|
`[Service]
|
||||||
expected := `[Service]
|
|
||||||
ExecStart=/usr/bin/echo "192.0.2.3"
|
ExecStart=/usr/bin/echo "192.0.2.3"
|
||||||
ExecStop=/usr/bin/echo 192.0.2.203
|
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)
|
env := NewEnvironment("./", "./", "./", "", "", tt.subs)
|
||||||
if output != expected {
|
got := env.Apply(tt.input)
|
||||||
t.Fatalf("Environment incorrectly applied.\nOutput:\n%s\nExpected:\n%s", output, expected)
|
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_PUBLIC_IPV4=1.2.3.4\nCOREOS_PRIVATE_IPV4=5.6.7.8\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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ package initialize
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
@@ -19,18 +20,25 @@ func (ee EtcdEnvironment) String() (out string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sorted sort.StringSlice
|
||||||
|
for k, _ := range norm {
|
||||||
|
sorted = append(sorted, k)
|
||||||
|
}
|
||||||
|
sorted.Sort()
|
||||||
|
|
||||||
out += "[Service]\n"
|
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)
|
out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unit creates a Unit file drop-in for etcd, using any configured
|
// Units creates a Unit file drop-in for etcd, using any configured
|
||||||
// options and adding a default MachineID if unset.
|
// options and adding a default MachineID if unset.
|
||||||
func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) {
|
func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
|
||||||
if ee == nil {
|
if ee == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -45,10 +53,11 @@ func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &system.Unit{
|
etcd := system.Unit{
|
||||||
Name: "etcd.service",
|
Name: "etcd.service",
|
||||||
Runtime: true,
|
Runtime: true,
|
||||||
DropIn: true,
|
DropIn: true,
|
||||||
Content: ee.String(),
|
Content: ee.String(),
|
||||||
}, nil
|
}
|
||||||
|
return []system.Unit{etcd}, nil
|
||||||
}
|
}
|
||||||
|
@@ -59,7 +59,7 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
||||||
ec := EtcdEnvironment{
|
ee := EtcdEnvironment{
|
||||||
"name": "node001",
|
"name": "node001",
|
||||||
"discovery": "http://disco.example.com/foobar",
|
"discovery": "http://disco.example.com/foobar",
|
||||||
"peer-bind-addr": "127.0.0.1:7002",
|
"peer-bind-addr": "127.0.0.1:7002",
|
||||||
@@ -70,17 +70,20 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
u, err := ec.Unit(dir)
|
sd := system.NewUnitManager(dir)
|
||||||
|
|
||||||
|
uu, err := ee.Units(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Generating etcd unit failed: %v", err)
|
t.Fatalf("Generating etcd unit failed: %v", err)
|
||||||
}
|
}
|
||||||
if u == nil {
|
if len(uu) != 1 {
|
||||||
t.Fatalf("Returned nil etcd unit unexpectedly")
|
t.Fatalf("Expected 1 unit to be returned, got %d", len(uu))
|
||||||
}
|
}
|
||||||
|
u := uu[0]
|
||||||
|
|
||||||
dst := system.UnitDestination(u, dir)
|
dst := u.Destination(dir)
|
||||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
os.Stderr.WriteString("writing to " + dir + "\n")
|
||||||
if err := system.PlaceUnit(u, dst); err != nil {
|
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,8 +104,8 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect := `[Service]
|
expect := `[Service]
|
||||||
Environment="ETCD_NAME=node001"
|
|
||||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||||
|
Environment="ETCD_NAME=node001"
|
||||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||||
`
|
`
|
||||||
if string(contents) != expect {
|
if string(contents) != expect {
|
||||||
@@ -118,23 +121,26 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
sd := system.NewUnitManager(dir)
|
||||||
|
|
||||||
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
|
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
|
||||||
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
|
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed writing out /etc/machine-id: %v", err)
|
t.Fatalf("Failed writing out /etc/machine-id: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := ee.Unit(dir)
|
uu, err := ee.Units(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Generating etcd unit failed: %v", err)
|
t.Fatalf("Generating etcd unit failed: %v", err)
|
||||||
}
|
}
|
||||||
if u == nil {
|
if len(uu) == 0 {
|
||||||
t.Fatalf("Returned nil etcd unit unexpectedly")
|
t.Fatalf("Returned empty etcd units unexpectedly")
|
||||||
}
|
}
|
||||||
|
u := uu[0]
|
||||||
|
|
||||||
dst := system.UnitDestination(u, dir)
|
dst := u.Destination(dir)
|
||||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
os.Stderr.WriteString("writing to " + dir + "\n")
|
||||||
if err := system.PlaceUnit(u, dst); err != nil {
|
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,8 +165,8 @@ func TestEtcdEnvironmentWhenNil(t *testing.T) {
|
|||||||
if ee != nil {
|
if ee != nil {
|
||||||
t.Fatalf("EtcdEnvironment is not nil")
|
t.Fatalf("EtcdEnvironment is not nil")
|
||||||
}
|
}
|
||||||
u, err := ee.Unit("")
|
uu, err := ee.Units("")
|
||||||
if u != nil || err != nil {
|
if len(uu) != 0 || err != nil {
|
||||||
t.Fatalf("Unit returned a non-nil value for nil input")
|
t.Fatalf("Units returned value for nil input")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,16 +19,17 @@ func (fe FleetEnvironment) String() (out string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unit generates a Unit file drop-in for fleet, if any fleet options were
|
// Units generates a Unit file drop-in for fleet, if any fleet options were
|
||||||
// configured in cloud-config
|
// configured in cloud-config
|
||||||
func (fe FleetEnvironment) Unit(root string) (*system.Unit, error) {
|
func (fe FleetEnvironment) Units(root string) ([]system.Unit, error) {
|
||||||
if len(fe) < 1 {
|
if len(fe) < 1 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return &system.Unit{
|
fleet := system.Unit{
|
||||||
Name: "fleet.service",
|
Name: "fleet.service",
|
||||||
Runtime: true,
|
Runtime: true,
|
||||||
DropIn: true,
|
DropIn: true,
|
||||||
Content: fe.String(),
|
Content: fe.String(),
|
||||||
}, nil
|
}
|
||||||
|
return []system.Unit{fleet}, nil
|
||||||
}
|
}
|
||||||
|
@@ -19,20 +19,21 @@ Environment="FLEET_PUBLIC_IP=12.34.56.78"
|
|||||||
|
|
||||||
func TestFleetUnit(t *testing.T) {
|
func TestFleetUnit(t *testing.T) {
|
||||||
cfg := make(FleetEnvironment, 0)
|
cfg := make(FleetEnvironment, 0)
|
||||||
u, err := cfg.Unit("/")
|
uu, err := cfg.Units("/")
|
||||||
if u != nil {
|
if len(uu) != 0 {
|
||||||
t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
|
t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg["public-ip"] = "12.34.56.78"
|
cfg["public-ip"] = "12.34.56.78"
|
||||||
|
|
||||||
u, err = cfg.Unit("/")
|
uu, err = cfg.Units("/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error generating fleet unit: %v", err)
|
t.Errorf("error generating fleet unit: %v", err)
|
||||||
}
|
}
|
||||||
if u == nil {
|
if len(uu) != 1 {
|
||||||
t.Fatalf("unexpectedly got nil unit generating fleet unit!")
|
t.Fatalf("expected 1 unit generated, got %d", len(uu))
|
||||||
}
|
}
|
||||||
|
u := uu[0]
|
||||||
if !u.Runtime {
|
if !u.Runtime {
|
||||||
t.Errorf("bad Runtime for generated fleet unit!")
|
t.Errorf("bad Runtime for generated fleet unit!")
|
||||||
}
|
}
|
||||||
|
@@ -50,9 +50,7 @@ func TestEtcHostsWrittenToDisk(t *testing.T) {
|
|||||||
t.Fatalf("manageEtcHosts returned nil file unexpectedly")
|
t.Fatalf("manageEtcHosts returned nil file unexpectedly")
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Path = path.Join(dir, f.Path)
|
if _, err := system.WriteFile(f, dir); err != nil {
|
||||||
|
|
||||||
if err := system.WriteFile(f); err != nil {
|
|
||||||
t.Fatalf("Error writing EtcHosts: %v", err)
|
t.Fatalf("Error writing EtcHosts: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
52
initialize/meta_data.go
Normal file
52
initialize/meta_data.go
Normal 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
|
||||||
|
}
|
69
initialize/meta_data_test.go
Normal file
69
initialize/meta_data_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -31,8 +31,7 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
|
|||||||
t.Fatalf("OEMRelease returned nil file unexpectedly")
|
t.Fatalf("OEMRelease returned nil file unexpectedly")
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Path = path.Join(dir, f.Path)
|
if _, err := system.WriteFile(f, dir); err != nil {
|
||||||
if err := system.WriteFile(f); err != nil {
|
|
||||||
t.Fatalf("Writing of OEMRelease failed: %v", err)
|
t.Fatalf("Writing of OEMRelease failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,7 +25,7 @@ func SSHImportKeysFromURL(system_user string, url string) error {
|
|||||||
|
|
||||||
func fetchUserKeys(url string) ([]string, error) {
|
func fetchUserKeys(url string) ([]string, error) {
|
||||||
client := pkg.NewHttpClient()
|
client := pkg.NewHttpClient()
|
||||||
data, err := client.Get(url)
|
data, err := client.GetRetry(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
locksmithUnit = "locksmithd.service"
|
locksmithUnit = "locksmithd.service"
|
||||||
|
updateEngineUnit = "update-engine.service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// updateOption represents a configurable update option, which, if set, will be
|
// updateOption represents a configurable update option, which, if set, will be
|
||||||
@@ -36,7 +37,6 @@ var updateOptions = []*updateOption{
|
|||||||
&updateOption{
|
&updateOption{
|
||||||
key: "group",
|
key: "group",
|
||||||
prefix: "GROUP=",
|
prefix: "GROUP=",
|
||||||
valid: []string{"master", "beta", "alpha", "stable"},
|
|
||||||
},
|
},
|
||||||
&updateOption{
|
&updateOption{
|
||||||
key: "server",
|
key: "server",
|
||||||
@@ -126,24 +126,40 @@ func (uc UpdateConfig) File(root string) (*system.File, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUnit generates a locksmith system.Unit, if reboot-strategy was set in
|
// Units generates units for the cloud-init initializer to act on:
|
||||||
// cloud-config, for the cloud-init initializer to act on appropriately
|
// - a locksmith system.Unit, if "reboot-strategy" was set in cloud-config
|
||||||
func (uc UpdateConfig) Unit(root string) (*system.Unit, error) {
|
// - an update_engine system.Unit, if "group" was set in cloud-config
|
||||||
strategy, ok := uc["reboot-strategy"]
|
func (uc UpdateConfig) Units(root string) ([]system.Unit, error) {
|
||||||
if !ok {
|
var units []system.Unit
|
||||||
return nil, nil
|
if strategy, ok := uc["reboot-strategy"]; ok {
|
||||||
|
ls := &system.Unit{
|
||||||
|
Name: locksmithUnit,
|
||||||
|
Command: "restart",
|
||||||
|
Mask: false,
|
||||||
|
Runtime: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if strategy == "off" {
|
||||||
|
ls.Command = "stop"
|
||||||
|
ls.Mask = true
|
||||||
|
}
|
||||||
|
units = append(units, *ls)
|
||||||
}
|
}
|
||||||
|
|
||||||
u := &system.Unit{
|
rue := false
|
||||||
Name: locksmithUnit,
|
if _, ok := uc["group"]; ok {
|
||||||
Command: "restart",
|
rue = true
|
||||||
Mask: false,
|
}
|
||||||
|
if _, ok := uc["server"]; ok {
|
||||||
|
rue = true
|
||||||
|
}
|
||||||
|
if rue {
|
||||||
|
ue := system.Unit{
|
||||||
|
Name: updateEngineUnit,
|
||||||
|
Command: "restart",
|
||||||
|
}
|
||||||
|
units = append(units, ue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strategy == "off" {
|
return units, nil
|
||||||
u.Command = "stop"
|
|
||||||
u.Mask = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return u, nil
|
|
||||||
}
|
}
|
||||||
|
@@ -38,12 +38,12 @@ func TestEmptyUpdateConfig(t *testing.T) {
|
|||||||
if f != nil {
|
if f != nil {
|
||||||
t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f)
|
t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f)
|
||||||
}
|
}
|
||||||
u, err := uc.Unit("")
|
uu, err := uc.Units("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("unexpected error getting unit from empty UpdateConfig")
|
t.Error("unexpected error getting unit from empty UpdateConfig")
|
||||||
}
|
}
|
||||||
if u != nil {
|
if len(uu) != 0 {
|
||||||
t.Errorf("getting unit from empty UpdateConfig should have returned nil, got %v", u)
|
t.Errorf("getting unit from empty UpdateConfig should have returned zero units, got %d", len(uu))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +106,21 @@ SERVER=http://foo.com`
|
|||||||
t.Errorf("File has incorrect contents, got %v, want %v", got, want)
|
t.Errorf("File has incorrect contents, got %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uu, err := u.Units(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error getting units from UpdateConfig: %v", err)
|
||||||
|
} else if len(uu) != 1 {
|
||||||
|
t.Errorf("unexpected number of files returned from UpdateConfig: want 1, got %d", len(uu))
|
||||||
|
} else {
|
||||||
|
unit := uu[0]
|
||||||
|
if unit.Name != "update-engine.service" {
|
||||||
|
t.Errorf("bad name for generated unit: want update-engine.service, got %s", unit.Name)
|
||||||
|
}
|
||||||
|
if unit.Command != "restart" {
|
||||||
|
t.Errorf("bad command for generated unit: want restart, got %s", unit.Command)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRebootStrategies(t *testing.T) {
|
func TestRebootStrategies(t *testing.T) {
|
||||||
@@ -145,12 +160,13 @@ func TestRebootStrategies(t *testing.T) {
|
|||||||
t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line)
|
t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
u, err := uc.Unit(dir)
|
uu, err := uc.Units(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
|
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
|
||||||
} else if u == nil {
|
} else if len(uu) != 1 {
|
||||||
t.Errorf("generated empty unit for reboot-strategy=%v", s.name)
|
t.Errorf("unexpected number of units for reboot-strategy=%v: %d", s.name, len(uu))
|
||||||
} else {
|
} else {
|
||||||
|
u := uu[0]
|
||||||
if u.Name != locksmithUnit {
|
if u.Name != locksmithUnit {
|
||||||
t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name)
|
t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name)
|
||||||
}
|
}
|
||||||
@@ -189,8 +205,7 @@ func TestUpdateConfWrittenToDisk(t *testing.T) {
|
|||||||
t.Fatal("Unexpectedly got nil updateconfig file")
|
t.Fatal("Unexpectedly got nil updateconfig file")
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Path = path.Join(dir, f.Path)
|
if _, err := system.WriteFile(f, dir); err != nil {
|
||||||
if err := system.WriteFile(f); err != nil {
|
|
||||||
t.Fatalf("Error writing update config: %v", err)
|
t.Fatalf("Error writing update config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,6 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ParseUserData(contents string) (interface{}, error) {
|
func ParseUserData(contents string) (interface{}, error) {
|
||||||
|
if len(contents) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
header := strings.SplitN(contents, "\n", 2)[0]
|
header := strings.SplitN(contents, "\n", 2)[0]
|
||||||
|
|
||||||
// Explicitly trim the header so we can handle user-data from
|
// 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, "#!") {
|
if strings.HasPrefix(header, "#!") {
|
||||||
log.Printf("Parsing user-data as script")
|
log.Printf("Parsing user-data as script")
|
||||||
return system.Script(contents), nil
|
return system.Script(contents), nil
|
||||||
|
|
||||||
} else if header == "#cloud-config" {
|
} else if header == "#cloud-config" {
|
||||||
log.Printf("Parsing user-data as cloud-config")
|
log.Printf("Parsing user-data as cloud-config")
|
||||||
cfg, err := NewCloudConfig(contents)
|
return NewCloudConfig(contents)
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
return *cfg, nil
|
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
|
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
|
||||||
}
|
}
|
||||||
|
@@ -37,7 +37,7 @@ func TestParseConfigCRLF(t *testing.T) {
|
|||||||
t.Fatalf("Failed parsing config: %v", err)
|
t.Fatalf("Failed parsing config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := ud.(CloudConfig)
|
cfg := ud.(*CloudConfig)
|
||||||
|
|
||||||
if cfg.Hostname != "foo" {
|
if cfg.Hostname != "foo" {
|
||||||
t.Error("Failed parsing hostname from config")
|
t.Error("Failed parsing hostname from config")
|
||||||
@@ -47,3 +47,12 @@ func TestParseConfigCRLF(t *testing.T) {
|
|||||||
t.Error("Parsed incorrect number of SSH keys")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -3,6 +3,7 @@ package initialize
|
|||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
@@ -28,21 +29,23 @@ func PersistScriptInWorkspace(script system.Script, workspace string) (string, e
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
|
relpath := strings.TrimPrefix(tmp.Name(), workspace)
|
||||||
|
|
||||||
file := system.File{
|
file := system.File{
|
||||||
Path: tmp.Name(),
|
Path: relpath,
|
||||||
RawFilePermissions: "0744",
|
RawFilePermissions: "0744",
|
||||||
Content: string(script),
|
Content: string(script),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = system.WriteFile(&file)
|
return system.WriteFile(&file, workspace)
|
||||||
return file.Path, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PersistUnitNameInWorkspace(name string, workspace string) error {
|
func PersistUnitNameInWorkspace(name string, workspace string) error {
|
||||||
file := system.File{
|
file := system.File{
|
||||||
Path: path.Join(workspace, "scripts", "unit-name"),
|
Path: path.Join("scripts", "unit-name"),
|
||||||
RawFilePermissions: "0644",
|
RawFilePermissions: "0644",
|
||||||
Content: name,
|
Content: name,
|
||||||
}
|
}
|
||||||
return system.WriteFile(&file)
|
_, err := system.WriteFile(&file, workspace)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -3,19 +3,30 @@ package network
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InterfaceGenerator interface {
|
type InterfaceGenerator interface {
|
||||||
Name() string
|
Name() string
|
||||||
|
Filename() string
|
||||||
Netdev() string
|
Netdev() string
|
||||||
Link() string
|
Link() string
|
||||||
Network() string
|
Network() string
|
||||||
|
Type() string
|
||||||
|
ModprobeParams() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type networkInterface interface {
|
||||||
|
InterfaceGenerator
|
||||||
|
Children() []networkInterface
|
||||||
|
setConfigDepth(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
type logicalInterface struct {
|
type logicalInterface struct {
|
||||||
name string
|
name string
|
||||||
config configMethod
|
config configMethod
|
||||||
children []InterfaceGenerator
|
children []networkInterface
|
||||||
|
configDepth int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *logicalInterface) Network() string {
|
func (i *logicalInterface) Network() string {
|
||||||
@@ -48,6 +59,26 @@ func (i *logicalInterface) Network() string {
|
|||||||
return config
|
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 {
|
type physicalInterface struct {
|
||||||
logicalInterface
|
logicalInterface
|
||||||
}
|
}
|
||||||
@@ -60,13 +91,14 @@ func (p *physicalInterface) Netdev() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *physicalInterface) Link() string {
|
func (p *physicalInterface) Type() string {
|
||||||
return ""
|
return "physical"
|
||||||
}
|
}
|
||||||
|
|
||||||
type bondInterface struct {
|
type bondInterface struct {
|
||||||
logicalInterface
|
logicalInterface
|
||||||
slaves []string
|
slaves []string
|
||||||
|
options map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bondInterface) Name() string {
|
func (b *bondInterface) Name() string {
|
||||||
@@ -77,8 +109,17 @@ func (b *bondInterface) Netdev() string {
|
|||||||
return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name)
|
return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bondInterface) Link() string {
|
func (b *bondInterface) Type() string {
|
||||||
return ""
|
return "bond"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bondInterface) ModprobeParams() string {
|
||||||
|
params := ""
|
||||||
|
for name, val := range b.options {
|
||||||
|
params += fmt.Sprintf("%s=%s ", name, val)
|
||||||
|
}
|
||||||
|
params = strings.TrimSuffix(params, " ")
|
||||||
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
type vlanInterface struct {
|
type vlanInterface struct {
|
||||||
@@ -92,102 +133,161 @@ func (v *vlanInterface) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *vlanInterface) Netdev() 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 {
|
func (v *vlanInterface) Type() string {
|
||||||
return ""
|
return "vlan"
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
|
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
|
||||||
bondStanzas := make(map[string]*stanzaInterface)
|
interfaceMap := createInterfaces(stanzas)
|
||||||
physicalStanzas := make(map[string]*stanzaInterface)
|
linkAncestors(interfaceMap)
|
||||||
vlanStanzas := make(map[string]*stanzaInterface)
|
markConfigDepths(interfaceMap)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
physicals := make(map[string]*physicalInterface)
|
interfaces := make([]InterfaceGenerator, 0, len(interfaceMap))
|
||||||
for _, p := range physicalStanzas {
|
for _, iface := range interfaceMap {
|
||||||
if _, ok := p.configMethod.(configMethodLoopback); ok {
|
interfaces = append(interfaces, iface)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return interfaces
|
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)
|
||||||
|
}
|
||||||
|
@@ -30,12 +30,13 @@ func TestPhysicalInterfaceLink(t *testing.T) {
|
|||||||
func TestPhysicalInterfaceNetwork(t *testing.T) {
|
func TestPhysicalInterfaceNetwork(t *testing.T) {
|
||||||
p := physicalInterface{logicalInterface{
|
p := physicalInterface{logicalInterface{
|
||||||
name: "testname",
|
name: "testname",
|
||||||
children: []InterfaceGenerator{
|
children: []networkInterface{
|
||||||
&bondInterface{
|
&bondInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "testbond1",
|
name: "testbond1",
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
},
|
},
|
||||||
&vlanInterface{
|
&vlanInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
@@ -67,14 +68,14 @@ VLAN=testvlan2
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBondInterfaceName(t *testing.T) {
|
func TestBondInterfaceName(t *testing.T) {
|
||||||
b := bondInterface{logicalInterface{name: "testname"}, nil}
|
b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
|
||||||
if b.Name() != "testname" {
|
if b.Name() != "testname" {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBondInterfaceNetdev(t *testing.T) {
|
func TestBondInterfaceNetdev(t *testing.T) {
|
||||||
b := bondInterface{logicalInterface{name: "testname"}, nil}
|
b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
|
||||||
netdev := `[NetDev]
|
netdev := `[NetDev]
|
||||||
Kind=bond
|
Kind=bond
|
||||||
Name=testname
|
Name=testname
|
||||||
@@ -96,12 +97,13 @@ func TestBondInterfaceNetwork(t *testing.T) {
|
|||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "testname",
|
name: "testname",
|
||||||
config: configMethodDHCP{},
|
config: configMethodDHCP{},
|
||||||
children: []InterfaceGenerator{
|
children: []networkInterface{
|
||||||
&bondInterface{
|
&bondInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "testbond1",
|
name: "testbond1",
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
},
|
},
|
||||||
&vlanInterface{
|
&vlanInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
@@ -120,6 +122,7 @@ func TestBondInterfaceNetwork(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
}
|
}
|
||||||
network := `[Match]
|
network := `[Match]
|
||||||
Name=testname
|
Name=testname
|
||||||
@@ -143,16 +146,26 @@ func TestVLANInterfaceName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestVLANInterfaceNetdev(t *testing.T) {
|
func TestVLANInterfaceNetdev(t *testing.T) {
|
||||||
v := vlanInterface{logicalInterface{name: "testname"}, 1, ""}
|
for _, tt := range []struct {
|
||||||
netdev := `[NetDev]
|
i vlanInterface
|
||||||
Kind=vlan
|
l string
|
||||||
Name=testname
|
}{
|
||||||
|
{
|
||||||
[VLAN]
|
vlanInterface{logicalInterface{name: "testname"}, 1, ""},
|
||||||
Id=1
|
"[NetDev]\nKind=vlan\nName=testname\n\n[VLAN]\nId=1\n",
|
||||||
`
|
},
|
||||||
if v.Netdev() != netdev {
|
{
|
||||||
t.FailNow()
|
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) {
|
func TestBuildInterfacesLo(t *testing.T) {
|
||||||
stanzas := []*stanzaInterface{
|
stanzas := []*stanzaInterface{
|
||||||
&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) {
|
func TestBuildInterfaces(t *testing.T) {
|
||||||
stanzas := []*stanzaInterface{
|
stanzas := []*stanzaInterface{
|
||||||
&stanzaInterface{
|
&stanzaInterface{
|
||||||
@@ -239,7 +382,9 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
auto: false,
|
auto: false,
|
||||||
configMethod: configMethodManual{},
|
configMethod: configMethodManual{},
|
||||||
options: map[string][]string{
|
options: map[string][]string{
|
||||||
"slaves": []string{"eth0"},
|
"bond-slaves": []string{"eth0"},
|
||||||
|
"bond-mode": []string{"4"},
|
||||||
|
"bond-miimon": []string{"100"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&stanzaInterface{
|
&stanzaInterface{
|
||||||
@@ -248,7 +393,7 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
auto: false,
|
auto: false,
|
||||||
configMethod: configMethodManual{},
|
configMethod: configMethodManual{},
|
||||||
options: map[string][]string{
|
options: map[string][]string{
|
||||||
"slaves": []string{"bond0"},
|
"bond-slaves": []string{"bond0"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&stanzaInterface{
|
&stanzaInterface{
|
||||||
@@ -275,43 +420,53 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
interfaces := buildInterfaces(stanzas)
|
interfaces := buildInterfaces(stanzas)
|
||||||
vlan1 := &vlanInterface{
|
vlan1 := &vlanInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "vlan1",
|
name: "vlan1",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []InterfaceGenerator{},
|
children: []networkInterface{},
|
||||||
|
configDepth: 0,
|
||||||
},
|
},
|
||||||
1,
|
1,
|
||||||
"bond0",
|
"bond0",
|
||||||
}
|
}
|
||||||
vlan0 := &vlanInterface{
|
vlan0 := &vlanInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "vlan0",
|
name: "vlan0",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []InterfaceGenerator{},
|
children: []networkInterface{},
|
||||||
|
configDepth: 0,
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
"eth0",
|
"eth0",
|
||||||
}
|
}
|
||||||
bond1 := &bondInterface{
|
bond1 := &bondInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "bond1",
|
name: "bond1",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []InterfaceGenerator{},
|
children: []networkInterface{},
|
||||||
|
configDepth: 0,
|
||||||
},
|
},
|
||||||
[]string{"bond0"},
|
[]string{"bond0"},
|
||||||
|
map[string]string{},
|
||||||
}
|
}
|
||||||
bond0 := &bondInterface{
|
bond0 := &bondInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "bond0",
|
name: "bond0",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []InterfaceGenerator{vlan1, bond1},
|
children: []networkInterface{bond1, vlan1},
|
||||||
|
configDepth: 1,
|
||||||
},
|
},
|
||||||
[]string{"eth0"},
|
[]string{"eth0"},
|
||||||
|
map[string]string{
|
||||||
|
"mode": "4",
|
||||||
|
"miimon": "100",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
eth0 := &physicalInterface{
|
eth0 := &physicalInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "eth0",
|
name: "eth0",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []InterfaceGenerator{vlan0, bond0},
|
children: []networkInterface{bond0, vlan0},
|
||||||
|
configDepth: 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1}
|
expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1}
|
||||||
@@ -319,3 +474,19 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
t.FailNow()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -40,13 +40,16 @@ type configMethodStatic struct {
|
|||||||
address net.IPNet
|
address net.IPNet
|
||||||
nameservers []net.IP
|
nameservers []net.IP
|
||||||
routes []route
|
routes []route
|
||||||
|
hwaddress net.HardwareAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
type configMethodLoopback struct{}
|
type configMethodLoopback struct{}
|
||||||
|
|
||||||
type configMethodManual struct{}
|
type configMethodManual struct{}
|
||||||
|
|
||||||
type configMethodDHCP struct{}
|
type configMethodDHCP struct {
|
||||||
|
hwaddress net.HardwareAddr
|
||||||
|
}
|
||||||
|
|
||||||
func parseStanzas(lines []string) (stanzas []stanza, err error) {
|
func parseStanzas(lines []string) (stanzas []stanza, err error) {
|
||||||
rawStanzas, err := splitStanzas(lines)
|
rawStanzas, err := splitStanzas(lines)
|
||||||
@@ -96,7 +99,7 @@ func splitStanzas(lines []string) ([][]string, error) {
|
|||||||
} else if curStanza != nil {
|
} else if curStanza != nil {
|
||||||
curStanza = append(curStanza, line)
|
curStanza = append(curStanza, line)
|
||||||
} else {
|
} 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":
|
case "iface":
|
||||||
return parseInterfaceStanza(attributes, rawStanza[1:])
|
return parseInterfaceStanza(attributes, rawStanza[1:])
|
||||||
default:
|
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 {
|
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 gateways, ok := optionMap["gateway"]; ok {
|
||||||
if len(gateways) == 1 {
|
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"] {
|
for _, nameserver := range optionMap["dns-nameservers"] {
|
||||||
config.nameservers = append(config.nameservers, net.ParseIP(nameserver))
|
config.nameservers = append(config.nameservers, net.ParseIP(nameserver))
|
||||||
}
|
}
|
||||||
@@ -245,9 +253,15 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa
|
|||||||
case "manual":
|
case "manual":
|
||||||
conf = configMethodManual{}
|
conf = configMethodManual{}
|
||||||
case "dhcp":
|
case "dhcp":
|
||||||
conf = configMethodDHCP{}
|
config := configMethodDHCP{}
|
||||||
|
if hwaddress, err := parseHwaddress(optionMap, iface); err == nil {
|
||||||
|
config.hwaddress = hwaddress
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conf = config
|
||||||
default:
|
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 {
|
if _, ok := optionMap["vlan_raw_device"]; ok {
|
||||||
@@ -265,8 +279,20 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa
|
|||||||
return parsePhysicalStanza(iface, conf, attributes, optionMap)
|
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) {
|
func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
|
||||||
options["slaves"] = options["bond-slaves"]
|
|
||||||
return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
|
return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,11 +308,11 @@ func parseVLANStanza(iface string, conf configMethod, attributes []string, optio
|
|||||||
} else if strings.HasPrefix(iface, "vlan") {
|
} else if strings.HasPrefix(iface, "vlan") {
|
||||||
id = strings.TrimPrefix(iface, "vlan")
|
id = strings.TrimPrefix(iface, "vlan")
|
||||||
} else {
|
} 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 {
|
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["id"] = []string{id}
|
||||||
options["raw_device"] = options["vlan_raw_device"]
|
options["raw_device"] = options["vlan_raw_device"]
|
||||||
|
@@ -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{"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 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 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)
|
_, err := parseInterfaceStanza(tt.in, tt.opts)
|
||||||
if err == nil || !strings.HasPrefix(err.Error(), tt.e) {
|
if err == nil || !strings.HasPrefix(err.Error(), tt.e) {
|
||||||
@@ -127,7 +129,7 @@ func TestParseBondStanzaNoSlaves(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
if bond.options["slaves"] != nil {
|
if bond.options["bond-slaves"] != nil {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,9 +152,6 @@ func TestParseBondStanza(t *testing.T) {
|
|||||||
if bond.configMethod != conf {
|
if bond.configMethod != conf {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(bond.options["slaves"], options["bond-slaves"]) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsePhysicalStanza(t *testing.T) {
|
func TestParsePhysicalStanza(t *testing.T) {
|
||||||
@@ -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"})
|
iface, err := parseInterfaceStanza([]string{"mybond", "inet", "manual"}, []string{"bond-slaves eth"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.FailNow()
|
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)
|
iface, err := parseInterfaceStanza([]string{"eth0.1", "inet", "manual"}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.FailNow()
|
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"})
|
iface, err := parseInterfaceStanza([]string{"vlan1", "inet", "manual"}, []string{"vlan_raw_device eth"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@@ -18,6 +18,28 @@ const (
|
|||||||
HTTP_4xx = 4
|
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 {
|
type HttpClient struct {
|
||||||
// Maximum exp backoff duration. Defaults to 5 seconds
|
// Maximum exp backoff duration. Defaults to 5 seconds
|
||||||
MaxBackoff time.Duration
|
MaxBackoff time.Duration
|
||||||
@@ -31,18 +53,42 @@ type HttpClient struct {
|
|||||||
|
|
||||||
// Whether or not to skip TLS verification. Defaults to false
|
// Whether or not to skip TLS verification. Defaults to false
|
||||||
SkipTLS bool
|
SkipTLS bool
|
||||||
|
|
||||||
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpClient() *HttpClient {
|
func NewHttpClient() *HttpClient {
|
||||||
return &HttpClient{
|
hc := &HttpClient{
|
||||||
MaxBackoff: time.Second * 5,
|
MaxBackoff: time.Second * 5,
|
||||||
MaxRetries: 15,
|
MaxRetries: 15,
|
||||||
Timeout: time.Duration(2) * time.Second,
|
Timeout: time.Duration(2) * time.Second,
|
||||||
SkipTLS: false,
|
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
|
interval = interval * 2
|
||||||
if interval > max {
|
if interval > max {
|
||||||
interval = max
|
interval = max
|
||||||
@@ -50,74 +96,61 @@ func expBackoff(interval, max time.Duration) time.Duration {
|
|||||||
return interval
|
return interval
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches a given URL with support for exponential backoff and maximum retries
|
// GetRetry fetches a given URL with support for exponential backoff and maximum retries
|
||||||
func (h *HttpClient) Get(rawurl string) ([]byte, error) {
|
func (h *HttpClient) GetRetry(rawurl string) ([]byte, error) {
|
||||||
if rawurl == "" {
|
if rawurl == "" {
|
||||||
return nil, errors.New("URL is empty. Skipping.")
|
return nil, ErrInvalid{errors.New("URL is empty. Skipping.")}
|
||||||
}
|
}
|
||||||
|
|
||||||
url, err := neturl.Parse(rawurl)
|
url, err := neturl.Parse(rawurl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, ErrInvalid{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfortunately, url.Parse is too generic to throw errors if a URL does not
|
// 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
|
// have a valid HTTP scheme. So, we have to do this extra validation
|
||||||
if !strings.HasPrefix(url.Scheme, "http") {
|
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()
|
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
|
duration := 50 * time.Millisecond
|
||||||
for retry := 1; retry <= h.MaxRetries; retry++ {
|
for retry := 1; retry <= h.MaxRetries; retry++ {
|
||||||
log.Printf("Fetching data from %s. Attempt #%d", dataURL, retry)
|
log.Printf("Fetching data from %s. Attempt #%d", dataURL, retry)
|
||||||
|
|
||||||
resp, err := client.Get(dataURL)
|
data, err := h.Get(dataURL)
|
||||||
|
switch err.(type) {
|
||||||
if err == nil {
|
case ErrNetwork:
|
||||||
defer resp.Body.Close()
|
log.Printf(err.Error())
|
||||||
status := resp.StatusCode / 100
|
case ErrServer:
|
||||||
|
log.Printf(err.Error())
|
||||||
if status == HTTP_2xx {
|
case ErrNotFound:
|
||||||
return ioutil.ReadAll(resp.Body)
|
return data, err
|
||||||
}
|
default:
|
||||||
|
return data, err
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
duration = expBackoff(duration, h.MaxBackoff)
|
duration = ExpBackoff(duration, h.MaxBackoff)
|
||||||
log.Printf("Sleeping for %v...", duration)
|
log.Printf("Sleeping for %v...", duration)
|
||||||
time.Sleep(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())}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@ func TestExpBackoff(t *testing.T) {
|
|||||||
duration := time.Millisecond
|
duration := time.Millisecond
|
||||||
max := time.Hour
|
max := time.Hour
|
||||||
for i := 0; i < math.MaxUint16; i++ {
|
for i := 0; i < math.MaxUint16; i++ {
|
||||||
duration = expBackoff(duration, max)
|
duration = ExpBackoff(duration, max)
|
||||||
if duration < 0 {
|
if duration < 0 {
|
||||||
t.Fatalf("duration too small: %v %v", duration, i)
|
t.Fatalf("duration too small: %v %v", duration, i)
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ func TestGetURLExpBackOff(t *testing.T) {
|
|||||||
ts := httptest.NewServer(mux)
|
ts := httptest.NewServer(mux)
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
data, err := client.Get(ts.URL)
|
data, err := client.GetRetry(ts.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Test case %d produced error: %v", i, err)
|
t.Errorf("Test case %d produced error: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ func TestGetURL4xx(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
_, err := client.Get(ts.URL)
|
_, err := client.GetRetry(ts.URL)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Incorrect result\ngot: %s\nwant: %s", err.Error(), "Not found. HTTP status code: 404")
|
t.Errorf("Incorrect result\ngot: %s\nwant: %s", err.Error(), "Not found. HTTP status code: 404")
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ coreos:
|
|||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
data, err := client.Get(ts.URL)
|
data, err := client.GetRetry(ts.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Incorrect result\ngot: %v\nwant: %v", 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 {
|
for _, test := range tests {
|
||||||
_, err := client.Get(test.url)
|
_, err := client.GetRetry(test.url)
|
||||||
if err == nil || err.Error() != test.want {
|
if err == nil || err.Error() != test.want {
|
||||||
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, test.want)
|
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, test.want)
|
||||||
}
|
}
|
||||||
|
89
system/env_file.go
Normal file
89
system/env_file.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
|
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, value := range pending {
|
||||||
|
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
|
||||||
|
}
|
426
system/env_file_test.go
Normal file
426
system/env_file_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@@ -31,33 +31,55 @@ func (f *File) Permissions() (os.FileMode, error) {
|
|||||||
return os.FileMode(perm), nil
|
return os.FileMode(perm), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteFile(f *File) error {
|
func WriteFile(f *File, root string) (string, error) {
|
||||||
if f.Encoding != "" {
|
if f.Encoding != "" {
|
||||||
return fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
|
return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(path.Dir(f.Path), os.FileMode(0755)); err != nil {
|
fullpath := path.Join(root, f.Path)
|
||||||
return err
|
dir := path.Dir(fullpath)
|
||||||
|
|
||||||
|
if err := EnsureDirectoryExists(dir); err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
perm, err := f.Permissions()
|
perm, err := f.Permissions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(f.Path, []byte(f.Content), perm); err != nil {
|
var tmp *os.File
|
||||||
return err
|
// Create a temporary file in the same directory to ensure it's on the same filesystem
|
||||||
|
if tmp, err = ioutil.TempFile(dir, "cloudinit-temp"); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(tmp.Name(), []byte(f.Content), perm); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmp.Close(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the permissions are as requested (since WriteFile can be affected by sticky bit)
|
||||||
|
if err := os.Chmod(tmp.Name(), perm); err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Owner != "" {
|
if f.Owner != "" {
|
||||||
// We shell out since we don't have a way to look up unix groups natively
|
// We shell out since we don't have a way to look up unix groups natively
|
||||||
cmd := exec.Command("chown", f.Owner, f.Path)
|
cmd := exec.Command("chown", f.Owner, tmp.Name())
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if err := os.Rename(tmp.Name(), fullpath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullpath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureDirectoryExists(dir string) error {
|
func EnsureDirectoryExists(dir string) error {
|
||||||
|
@@ -4,7 +4,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,18 +12,22 @@ func TestWriteFileUnencodedContent(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
t.Fatalf("Unable to create tempdir: %v", err)
|
||||||
}
|
}
|
||||||
defer syscall.Rmdir(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
fullPath := path.Join(dir, "tmp", "foo")
|
fn := "foo"
|
||||||
|
fullPath := path.Join(dir, fn)
|
||||||
|
|
||||||
wf := File{
|
wf := File{
|
||||||
Path: fullPath,
|
Path: fn,
|
||||||
Content: "bar",
|
Content: "bar",
|
||||||
RawFilePermissions: "0644",
|
RawFilePermissions: "0644",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := WriteFile(&wf); err != nil {
|
path, err := WriteFile(&wf, dir)
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("Processing of WriteFile failed: %v", err)
|
t.Fatalf("Processing of WriteFile failed: %v", err)
|
||||||
|
} else if path != fullPath {
|
||||||
|
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fi, err := os.Stat(fullPath)
|
fi, err := os.Stat(fullPath)
|
||||||
@@ -51,15 +54,15 @@ func TestWriteFileInvalidPermission(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
t.Fatalf("Unable to create tempdir: %v", err)
|
||||||
}
|
}
|
||||||
defer syscall.Rmdir(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
wf := File{
|
wf := File{
|
||||||
Path: path.Join(dir, "tmp", "foo"),
|
Path: path.Join(dir, "tmp", "foo"),
|
||||||
Content: "bar",
|
Content: "bar",
|
||||||
RawFilePermissions: "pants",
|
RawFilePermissions: "pants",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := WriteFile(&wf); err == nil {
|
if _, err := WriteFile(&wf, dir); err == nil {
|
||||||
t.Fatalf("Expected error to be raised when writing file with invalid permission")
|
t.Fatalf("Expected error to be raised when writing file with invalid permission")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,17 +72,21 @@ func TestWriteFilePermissions(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
t.Fatalf("Unable to create tempdir: %v", err)
|
||||||
}
|
}
|
||||||
defer syscall.Rmdir(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
fullPath := path.Join(dir, "tmp", "foo")
|
fn := "foo"
|
||||||
|
fullPath := path.Join(dir, fn)
|
||||||
|
|
||||||
wf := File{
|
wf := File{
|
||||||
Path: fullPath,
|
Path: fn,
|
||||||
RawFilePermissions: "0755",
|
RawFilePermissions: "0755",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := WriteFile(&wf); err != nil {
|
path, err := WriteFile(&wf, dir)
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("Processing of WriteFile failed: %v", err)
|
t.Fatalf("Processing of WriteFile failed: %v", err)
|
||||||
|
} else if path != fullPath {
|
||||||
|
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fi, err := os.Stat(fullPath)
|
fi, err := os.Stat(fullPath)
|
||||||
@@ -97,15 +104,15 @@ func TestWriteFileEncodedContent(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
t.Fatalf("Unable to create tempdir: %v", err)
|
||||||
}
|
}
|
||||||
defer syscall.Rmdir(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
wf := File{
|
wf := File{
|
||||||
Path: path.Join(dir, "tmp", "foo"),
|
Path: path.Join(dir, "tmp", "foo"),
|
||||||
Content: "",
|
Content: "",
|
||||||
Encoding: "base64",
|
Encoding: "base64",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := WriteFile(&wf); err == nil {
|
if _, err := WriteFile(&wf, dir); err == nil {
|
||||||
t.Fatalf("Expected error to be raised when writing file with encoding")
|
t.Fatalf("Expected error to be raised when writing file with encoding")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,10 +2,11 @@ package system
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/network"
|
"github.com/coreos/coreos-cloudinit/network"
|
||||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
|
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
|
||||||
@@ -17,6 +18,13 @@ const (
|
|||||||
|
|
||||||
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
if e := restartNetworkd(); e != nil {
|
||||||
|
err = e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO(crawford): Get rid of this once networkd fixes the race
|
||||||
|
// https://bugs.freedesktop.org/show_bug.cgi?id=76077
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
if e := restartNetworkd(); e != nil {
|
if e := restartNetworkd(); e != nil {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
@@ -26,16 +34,17 @@ func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = probe8012q(); err != nil {
|
if err = maybeProbe8012q(interfaces); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return maybeProbeBonding(interfaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
||||||
sysInterfaceMap := make(map[string]*net.Interface)
|
sysInterfaceMap := make(map[string]*net.Interface)
|
||||||
if systemInterfaces, err := net.Interfaces(); err == nil {
|
if systemInterfaces, err := net.Interfaces(); err == nil {
|
||||||
for _, iface := range systemInterfaces {
|
for _, iface := range systemInterfaces {
|
||||||
|
iface := iface
|
||||||
sysInterfaceMap[iface.Name] = &iface
|
sysInterfaceMap[iface.Name] = &iface
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -44,6 +53,7 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
|||||||
|
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok {
|
if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok {
|
||||||
|
log.Printf("Taking down interface %q\n", systemInterface.Name)
|
||||||
if err := netlink.NetworkLinkDown(systemInterface); err != nil {
|
if err := netlink.NetworkLinkDown(systemInterface); err != nil {
|
||||||
fmt.Printf("Error while downing interface %q (%s). Continuing...\n", systemInterface.Name, err)
|
fmt.Printf("Error while downing interface %q (%s). Continuing...\n", systemInterface.Name, err)
|
||||||
}
|
}
|
||||||
@@ -53,26 +63,45 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func probe8012q() error {
|
func maybeProbe8012q(interfaces []network.InterfaceGenerator) error {
|
||||||
return exec.Command("modprobe", "8021q").Run()
|
for _, iface := range interfaces {
|
||||||
|
if iface.Type() == "vlan" {
|
||||||
|
log.Printf("Probing LKM %q (%q)\n", "8021q", "8021q")
|
||||||
|
return exec.Command("modprobe", "8021q").Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeProbeBonding(interfaces []network.InterfaceGenerator) error {
|
||||||
|
args := []string{"bonding"}
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
if iface.Type() == "bond" {
|
||||||
|
args = append(args, strings.Split(iface.ModprobeParams(), " ")...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("Probing LKM %q (%q)\n", "bonding", args)
|
||||||
|
return exec.Command("modprobe", args...).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartNetworkd() error {
|
func restartNetworkd() error {
|
||||||
_, err := RunUnitCommand("restart", "systemd-networkd.service")
|
log.Printf("Restarting networkd.service\n")
|
||||||
|
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
|
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
filename := path.Join(runtimeNetworkPath, fmt.Sprintf("%s.netdev", iface.Name()))
|
filename := fmt.Sprintf("%s.netdev", iface.Filename())
|
||||||
if err := writeConfig(filename, iface.Netdev()); err != nil {
|
if err := writeConfig(filename, iface.Netdev()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.link", iface.Name()))
|
filename = fmt.Sprintf("%s.link", iface.Filename())
|
||||||
if err := writeConfig(filename, iface.Link()); err != nil {
|
if err := writeConfig(filename, iface.Link()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.network", iface.Name()))
|
filename = fmt.Sprintf("%s.network", iface.Filename())
|
||||||
if err := writeConfig(filename, iface.Network()); err != nil {
|
if err := writeConfig(filename, iface.Network()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -84,6 +113,7 @@ func writeConfig(filename string, config string) error {
|
|||||||
if config == "" {
|
if config == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
log.Printf("Writing networkd unit %q\n", filename)
|
||||||
return ioutil.WriteFile(filename, []byte(config), 0444)
|
_, err := WriteFile(&File{Content: config, Path: filename}, runtimeNetworkPath)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -13,63 +13,21 @@ import (
|
|||||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
|
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func NewUnitManager(root string) UnitManager {
|
||||||
|
return &systemd{root}
|
||||||
|
}
|
||||||
|
|
||||||
|
type systemd struct {
|
||||||
|
root string
|
||||||
|
}
|
||||||
|
|
||||||
// fakeMachineID is placed on non-usr CoreOS images and should
|
// fakeMachineID is placed on non-usr CoreOS images and should
|
||||||
// never be used as a true MachineID
|
// never be used as a true MachineID
|
||||||
const fakeMachineID = "42000000000000000000000000000042"
|
const fakeMachineID = "42000000000000000000000000000042"
|
||||||
|
|
||||||
// Name for drop-in service configuration files created by cloudconfig
|
|
||||||
const cloudConfigDropIn = "20-cloudinit.conf"
|
|
||||||
|
|
||||||
type Unit struct {
|
|
||||||
Name string
|
|
||||||
Mask bool
|
|
||||||
Enable bool
|
|
||||||
Runtime bool
|
|
||||||
Content string
|
|
||||||
Command string
|
|
||||||
|
|
||||||
// For drop-in units, a cloudinit.conf is generated.
|
|
||||||
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
|
|
||||||
// until the correct behaviour for multiple drop-in units is determined.
|
|
||||||
DropIn bool `yaml:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unit) Type() string {
|
|
||||||
ext := filepath.Ext(u.Name)
|
|
||||||
return strings.TrimLeft(ext, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unit) Group() (group string) {
|
|
||||||
t := u.Type()
|
|
||||||
if t == "network" || t == "netdev" || t == "link" {
|
|
||||||
group = "network"
|
|
||||||
} else {
|
|
||||||
group = "system"
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type Script []byte
|
|
||||||
|
|
||||||
// UnitDestination builds the appropriate absolute file path for
|
|
||||||
// the given Unit. The root argument indicates the effective base
|
|
||||||
// directory of the system (similar to a chroot).
|
|
||||||
func UnitDestination(u *Unit, root string) string {
|
|
||||||
dir := "etc"
|
|
||||||
if u.Runtime {
|
|
||||||
dir = "run"
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.DropIn {
|
|
||||||
return path.Join(root, dir, "systemd", u.Group(), fmt.Sprintf("%s.d", u.Name), cloudConfigDropIn)
|
|
||||||
} else {
|
|
||||||
return path.Join(root, dir, "systemd", u.Group(), u.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaceUnit writes a unit file at the provided destination, creating
|
// PlaceUnit writes a unit file at the provided destination, creating
|
||||||
// parent directories as necessary.
|
// parent directories as necessary.
|
||||||
func PlaceUnit(u *Unit, dst string) error {
|
func (s *systemd) PlaceUnit(u *Unit, dst string) error {
|
||||||
dir := filepath.Dir(dst)
|
dir := filepath.Dir(dst)
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
|
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
|
||||||
@@ -78,12 +36,12 @@ func PlaceUnit(u *Unit, dst string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
file := File{
|
file := File{
|
||||||
Path: dst,
|
Path: filepath.Base(dst),
|
||||||
Content: u.Content,
|
Content: u.Content,
|
||||||
RawFilePermissions: "0644",
|
RawFilePermissions: "0644",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := WriteFile(&file)
|
_, err := WriteFile(&file, dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -91,7 +49,7 @@ func PlaceUnit(u *Unit, dst string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnableUnitFile(unit string, runtime bool) error {
|
func (s *systemd) EnableUnitFile(unit string, runtime bool) error {
|
||||||
conn, err := dbus.New()
|
conn, err := dbus.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -102,7 +60,7 @@ func EnableUnitFile(unit string, runtime bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunUnitCommand(command, unit string) (string, error) {
|
func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
|
||||||
conn, err := dbus.New()
|
conn, err := dbus.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -131,7 +89,7 @@ func RunUnitCommand(command, unit string) (string, error) {
|
|||||||
return fn(unit, "replace")
|
return fn(unit, "replace")
|
||||||
}
|
}
|
||||||
|
|
||||||
func DaemonReload() error {
|
func (s *systemd) DaemonReload() error {
|
||||||
conn, err := dbus.New()
|
conn, err := dbus.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -140,6 +98,57 @@ func DaemonReload() error {
|
|||||||
return conn.Reload()
|
return conn.Reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaskUnit masks the given Unit by symlinking its unit file to
|
||||||
|
// /dev/null, analogous to `systemctl mask`.
|
||||||
|
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
||||||
|
// file at the location*, to ensure that the mask will succeed.
|
||||||
|
func (s *systemd) MaskUnit(unit *Unit) error {
|
||||||
|
masked := unit.Destination(s.root)
|
||||||
|
if _, err := os.Stat(masked); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err := os.Remove(masked); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Symlink("/dev/null", masked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
|
||||||
|
// associated with the given Unit is empty or appears to be a symlink to
|
||||||
|
// /dev/null, it is removed.
|
||||||
|
func (s *systemd) UnmaskUnit(unit *Unit) error {
|
||||||
|
masked := unit.Destination(s.root)
|
||||||
|
ne, err := nullOrEmpty(masked)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ne {
|
||||||
|
log.Printf("%s is not null or empty, refusing to unmask", masked)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.Remove(masked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nullOrEmpty checks whether a given path appears to be an empty regular file
|
||||||
|
// or a symlink to /dev/null
|
||||||
|
func nullOrEmpty(path string) (bool, error) {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
m := fi.Mode()
|
||||||
|
if m.IsRegular() && fi.Size() <= 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if m&os.ModeCharDevice > 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ExecuteScript(scriptPath string) (string, error) {
|
func ExecuteScript(scriptPath string) (string, error) {
|
||||||
props := []dbus.Property{
|
props := []dbus.Property{
|
||||||
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
|
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
|
||||||
@@ -178,19 +187,3 @@ func MachineID(root string) string {
|
|||||||
|
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaskUnit masks a Unit by the given name by symlinking its unit file (in
|
|
||||||
// /etc/systemd/system) to /dev/null, analogous to `systemctl mask`
|
|
||||||
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
|
||||||
// file* in /etc/systemd/system, to ensure that the mask will succeed.
|
|
||||||
func MaskUnit(unit string, root string) error {
|
|
||||||
masked := path.Join(root, "etc", "systemd", "system", unit)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
@@ -25,13 +25,15 @@ Address=10.209.171.177/19
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
dst := UnitDestination(&u, dir)
|
sd := &systemd{dir}
|
||||||
|
|
||||||
|
dst := u.Destination(dir)
|
||||||
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
||||||
if dst != expectDst {
|
if dst != expectDst {
|
||||||
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
|
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := PlaceUnit(&u, dst); err != nil {
|
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||||
t.Fatalf("PlaceUnit failed: %v", err)
|
t.Fatalf("PlaceUnit failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,18 +71,18 @@ func TestUnitDestination(t *testing.T) {
|
|||||||
DropIn: false,
|
DropIn: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
dst := UnitDestination(&u, dir)
|
dst := u.Destination(dir)
|
||||||
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
|
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
|
||||||
if dst != expectDst {
|
if dst != expectDst {
|
||||||
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst)
|
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.DropIn = true
|
u.DropIn = true
|
||||||
|
|
||||||
dst = UnitDestination(&u, dir)
|
dst = u.Destination(dir)
|
||||||
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
|
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
|
||||||
if dst != expectDst {
|
if dst != expectDst {
|
||||||
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst)
|
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,13 +102,15 @@ Where=/media/state
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
dst := UnitDestination(&u, dir)
|
sd := &systemd{dir}
|
||||||
|
|
||||||
|
dst := u.Destination(dir)
|
||||||
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
||||||
if dst != expectDst {
|
if dst != expectDst {
|
||||||
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
|
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := PlaceUnit(&u, dst); err != nil {
|
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||||
t.Fatalf("PlaceUnit failed: %v", err)
|
t.Fatalf("PlaceUnit failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,8 +159,11 @@ func TestMaskUnit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
sd := &systemd{dir}
|
||||||
|
|
||||||
// Ensure mask works with units that do not currently exist
|
// Ensure mask works with units that do not currently exist
|
||||||
if err := MaskUnit("foo.service", dir); err != nil {
|
uf := &Unit{Name: "foo.service"}
|
||||||
|
if err := sd.MaskUnit(uf); err != nil {
|
||||||
t.Fatalf("Unable to mask new unit: %v", err)
|
t.Fatalf("Unable to mask new unit: %v", err)
|
||||||
}
|
}
|
||||||
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
|
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
|
||||||
@@ -169,11 +176,12 @@ func TestMaskUnit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure mask works with unit files that already exist
|
// Ensure mask works with unit files that already exist
|
||||||
|
ub := &Unit{Name: "bar.service"}
|
||||||
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
|
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
|
||||||
if _, err := os.Create(barPath); err != nil {
|
if _, err := os.Create(barPath); err != nil {
|
||||||
t.Fatalf("Error creating new unit file: %v", err)
|
t.Fatalf("Error creating new unit file: %v", err)
|
||||||
}
|
}
|
||||||
if err := MaskUnit("bar.service", dir); err != nil {
|
if err := sd.MaskUnit(ub); err != nil {
|
||||||
t.Fatalf("Unable to mask existing unit: %v", err)
|
t.Fatalf("Unable to mask existing unit: %v", err)
|
||||||
}
|
}
|
||||||
barTgt, err := os.Readlink(barPath)
|
barTgt, err := os.Readlink(barPath)
|
||||||
@@ -184,3 +192,96 @@ func TestMaskUnit(t *testing.T) {
|
|||||||
t.Fatalf("unit not masked, got unit target", barTgt)
|
t.Fatalf("unit not masked, got unit target", barTgt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmaskUnit(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)
|
||||||
|
|
||||||
|
sd := &systemd{dir}
|
||||||
|
|
||||||
|
nilUnit := &Unit{Name: "null.service"}
|
||||||
|
if err := sd.UnmaskUnit(nilUnit); err != nil {
|
||||||
|
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uf := &Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}
|
||||||
|
dst := uf.Destination(dir)
|
||||||
|
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
|
||||||
|
t.Fatalf("Unable to create unit directory: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Create(dst); err != nil {
|
||||||
|
t.Fatalf("Unable to write unit file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
|
||||||
|
t.Fatalf("Unable to write unit file: %v", err)
|
||||||
|
}
|
||||||
|
if err := sd.UnmaskUnit(uf); err != nil {
|
||||||
|
t.Errorf("unmask of non-empty unit returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
got, _ := ioutil.ReadFile(dst)
|
||||||
|
if string(got) != uf.Content {
|
||||||
|
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
|
||||||
|
}
|
||||||
|
|
||||||
|
ub := &Unit{Name: "bar.service"}
|
||||||
|
dst = ub.Destination(dir)
|
||||||
|
if err := os.Symlink("/dev/null", dst); err != nil {
|
||||||
|
t.Fatalf("Unable to create masked unit: %v", err)
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
t.Errorf("expected %s to not exist after unmask, but got err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNullOrEmpty(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)
|
||||||
|
|
||||||
|
non := path.Join(dir, "does_not_exist")
|
||||||
|
ne, err := nullOrEmpty(non)
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
t.Errorf("nullOrEmpty on nonexistent file returned bad error: %v", err)
|
||||||
|
}
|
||||||
|
if ne {
|
||||||
|
t.Errorf("nullOrEmpty returned true unxpectedly")
|
||||||
|
}
|
||||||
|
|
||||||
|
regEmpty := path.Join(dir, "regular_empty_file")
|
||||||
|
_, err = os.Create(regEmpty)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create tempfile: %v", err)
|
||||||
|
}
|
||||||
|
gotNe, gotErr := nullOrEmpty(regEmpty)
|
||||||
|
if !gotNe || gotErr != nil {
|
||||||
|
t.Errorf("nullOrEmpty of regular empty file returned %t, %v - want true, nil", gotNe, gotErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := path.Join(dir, "regular_file")
|
||||||
|
if err := ioutil.WriteFile(reg, []byte("asdf"), 700); err != nil {
|
||||||
|
t.Fatalf("Unable to create tempfile: %v", err)
|
||||||
|
}
|
||||||
|
gotNe, gotErr = nullOrEmpty(reg)
|
||||||
|
if gotNe || gotErr != nil {
|
||||||
|
t.Errorf("nullOrEmpty of regular file returned %t, %v - want false, nil", gotNe, gotErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
null := path.Join(dir, "null")
|
||||||
|
if err := os.Symlink(os.DevNull, null); err != nil {
|
||||||
|
t.Fatalf("Unable to create /dev/null link: %s", err)
|
||||||
|
}
|
||||||
|
gotNe, gotErr = nullOrEmpty(null)
|
||||||
|
if !gotNe || gotErr != nil {
|
||||||
|
t.Errorf("nullOrEmpty of null symlink returned %t, %v - want true, nil", gotNe, gotErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
67
system/unit.go
Normal file
67
system/unit.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
2
test
2
test
@@ -18,7 +18,7 @@ declare -a TESTPKGS=(initialize system datasource pkg network)
|
|||||||
if [ -z "$PKG" ]; then
|
if [ -z "$PKG" ]; then
|
||||||
GOFMTPATH="$TESTPKGS coreos-cloudinit.go"
|
GOFMTPATH="$TESTPKGS coreos-cloudinit.go"
|
||||||
# prepend repo path to each package
|
# prepend repo path to each package
|
||||||
TESTPKGS=${TESTPKGS[@]/#/${REPO_PATH}/}
|
TESTPKGS="${TESTPKGS[@]/#/${REPO_PATH}/} ./"
|
||||||
else
|
else
|
||||||
GOFMTPATH="$TESTPKGS"
|
GOFMTPATH="$TESTPKGS"
|
||||||
# strip out slashes and dots from PKG=./foo/
|
# strip out slashes and dots from PKG=./foo/
|
||||||
|
@@ -3,9 +3,9 @@
|
|||||||
ACTION!="add|change", GOTO="coreos_configdrive_end"
|
ACTION!="add|change", GOTO="coreos_configdrive_end"
|
||||||
|
|
||||||
# A normal config drive. Block device formatted with iso9660 or fat
|
# A normal config drive. Block device formatted with iso9660 or fat
|
||||||
SUBSYSTEM=="block", ENV{ID_FS_TYPE}=="iso9660|vfat", ENV{ID_FS_LABEL}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="configdrive-block.service"
|
SUBSYSTEM=="block", ENV{ID_FS_TYPE}=="iso9660|vfat", ENV{ID_FS_LABEL}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="media-configdrive.mount"
|
||||||
|
|
||||||
# Addtionally support virtfs from QEMU
|
# Addtionally support virtfs from QEMU
|
||||||
SUBSYSTEM=="virtio", DRIVER=="9pnet_virtio", ATTR{mount_tag}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="configdrive-virtfs.service"
|
SUBSYSTEM=="virtio", DRIVER=="9pnet_virtio", ATTR{mount_tag}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="media-configvirtfs.mount"
|
||||||
|
|
||||||
LABEL="coreos_configdrive_end"
|
LABEL="coreos_configdrive_end"
|
||||||
|
@@ -1,11 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Mount config drive
|
|
||||||
Conflicts=configdrive-virtfs.service umount.target
|
|
||||||
ConditionPathIsMountPoint=!/media/configdrive
|
|
||||||
# Only mount config drive block devices automatically in virtual machines
|
|
||||||
ConditionVirtualization=vm
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
RemainAfterExit=no
|
|
||||||
ExecStart=/bin/mount -t auto -o ro,x-mount.mkdir LABEL=config-2 /media/configdrive
|
|
@@ -1,14 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Mount config drive from virtfs
|
|
||||||
Conflicts=configdrive-block.service umount.target
|
|
||||||
ConditionPathIsMountPoint=!/media/configdrive
|
|
||||||
ConditionVirtualization=vm
|
|
||||||
|
|
||||||
# Support old style setup for now
|
|
||||||
Wants=addon-run@media-configdrive.service addon-config@media-configdrive.service
|
|
||||||
Before=addon-run@media-configdrive.service addon-config@media-configdrive.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
RemainAfterExit=no
|
|
||||||
ExecStart=/bin/mount -t 9p -o trans=virtio,version=9p2000.L,x-mount.mkdir config-2 /media/configdrive
|
|
13
units/media-configdrive.mount
Normal file
13
units/media-configdrive.mount
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Wants=user-configdrive.service
|
||||||
|
Before=user-configdrive.service
|
||||||
|
# Only mount config drive block devices automatically in virtual machines
|
||||||
|
# or any host that has it explicitly enabled and not explicitly disabled.
|
||||||
|
ConditionVirtualization=|vm
|
||||||
|
ConditionKernelCommandLine=|coreos.configdrive=1
|
||||||
|
ConditionKernelCommandLine=!coreos.configdrive=0
|
||||||
|
|
||||||
|
[Mount]
|
||||||
|
What=LABEL=config-2
|
||||||
|
Where=/media/configdrive
|
||||||
|
Options=ro
|
18
units/media-configvirtfs.mount
Normal file
18
units/media-configvirtfs.mount
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Wants=user-configvirtfs.service
|
||||||
|
Before=user-configvirtfs.service
|
||||||
|
# Only mount config drive block devices automatically in virtual machines
|
||||||
|
# or any host that has it explicitly enabled and not explicitly disabled.
|
||||||
|
ConditionVirtualization=|vm
|
||||||
|
ConditionKernelCommandLine=|coreos.configdrive=1
|
||||||
|
ConditionKernelCommandLine=!coreos.configdrive=0
|
||||||
|
|
||||||
|
# Support old style setup for now
|
||||||
|
Wants=addon-run@media-configvirtfs.service addon-config@media-configvirtfs.service
|
||||||
|
Before=addon-run@media-configvirtfs.service addon-config@media-configvirtfs.service
|
||||||
|
|
||||||
|
[Mount]
|
||||||
|
What=config-2
|
||||||
|
Where=/media/configvirtfs
|
||||||
|
Options=ro,trans=virtio,version=9p2000.L
|
||||||
|
Type=9p
|
@@ -1,5 +1,10 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Watch for a cloud-config at /media/configdrive
|
Description=Watch for a cloud-config at /media/configdrive
|
||||||
|
|
||||||
|
# Note: This unit is essentially just here as a fall-back mechanism to
|
||||||
|
# trigger cloudinit if it isn't triggered explicitly by other means
|
||||||
|
# such as by a Wants= in the mount unit. This ensures we handle the
|
||||||
|
# case where /media/configdrive is provided to a CoreOS container.
|
||||||
|
|
||||||
[Path]
|
[Path]
|
||||||
DirectoryNotEmpty=/media/configdrive
|
DirectoryNotEmpty=/media/configdrive
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Load cloud-config from /media/configdrive
|
Description=Load cloud-config from /media/configdrive
|
||||||
Requires=coreos-setup-environment.service
|
Requires=coreos-setup-environment.service
|
||||||
After=coreos-setup-environment.service
|
After=coreos-setup-environment.service system-config.target
|
||||||
Before=user-config.target
|
Before=user-config.target
|
||||||
|
|
||||||
# HACK: work around ordering between config drive and ec2 metadata It is
|
# 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.
|
# systemd knows about the ordering as early as possible.
|
||||||
# coreos-cloudinit could implement a simple lock but that cannot be used
|
# coreos-cloudinit could implement a simple lock but that cannot be used
|
||||||
# until after the systemd dbus calls are made non-blocking.
|
# until after the systemd dbus calls are made non-blocking.
|
||||||
After=system-cloudinit@usr-share-oem-cloud\x2dconfig.yml.service
|
|
||||||
After=ec2-cloudinit.service
|
After=ec2-cloudinit.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
11
units/user-configvirtfs.service
Normal file
11
units/user-configvirtfs.service
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Load cloud-config from /media/configvirtfs
|
||||||
|
Requires=coreos-setup-environment.service
|
||||||
|
After=coreos-setup-environment.service
|
||||||
|
Before=user-config.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
EnvironmentFile=-/etc/environment
|
||||||
|
ExecStart=/usr/bin/coreos-cloudinit --from-configdrive=/media/configvirtfs
|
Reference in New Issue
Block a user