Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2f52ad4ef8 | ||
|
735d6c6161 | ||
|
1cf275bad6 | ||
|
f1c97cb4d5 | ||
|
d143904aa9 | ||
|
c428ce2cc5 | ||
|
dfb5b4fc3a | ||
|
97d5538533 | ||
|
6b8f82b5d3 | ||
|
facde6609f | ||
|
d68ae84b37 | ||
|
54aa39543b | ||
|
8566a2c118 | ||
|
49ac083af5 | ||
|
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 |
@@ -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"
|
||||||
@@ -114,7 +116,7 @@ The `reboot-strategy` parameter also affects the behaviour of [locksmith](https:
|
|||||||
|
|
||||||
##### Example
|
##### Example
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
coreos:
|
coreos:
|
||||||
update:
|
update:
|
||||||
@@ -123,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.
|
||||||
@@ -138,7 +142,7 @@ The `coreos.units.*` parameters define a list of arbitrary systemd units to star
|
|||||||
|
|
||||||
Write a unit to disk, automatically starting it.
|
Write a unit to disk, automatically starting it.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
@@ -159,7 +163,7 @@ coreos:
|
|||||||
|
|
||||||
Start the built-in `etcd` and `fleet` services:
|
Start the built-in `etcd` and `fleet` services:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
@@ -177,7 +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:
|
||||||
@@ -189,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
|
||||||
@@ -203,7 +207,7 @@ All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the use
|
|||||||
- **name**: Required. Login name of user
|
- **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
|
||||||
@@ -222,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:
|
||||||
@@ -261,7 +265,7 @@ Using a higher number of rounds will help create more secure passwords, but give
|
|||||||
|
|
||||||
Using the `coreos-ssh-import-github` field, we can import public SSH keys from a GitHub user to use as authorized keys to a server.
|
Using the `coreos-ssh-import-github` field, we can import public SSH keys from a GitHub user to use as authorized keys to a server.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
users:
|
users:
|
||||||
@@ -274,7 +278,7 @@ users:
|
|||||||
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
|
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
|
||||||
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
|
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
users:
|
users:
|
||||||
@@ -284,7 +288,7 @@ users:
|
|||||||
|
|
||||||
You can also specify any URL whose response matches the JSON format for public keys:
|
You can also specify any URL whose response matches the JSON format for public keys:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
users:
|
users:
|
||||||
@@ -304,7 +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
|
||||||
@@ -321,7 +325,7 @@ Currently, the only supported value is "localhost" which will cause your system'
|
|||||||
to resolve to "127.0.0.1". This is helpful when the host does not have DNS
|
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...]
|
||||||
|
```
|
||||||
|
@@ -8,13 +8,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/datasource"
|
"github.com/coreos/coreos-cloudinit/datasource"
|
||||||
|
"github.com/coreos/coreos-cloudinit/datasource/configdrive"
|
||||||
|
"github.com/coreos/coreos-cloudinit/datasource/file"
|
||||||
|
"github.com/coreos/coreos-cloudinit/datasource/metadata/ec2"
|
||||||
|
"github.com/coreos/coreos-cloudinit/datasource/proc_cmdline"
|
||||||
|
"github.com/coreos/coreos-cloudinit/datasource/url"
|
||||||
"github.com/coreos/coreos-cloudinit/initialize"
|
"github.com/coreos/coreos-cloudinit/initialize"
|
||||||
"github.com/coreos/coreos-cloudinit/pkg"
|
"github.com/coreos/coreos-cloudinit/pkg"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "0.8.4"
|
version = "0.9.2"
|
||||||
datasourceInterval = 100 * time.Millisecond
|
datasourceInterval = 100 * time.Millisecond
|
||||||
datasourceMaxInterval = 30 * time.Second
|
datasourceMaxInterval = 30 * time.Second
|
||||||
datasourceTimeout = 5 * time.Minute
|
datasourceTimeout = 5 * time.Minute
|
||||||
@@ -24,11 +29,12 @@ var (
|
|||||||
printVersion bool
|
printVersion bool
|
||||||
ignoreFailure bool
|
ignoreFailure bool
|
||||||
sources struct {
|
sources struct {
|
||||||
file string
|
file string
|
||||||
configDrive string
|
configDrive string
|
||||||
metadataService bool
|
metadataService bool
|
||||||
url string
|
ec2MetadataService string
|
||||||
procCmdLine bool
|
url string
|
||||||
|
procCmdLine bool
|
||||||
}
|
}
|
||||||
convertNetconf string
|
convertNetconf string
|
||||||
workspace string
|
workspace string
|
||||||
@@ -40,9 +46,10 @@ func init() {
|
|||||||
flag.BoolVar(&ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")
|
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.file, "from-file", "", "Read user-data from provided file")
|
||||||
flag.StringVar(&sources.configDrive, "from-configdrive", "", "Read data from provided cloud-drive directory")
|
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.BoolVar(&sources.metadataService, "from-metadata-service", false, "[DEPRECATED - Use -from-ec2-metadata] Download data from metadata service")
|
||||||
|
flag.StringVar(&sources.ec2MetadataService, "from-ec2-metadata", "", "Download data from the provided metadata service")
|
||||||
flag.StringVar(&sources.url, "from-url", "", "Download user-data from provided url")
|
flag.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.BoolVar(&sources.procCmdLine, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=<url>', using the cloud-config served by an HTTP GET to <url>", proc_cmdline.ProcCmdlineLocation, proc_cmdline.ProcCmdlineCloudConfigFlag))
|
||||||
flag.StringVar(&convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files (requires the -from-configdrive flag)")
|
flag.StringVar(&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(&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")
|
flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
|
||||||
@@ -78,7 +85,7 @@ func main() {
|
|||||||
|
|
||||||
dss := getDatasources()
|
dss := getDatasources()
|
||||||
if len(dss) == 0 {
|
if len(dss) == 0 {
|
||||||
fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-metadata-service, --from-url or --from-proc-cmdline")
|
fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --from-url or --from-proc-cmdline")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +179,7 @@ func main() {
|
|||||||
func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudConfig) {
|
func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudConfig) {
|
||||||
if mdcc.Hostname != "" {
|
if mdcc.Hostname != "" {
|
||||||
if udcc.Hostname != "" {
|
if udcc.Hostname != "" {
|
||||||
fmt.Printf("Warning: user-data hostname (%s) overrides metadata hostname (%s)", udcc.Hostname, mdcc.Hostname)
|
fmt.Printf("Warning: user-data hostname (%s) overrides metadata hostname (%s)\n", udcc.Hostname, mdcc.Hostname)
|
||||||
} else {
|
} else {
|
||||||
udcc.Hostname = mdcc.Hostname
|
udcc.Hostname = mdcc.Hostname
|
||||||
}
|
}
|
||||||
@@ -183,7 +190,7 @@ func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudCon
|
|||||||
}
|
}
|
||||||
if mdcc.NetworkConfigPath != "" {
|
if mdcc.NetworkConfigPath != "" {
|
||||||
if udcc.NetworkConfigPath != "" {
|
if udcc.NetworkConfigPath != "" {
|
||||||
fmt.Printf("Warning: user-data NetworkConfigPath %s overrides metadata NetworkConfigPath %s", udcc.NetworkConfigPath, mdcc.NetworkConfigPath)
|
fmt.Printf("Warning: user-data NetworkConfigPath %s overrides metadata NetworkConfigPath %s\n", udcc.NetworkConfigPath, mdcc.NetworkConfigPath)
|
||||||
} else {
|
} else {
|
||||||
udcc.NetworkConfigPath = mdcc.NetworkConfigPath
|
udcc.NetworkConfigPath = mdcc.NetworkConfigPath
|
||||||
}
|
}
|
||||||
@@ -196,19 +203,22 @@ func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudCon
|
|||||||
func getDatasources() []datasource.Datasource {
|
func getDatasources() []datasource.Datasource {
|
||||||
dss := make([]datasource.Datasource, 0, 5)
|
dss := make([]datasource.Datasource, 0, 5)
|
||||||
if sources.file != "" {
|
if sources.file != "" {
|
||||||
dss = append(dss, datasource.NewLocalFile(sources.file))
|
dss = append(dss, file.NewDatasource(sources.file))
|
||||||
}
|
}
|
||||||
if sources.url != "" {
|
if sources.url != "" {
|
||||||
dss = append(dss, datasource.NewRemoteFile(sources.url))
|
dss = append(dss, url.NewDatasource(sources.url))
|
||||||
}
|
}
|
||||||
if sources.configDrive != "" {
|
if sources.configDrive != "" {
|
||||||
dss = append(dss, datasource.NewConfigDrive(sources.configDrive))
|
dss = append(dss, configdrive.NewDatasource(sources.configDrive))
|
||||||
}
|
}
|
||||||
if sources.metadataService {
|
if sources.metadataService {
|
||||||
dss = append(dss, datasource.NewMetadataService())
|
dss = append(dss, ec2.NewDatasource(ec2.DefaultAddress))
|
||||||
|
}
|
||||||
|
if sources.ec2MetadataService != "" {
|
||||||
|
dss = append(dss, ec2.NewDatasource(sources.ec2MetadataService))
|
||||||
}
|
}
|
||||||
if sources.procCmdLine {
|
if sources.procCmdLine {
|
||||||
dss = append(dss, datasource.NewProcCmdline())
|
dss = append(dss, proc_cmdline.NewDatasource())
|
||||||
}
|
}
|
||||||
return dss
|
return dss
|
||||||
}
|
}
|
||||||
@@ -240,7 +250,7 @@ func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
|||||||
select {
|
select {
|
||||||
case <-stop:
|
case <-stop:
|
||||||
return
|
return
|
||||||
case <-time.Tick(duration):
|
case <-time.After(duration):
|
||||||
duration = pkg.ExpBackoff(duration, datasourceMaxInterval)
|
duration = pkg.ExpBackoff(duration, datasourceMaxInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,7 +267,7 @@ func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
|||||||
select {
|
select {
|
||||||
case s = <-ds:
|
case s = <-ds:
|
||||||
case <-done:
|
case <-done:
|
||||||
case <-time.Tick(datasourceTimeout):
|
case <-time.After(datasourceTimeout):
|
||||||
}
|
}
|
||||||
|
|
||||||
close(stop)
|
close(stop)
|
||||||
|
@@ -1,48 +0,0 @@
|
|||||||
package datasource
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
type configDrive struct {
|
|
||||||
root string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfigDrive(root string) *configDrive {
|
|
||||||
return &configDrive{path.Join(root, "openstack")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *configDrive) IsAvailable() bool {
|
|
||||||
_, err := os.Stat(cd.root)
|
|
||||||
return !os.IsNotExist(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *configDrive) AvailabilityChanges() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *configDrive) ConfigRoot() string {
|
|
||||||
return cd.root
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *configDrive) FetchMetadata() ([]byte, error) {
|
|
||||||
return cd.readFile("meta_data.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *configDrive) FetchUserdata() ([]byte, error) {
|
|
||||||
return cd.readFile("user_data")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *configDrive) Type() string {
|
|
||||||
return "cloud-drive"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *configDrive) readFile(filename string) ([]byte, error) {
|
|
||||||
data, err := ioutil.ReadFile(path.Join(cd.root, "latest", filename))
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return data, err
|
|
||||||
}
|
|
69
datasource/configdrive/configdrive.go
Normal file
69
datasource/configdrive/configdrive.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package configdrive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ec2ApiVersion = "2009-04-04"
|
||||||
|
openstackApiVersion = "latest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configDrive struct {
|
||||||
|
root string
|
||||||
|
readFile func(filename string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDatasource(root string) *configDrive {
|
||||||
|
return &configDrive{root, ioutil.ReadFile}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) IsAvailable() bool {
|
||||||
|
_, err := os.Stat(cd.root)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) AvailabilityChanges() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) ConfigRoot() string {
|
||||||
|
return cd.openstackRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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", openstackApiVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *configDrive) tryReadFile(filename string) ([]byte, error) {
|
||||||
|
data, err := cd.readFile(filename)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
135
datasource/configdrive/configdrive_test.go
Normal file
135
datasource/configdrive/configdrive_test.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package configdrive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockFilesystem []string
|
||||||
|
|
||||||
|
func (m mockFilesystem) readFile(filename string) ([]byte, error) {
|
||||||
|
for _, file := range m {
|
||||||
|
if file == filename {
|
||||||
|
return []byte(filename), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDatasource(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
root string
|
||||||
|
expectRoot string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
root: "",
|
||||||
|
expectRoot: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: "/media/configdrive",
|
||||||
|
expectRoot: "/media/configdrive",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
service := NewDatasource(tt.root)
|
||||||
|
if service.root != tt.expectRoot {
|
||||||
|
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,10 @@
|
|||||||
package datasource
|
package datasource
|
||||||
|
|
||||||
|
const (
|
||||||
|
Ec2ApiVersion = "2009-04-04"
|
||||||
|
OpenstackApiVersion = "2012-08-10"
|
||||||
|
)
|
||||||
|
|
||||||
type Datasource interface {
|
type Datasource interface {
|
||||||
IsAvailable() bool
|
IsAvailable() bool
|
||||||
AvailabilityChanges() bool
|
AvailabilityChanges() bool
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package datasource
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -9,7 +9,7 @@ type localFile struct {
|
|||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLocalFile(path string) *localFile {
|
func NewDatasource(path string) *localFile {
|
||||||
return &localFile{path}
|
return &localFile{path}
|
||||||
}
|
}
|
||||||
|
|
141
datasource/metadata/ec2/metadata.go
Normal file
141
datasource/metadata/ec2/metadata.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package ec2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultAddress = "http://169.254.169.254/"
|
||||||
|
apiVersion = "2009-04-04"
|
||||||
|
userdataUrl = apiVersion + "/user-data"
|
||||||
|
metadataUrl = apiVersion + "/meta-data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type metadataService struct {
|
||||||
|
root string
|
||||||
|
client pkg.Getter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDatasource(root string) *metadataService {
|
||||||
|
if !strings.HasSuffix(root, "/") {
|
||||||
|
root += "/"
|
||||||
|
}
|
||||||
|
return &metadataService{root, pkg.NewHttpClient()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms metadataService) IsAvailable() bool {
|
||||||
|
_, err := ms.client.Get(ms.root + apiVersion)
|
||||||
|
return (err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms metadataService) AvailabilityChanges() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms metadataService) ConfigRoot() string {
|
||||||
|
return ms.root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms metadataService) FetchMetadata() ([]byte, error) {
|
||||||
|
attrs := make(map[string]interface{})
|
||||||
|
if keynames, err := fetchAttributes(ms.client, fmt.Sprintf("%s/public-keys", ms.metadataUrl())); err == nil {
|
||||||
|
keyIDs := make(map[string]string)
|
||||||
|
for _, keyname := range keynames {
|
||||||
|
tokens := strings.SplitN(keyname, "=", 2)
|
||||||
|
if len(tokens) != 2 {
|
||||||
|
return nil, fmt.Errorf("malformed public key: %q", keyname)
|
||||||
|
}
|
||||||
|
keyIDs[tokens[1]] = tokens[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make(map[string]string)
|
||||||
|
for name, id := range keyIDs {
|
||||||
|
sshkey, err := fetchAttribute(ms.client, fmt.Sprintf("%s/public-keys/%s/openssh-key", ms.metadataUrl(), id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keys[name] = sshkey
|
||||||
|
fmt.Printf("Found SSH key for %q\n", name)
|
||||||
|
}
|
||||||
|
attrs["public_keys"] = keys
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostname, err := fetchAttribute(ms.client, fmt.Sprintf("%s/hostname", ms.metadataUrl())); err == nil {
|
||||||
|
attrs["hostname"] = hostname
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localAddr, err := fetchAttribute(ms.client, fmt.Sprintf("%s/local-ipv4", ms.metadataUrl())); err == nil {
|
||||||
|
attrs["local-ipv4"] = localAddr
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if publicAddr, err := fetchAttribute(ms.client, fmt.Sprintf("%s/public-ipv4", ms.metadataUrl())); err == nil {
|
||||||
|
attrs["public-ipv4"] = publicAddr
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if content_path, err := fetchAttribute(ms.client, fmt.Sprintf("%s/network_config/content_path", ms.metadataUrl())); err == nil {
|
||||||
|
attrs["network_config"] = map[string]string{
|
||||||
|
"content_path": content_path,
|
||||||
|
}
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms metadataService) FetchUserdata() ([]byte, error) {
|
||||||
|
if data, err := ms.client.GetRetry(ms.userdataUrl()); err == nil {
|
||||||
|
return data, err
|
||||||
|
} else if _, ok := err.(pkg.ErrNotFound); ok {
|
||||||
|
return []byte{}, nil
|
||||||
|
} else {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms metadataService) Type() string {
|
||||||
|
return "ec2-metadata-service"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms metadataService) metadataUrl() string {
|
||||||
|
return (ms.root + metadataUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms metadataService) userdataUrl() string {
|
||||||
|
return (ms.root + userdataUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAttributes(client pkg.Getter, url string) ([]string, error) {
|
||||||
|
resp, err := client.GetRetry(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(bytes.NewBuffer(resp))
|
||||||
|
data := make([]string, 0)
|
||||||
|
for scanner.Scan() {
|
||||||
|
data = append(data, scanner.Text())
|
||||||
|
}
|
||||||
|
return data, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAttribute(client pkg.Getter, url string) (string, error) {
|
||||||
|
if attrs, err := fetchAttributes(client, url); err == nil && len(attrs) > 0 {
|
||||||
|
return attrs[0], nil
|
||||||
|
} else {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
324
datasource/metadata/ec2/metadata_test.go
Normal file
324
datasource/metadata/ec2/metadata_test.go
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
package ec2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testHttpClient struct {
|
||||||
|
resources map[string]string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testHttpClient) GetRetry(url string) ([]byte, error) {
|
||||||
|
if t.err != nil {
|
||||||
|
return nil, t.err
|
||||||
|
}
|
||||||
|
if val, ok := t.resources[url]; ok {
|
||||||
|
return []byte(val), nil
|
||||||
|
} else {
|
||||||
|
return nil, pkg.ErrNotFound{fmt.Errorf("not found: %q", url)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testHttpClient) Get(url string) ([]byte, error) {
|
||||||
|
return t.GetRetry(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAvailabilityChanges(t *testing.T) {
|
||||||
|
want := true
|
||||||
|
if ac := (metadataService{}).AvailabilityChanges(); ac != want {
|
||||||
|
t.Fatalf("bad AvailabilityChanges: want %q, got %q", want, ac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestType(t *testing.T) {
|
||||||
|
want := "ec2-metadata-service"
|
||||||
|
if kind := (metadataService{}).Type(); kind != want {
|
||||||
|
t.Fatalf("bad type: want %q, got %q", want, kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAvailable(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
root string
|
||||||
|
resources map[string]string
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
root: "/",
|
||||||
|
resources: map[string]string{
|
||||||
|
"/2009-04-04": "",
|
||||||
|
},
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: "/",
|
||||||
|
resources: map[string]string{},
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
service := &metadataService{tt.root, &testHttpClient{tt.resources, nil}}
|
||||||
|
if a := service.IsAvailable(); a != tt.expect {
|
||||||
|
t.Fatalf("bad isAvailable (%q): want %q, got %q", tt.resources, tt.expect, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchUserdata(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
root string
|
||||||
|
resources map[string]string
|
||||||
|
userdata []byte
|
||||||
|
clientErr error
|
||||||
|
expectErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
root: "/",
|
||||||
|
resources: map[string]string{
|
||||||
|
"/2009-04-04/user-data": "hello",
|
||||||
|
},
|
||||||
|
userdata: []byte("hello"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: "/",
|
||||||
|
clientErr: pkg.ErrNotFound{fmt.Errorf("test not found error")},
|
||||||
|
userdata: []byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: "/",
|
||||||
|
clientErr: pkg.ErrTimeout{fmt.Errorf("test timeout error")},
|
||||||
|
expectErr: pkg.ErrTimeout{fmt.Errorf("test timeout error")},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
service := &metadataService{tt.root, &testHttpClient{tt.resources, tt.clientErr}}
|
||||||
|
data, err := service.FetchUserdata()
|
||||||
|
if Error(err) != Error(tt.expectErr) {
|
||||||
|
t.Fatalf("bad error (%q): want %q, got %q", tt.resources, tt.expectErr, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, tt.userdata) {
|
||||||
|
t.Fatalf("bad userdata (%q): want %q, got %q", tt.resources, tt.userdata, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUrls(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
root string
|
||||||
|
expectRoot string
|
||||||
|
userdata string
|
||||||
|
metadata string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
root: "/",
|
||||||
|
expectRoot: "/",
|
||||||
|
userdata: "/2009-04-04/user-data",
|
||||||
|
metadata: "/2009-04-04/meta-data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: "http://169.254.169.254/",
|
||||||
|
expectRoot: "http://169.254.169.254/",
|
||||||
|
userdata: "http://169.254.169.254/2009-04-04/user-data",
|
||||||
|
metadata: "http://169.254.169.254/2009-04-04/meta-data",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
service := &metadataService{tt.root, nil}
|
||||||
|
if url := service.userdataUrl(); url != tt.userdata {
|
||||||
|
t.Fatalf("bad url (%q): want %q, got %q", tt.root, tt.userdata, url)
|
||||||
|
}
|
||||||
|
if url := service.metadataUrl(); url != tt.metadata {
|
||||||
|
t.Fatalf("bad url (%q): want %q, got %q", tt.root, tt.metadata, url)
|
||||||
|
}
|
||||||
|
if url := service.ConfigRoot(); url != tt.expectRoot {
|
||||||
|
t.Fatalf("bad url (%q): want %q, got %q", tt.root, tt.expectRoot, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchAttributes(t *testing.T) {
|
||||||
|
for _, s := range []struct {
|
||||||
|
resources map[string]string
|
||||||
|
err error
|
||||||
|
tests []struct {
|
||||||
|
path string
|
||||||
|
val []string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
resources: map[string]string{
|
||||||
|
"/": "a\nb\nc/",
|
||||||
|
"/c/": "d\ne/",
|
||||||
|
"/c/e/": "f",
|
||||||
|
"/a": "1",
|
||||||
|
"/b": "2",
|
||||||
|
"/c/d": "3",
|
||||||
|
"/c/e/f": "4",
|
||||||
|
},
|
||||||
|
tests: []struct {
|
||||||
|
path string
|
||||||
|
val []string
|
||||||
|
}{
|
||||||
|
{"/", []string{"a", "b", "c/"}},
|
||||||
|
{"/b", []string{"2"}},
|
||||||
|
{"/c/d", []string{"3"}},
|
||||||
|
{"/c/e/", []string{"f"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: pkg.ErrNotFound{fmt.Errorf("test error")},
|
||||||
|
tests: []struct {
|
||||||
|
path string
|
||||||
|
val []string
|
||||||
|
}{
|
||||||
|
{"", nil},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
client := &testHttpClient{s.resources, s.err}
|
||||||
|
for _, tt := range s.tests {
|
||||||
|
attrs, err := fetchAttributes(client, tt.path)
|
||||||
|
if err != s.err {
|
||||||
|
t.Fatalf("bad error for %q (%q): want %q, got %q", tt.path, s.resources, s.err, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(attrs, tt.val) {
|
||||||
|
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.resources, tt.val, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchAttribute(t *testing.T) {
|
||||||
|
for _, s := range []struct {
|
||||||
|
resources map[string]string
|
||||||
|
err error
|
||||||
|
tests []struct {
|
||||||
|
path string
|
||||||
|
val string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
resources: map[string]string{
|
||||||
|
"/": "a\nb\nc/",
|
||||||
|
"/c/": "d\ne/",
|
||||||
|
"/c/e/": "f",
|
||||||
|
"/a": "1",
|
||||||
|
"/b": "2",
|
||||||
|
"/c/d": "3",
|
||||||
|
"/c/e/f": "4",
|
||||||
|
},
|
||||||
|
tests: []struct {
|
||||||
|
path string
|
||||||
|
val string
|
||||||
|
}{
|
||||||
|
{"/a", "1"},
|
||||||
|
{"/b", "2"},
|
||||||
|
{"/c/d", "3"},
|
||||||
|
{"/c/e/f", "4"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: pkg.ErrNotFound{fmt.Errorf("test error")},
|
||||||
|
tests: []struct {
|
||||||
|
path string
|
||||||
|
val string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
client := &testHttpClient{s.resources, s.err}
|
||||||
|
for _, tt := range s.tests {
|
||||||
|
attr, err := fetchAttribute(client, tt.path)
|
||||||
|
if err != s.err {
|
||||||
|
t.Fatalf("bad error for %q (%q): want %q, got %q", tt.path, s.resources, s.err, err)
|
||||||
|
}
|
||||||
|
if attr != tt.val {
|
||||||
|
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.resources, tt.val, attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchMetadata(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
root string
|
||||||
|
resources map[string]string
|
||||||
|
expect []byte
|
||||||
|
clientErr error
|
||||||
|
expectErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
root: "/",
|
||||||
|
resources: map[string]string{
|
||||||
|
"/2009-04-04/meta-data/public-keys": "bad\n",
|
||||||
|
},
|
||||||
|
expectErr: fmt.Errorf("malformed public key: \"bad\""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: "/",
|
||||||
|
resources: map[string]string{
|
||||||
|
"/2009-04-04/meta-data/hostname": "host",
|
||||||
|
"/2009-04-04/meta-data/local-ipv4": "1.2.3.4",
|
||||||
|
"/2009-04-04/meta-data/public-ipv4": "5.6.7.8",
|
||||||
|
"/2009-04-04/meta-data/public-keys": "0=test1\n",
|
||||||
|
"/2009-04-04/meta-data/public-keys/0": "openssh-key",
|
||||||
|
"/2009-04-04/meta-data/public-keys/0/openssh-key": "key",
|
||||||
|
"/2009-04-04/meta-data/network_config/content_path": "path",
|
||||||
|
},
|
||||||
|
expect: []byte(`{"hostname":"host","local-ipv4":"1.2.3.4","network_config":{"content_path":"path"},"public-ipv4":"5.6.7.8","public_keys":{"test1":"key"}}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clientErr: pkg.ErrTimeout{fmt.Errorf("test error")},
|
||||||
|
expectErr: pkg.ErrTimeout{fmt.Errorf("test error")},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
service := &metadataService{tt.root, &testHttpClient{tt.resources, tt.clientErr}}
|
||||||
|
metadata, err := service.FetchMetadata()
|
||||||
|
if Error(err) != Error(tt.expectErr) {
|
||||||
|
t.Fatalf("bad error (%q): want %q, got %q", tt.resources, tt.expectErr, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(metadata, tt.expect) {
|
||||||
|
t.Fatalf("bad fetch (%q): want %q, got %q", tt.resources, tt.expect, metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDatasource(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
root string
|
||||||
|
expectRoot string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
root: "",
|
||||||
|
expectRoot: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: "/",
|
||||||
|
expectRoot: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: "http://169.254.169.254",
|
||||||
|
expectRoot: "http://169.254.169.254/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: "http://169.254.169.254/",
|
||||||
|
expectRoot: "http://169.254.169.254/",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
service := NewDatasource(tt.root)
|
||||||
|
if service.root != tt.expectRoot {
|
||||||
|
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(err error) string {
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
@@ -1,143 +0,0 @@
|
|||||||
package datasource
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/pkg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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/"
|
|
||||||
Ec2ApiVersion = "2009-04-04"
|
|
||||||
Ec2UserdataUrl = BaseUrl + Ec2ApiVersion + "/user-data"
|
|
||||||
Ec2MetadataUrl = BaseUrl + Ec2ApiVersion + "/meta-data"
|
|
||||||
OpenstackApiVersion = "openstack/2012-08-10"
|
|
||||||
OpenstackUserdataUrl = BaseUrl + OpenstackApiVersion + "/user_data"
|
|
||||||
OpenstackMetadataUrl = BaseUrl + OpenstackApiVersion + "/meta_data.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
type metadataService struct{}
|
|
||||||
|
|
||||||
type getter interface {
|
|
||||||
GetRetry(string) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMetadataService() *metadataService {
|
|
||||||
return &metadataService{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *metadataService) IsAvailable() bool {
|
|
||||||
client := pkg.NewHttpClient()
|
|
||||||
_, 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
|
|
||||||
}
|
|
||||||
return client.GetRetry(OpenstackUserdataUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *metadataService) Type() string {
|
|
||||||
return "metadata-service"
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchMetadata(client getter) ([]byte, error) {
|
|
||||||
if metadata, err := client.GetRetry(OpenstackMetadataUrl); err == nil {
|
|
||||||
return metadata, nil
|
|
||||||
} else if _, ok := err.(pkg.ErrTimeout); ok {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,165 +0,0 @@
|
|||||||
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 TestFetchAttributes(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 TestFetchAttribute(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 TestFetchMetadata(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"}}`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metadata: map[string]string{
|
|
||||||
"http://169.254.169.254/openstack/2012-08-10/meta_data.json": "test",
|
|
||||||
},
|
|
||||||
expect: []byte("test"),
|
|
||||||
},
|
|
||||||
{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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,4 +1,4 @@
|
|||||||
package datasource
|
package proc_cmdline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -18,7 +18,7 @@ type procCmdline struct {
|
|||||||
Location string
|
Location string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProcCmdline() *procCmdline {
|
func NewDatasource() *procCmdline {
|
||||||
return &procCmdline{Location: ProcCmdlineLocation}
|
return &procCmdline{Location: ProcCmdlineLocation}
|
||||||
}
|
}
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package datasource
|
package proc_cmdline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -75,7 +75,7 @@ func TestProcCmdlineAndFetchConfig(t *testing.T) {
|
|||||||
t.Errorf("Test produced error: %v", err)
|
t.Errorf("Test produced error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := NewProcCmdline()
|
p := NewDatasource()
|
||||||
p.Location = file.Name()
|
p.Location = file.Name()
|
||||||
cfg, err := p.FetchUserdata()
|
cfg, err := p.FetchUserdata()
|
||||||
if err != nil {
|
if err != nil {
|
@@ -1,4 +1,4 @@
|
|||||||
package datasource
|
package url
|
||||||
|
|
||||||
import "github.com/coreos/coreos-cloudinit/pkg"
|
import "github.com/coreos/coreos-cloudinit/pkg"
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ type remoteFile struct {
|
|||||||
url string
|
url string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRemoteFile(url string) *remoteFile {
|
func NewDatasource(url string) *remoteFile {
|
||||||
return &remoteFile{url}
|
return &remoteFile{url}
|
||||||
}
|
}
|
||||||
|
|
@@ -66,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,12 +234,27 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
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 {
|
||||||
path, err := system.WriteFile(&file, env.Root())
|
fullPath, err := system.WriteFile(&file, env.Root())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Wrote file %s to filesystem", path)
|
if path.Clean(file.Path) == "/etc/environment" {
|
||||||
|
wroteEnvironment = true
|
||||||
|
}
|
||||||
|
log.Printf("Wrote file %s to filesystem", fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wroteEnvironment {
|
||||||
|
ef := env.DefaultEnvironmentFile()
|
||||||
|
if ef != nil {
|
||||||
|
err := system.WriteEnvFile(ef, env.Root())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Updated /etc/environment")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if env.NetconfType() != "" {
|
if env.NetconfType() != "" {
|
||||||
@@ -257,13 +283,23 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
um := system.NewUnitManager(env.Root())
|
||||||
|
return processUnits(cfg.Coreos.Units, env.Root(), um)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// processUnits takes a set of Units and applies them to the given root using
|
||||||
|
// the given UnitManager. This can involve things like writing unit files to
|
||||||
|
// disk, masking/unmasking units, or invoking systemd
|
||||||
|
// commands against units. It returns any error encountered.
|
||||||
|
func processUnits(units []system.Unit, root string, um system.UnitManager) error {
|
||||||
commands := make(map[string]string, 0)
|
commands := make(map[string]string, 0)
|
||||||
reload := false
|
reload := false
|
||||||
for _, unit := range cfg.Coreos.Units {
|
for _, unit := range units {
|
||||||
dst := unit.Destination(env.Root())
|
dst := unit.Destination(root)
|
||||||
if unit.Content != "" {
|
if unit.Content != "" {
|
||||||
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
|
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
|
||||||
if err := system.PlaceUnit(&unit, dst); err != nil {
|
if err := um.PlaceUnit(&unit, dst); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Placed unit %s at %s", unit.Name, dst)
|
log.Printf("Placed unit %s at %s", unit.Name, dst)
|
||||||
@@ -272,12 +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, env.Root()); err != nil {
|
if err := um.MaskUnit(&unit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if unit.Runtime {
|
} else if unit.Runtime {
|
||||||
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
|
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
|
||||||
if err := system.UnmaskUnit(&unit, env.Root()); err != nil {
|
if err := um.UnmaskUnit(&unit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,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)
|
||||||
@@ -302,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,6 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultSSHKeyName = "coreos-cloudinit"
|
const DefaultSSHKeyName = "coreos-cloudinit"
|
||||||
@@ -65,6 +67,26 @@ func (e *Environment) Apply(data string) string {
|
|||||||
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,8 +1,12 @@
|
|||||||
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) {
|
||||||
@@ -56,3 +60,47 @@ ExecStop=/usr/bin/echo $unknown`,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnvironmentFile(t *testing.T) {
|
||||||
|
subs := map[string]string{
|
||||||
|
"$public_ipv4": "1.2.3.4",
|
||||||
|
"$private_ipv4": "5.6.7.8",
|
||||||
|
}
|
||||||
|
expect := "COREOS_PRIVATE_IPV4=5.6.7.8\nCOREOS_PUBLIC_IPV4=1.2.3.4\n"
|
||||||
|
|
||||||
|
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create tempdir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
env := NewEnvironment("./", "./", "./", "", "", subs)
|
||||||
|
ef := env.DefaultEnvironmentFile()
|
||||||
|
err = system.WriteEnvFile(ef, dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteEnvFile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := path.Join(dir, "etc", "environment")
|
||||||
|
contents, err := ioutil.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to read expected file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(contents) != expect {
|
||||||
|
t.Fatalf("File has incorrect contents: %q", contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvironmentFileNil(t *testing.T) {
|
||||||
|
subs := map[string]string{
|
||||||
|
"$public_ipv4": "",
|
||||||
|
"$private_ipv4": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
env := NewEnvironment("./", "./", "./", "", "", subs)
|
||||||
|
ef := env.DefaultEnvironmentFile()
|
||||||
|
if ef != nil {
|
||||||
|
t.Fatalf("Environment file not nil: %v", ef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -39,7 +39,7 @@ func (ee EtcdEnvironment) String() (out string) {
|
|||||||
// Units 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) Units(root string) ([]system.Unit, error) {
|
func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
|
||||||
if ee == nil {
|
if len(ee) < 1 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -70,6 +70,8 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
sd := system.NewUnitManager(dir)
|
||||||
|
|
||||||
uu, err := ee.Units(dir)
|
uu, err := ee.Units(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Generating etcd unit failed: %v", err)
|
t.Fatalf("Generating etcd unit failed: %v", err)
|
||||||
@@ -81,7 +83,7 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
|||||||
|
|
||||||
dst := u.Destination(dir)
|
dst := u.Destination(dir)
|
||||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
os.Stderr.WriteString("writing to " + dir + "\n")
|
||||||
if err := system.PlaceUnit(&u, dst); err != nil {
|
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,14 +113,27 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
|
func TestEtcdEnvironmentEmptyNoOp(t *testing.T) {
|
||||||
ee := EtcdEnvironment{}
|
ee := EtcdEnvironment{}
|
||||||
|
uu, err := ee.Units("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(uu) > 0 {
|
||||||
|
t.Fatalf("Generated etcd units unexpectedly: %v")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
|
||||||
|
ee := EtcdEnvironment{"foo": "bar"}
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
t.Fatalf("Unable to create tempdir: %v", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
sd := system.NewUnitManager(dir)
|
||||||
|
|
||||||
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
|
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
|
||||||
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
|
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -136,7 +151,7 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
|
|||||||
|
|
||||||
dst := u.Destination(dir)
|
dst := u.Destination(dir)
|
||||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
os.Stderr.WriteString("writing to " + dir + "\n")
|
||||||
if err := system.PlaceUnit(&u, dst); err != nil {
|
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +163,7 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect := `[Service]
|
expect := `[Service]
|
||||||
|
Environment="ETCD_FOO=bar"
|
||||||
Environment="ETCD_NAME=node007"
|
Environment="ETCD_NAME=node007"
|
||||||
`
|
`
|
||||||
if string(contents) != expect {
|
if string(contents) != expect {
|
||||||
|
@@ -3,6 +3,7 @@ package network
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InterfaceGenerator interface {
|
type InterfaceGenerator interface {
|
||||||
@@ -11,6 +12,8 @@ type InterfaceGenerator interface {
|
|||||||
Netdev() string
|
Netdev() string
|
||||||
Link() string
|
Link() string
|
||||||
Network() string
|
Network() string
|
||||||
|
Type() string
|
||||||
|
ModprobeParams() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type networkInterface interface {
|
type networkInterface interface {
|
||||||
@@ -68,6 +71,10 @@ func (i *logicalInterface) Children() []networkInterface {
|
|||||||
return i.children
|
return i.children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *logicalInterface) ModprobeParams() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (i *logicalInterface) setConfigDepth(depth int) {
|
func (i *logicalInterface) setConfigDepth(depth int) {
|
||||||
i.configDepth = depth
|
i.configDepth = depth
|
||||||
}
|
}
|
||||||
@@ -84,9 +91,14 @@ func (p *physicalInterface) Netdev() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *physicalInterface) Type() string {
|
||||||
|
return "physical"
|
||||||
|
}
|
||||||
|
|
||||||
type bondInterface struct {
|
type bondInterface struct {
|
||||||
logicalInterface
|
logicalInterface
|
||||||
slaves []string
|
slaves []string
|
||||||
|
options map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bondInterface) Name() string {
|
func (b *bondInterface) Name() string {
|
||||||
@@ -97,6 +109,19 @@ func (b *bondInterface) Netdev() string {
|
|||||||
return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name)
|
return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *bondInterface) Type() string {
|
||||||
|
return "bond"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bondInterface) ModprobeParams() string {
|
||||||
|
params := ""
|
||||||
|
for name, val := range b.options {
|
||||||
|
params += fmt.Sprintf("%s=%s ", name, val)
|
||||||
|
}
|
||||||
|
params = strings.TrimSuffix(params, " ")
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
type vlanInterface struct {
|
type vlanInterface struct {
|
||||||
logicalInterface
|
logicalInterface
|
||||||
id int
|
id int
|
||||||
@@ -123,6 +148,10 @@ func (v *vlanInterface) Netdev() string {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *vlanInterface) Type() string {
|
||||||
|
return "vlan"
|
||||||
|
}
|
||||||
|
|
||||||
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
|
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
|
||||||
interfaceMap := createInterfaces(stanzas)
|
interfaceMap := createInterfaces(stanzas)
|
||||||
linkAncestors(interfaceMap)
|
linkAncestors(interfaceMap)
|
||||||
@@ -141,15 +170,22 @@ func createInterfaces(stanzas []*stanzaInterface) map[string]networkInterface {
|
|||||||
for _, iface := range stanzas {
|
for _, iface := range stanzas {
|
||||||
switch iface.kind {
|
switch iface.kind {
|
||||||
case interfaceBond:
|
case interfaceBond:
|
||||||
|
bondOptions := make(map[string]string)
|
||||||
|
for _, k := range []string{"mode", "miimon", "lacp-rate"} {
|
||||||
|
if v, ok := iface.options["bond-"+k]; ok && len(v) > 0 {
|
||||||
|
bondOptions[k] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
interfaceMap[iface.name] = &bondInterface{
|
interfaceMap[iface.name] = &bondInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: iface.name,
|
name: iface.name,
|
||||||
config: iface.configMethod,
|
config: iface.configMethod,
|
||||||
children: []networkInterface{},
|
children: []networkInterface{},
|
||||||
},
|
},
|
||||||
iface.options["slaves"],
|
iface.options["bond-slaves"],
|
||||||
|
bondOptions,
|
||||||
}
|
}
|
||||||
for _, slave := range iface.options["slaves"] {
|
for _, slave := range iface.options["bond-slaves"] {
|
||||||
if _, ok := interfaceMap[slave]; !ok {
|
if _, ok := interfaceMap[slave]; !ok {
|
||||||
interfaceMap[slave] = &physicalInterface{
|
interfaceMap[slave] = &physicalInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
@@ -241,13 +277,17 @@ func markConfigDepths(interfaceMap map[string]networkInterface) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, iface := range rootInterfaceMap {
|
for _, iface := range rootInterfaceMap {
|
||||||
setDepth(iface, 0)
|
setDepth(iface)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDepth(iface networkInterface, depth int) {
|
func setDepth(iface networkInterface) int {
|
||||||
iface.setConfigDepth(depth)
|
maxDepth := 0
|
||||||
for _, child := range iface.Children() {
|
for _, child := range iface.Children() {
|
||||||
setDepth(child, depth+1)
|
if depth := setDepth(child); depth > maxDepth {
|
||||||
|
maxDepth = depth
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
iface.setConfigDepth(maxDepth)
|
||||||
|
return (maxDepth + 1)
|
||||||
}
|
}
|
||||||
|
@@ -36,6 +36,7 @@ func TestPhysicalInterfaceNetwork(t *testing.T) {
|
|||||||
name: "testbond1",
|
name: "testbond1",
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
},
|
},
|
||||||
&vlanInterface{
|
&vlanInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
@@ -67,14 +68,14 @@ VLAN=testvlan2
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBondInterfaceName(t *testing.T) {
|
func TestBondInterfaceName(t *testing.T) {
|
||||||
b := bondInterface{logicalInterface{name: "testname"}, nil}
|
b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
|
||||||
if b.Name() != "testname" {
|
if b.Name() != "testname" {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBondInterfaceNetdev(t *testing.T) {
|
func TestBondInterfaceNetdev(t *testing.T) {
|
||||||
b := bondInterface{logicalInterface{name: "testname"}, nil}
|
b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
|
||||||
netdev := `[NetDev]
|
netdev := `[NetDev]
|
||||||
Kind=bond
|
Kind=bond
|
||||||
Name=testname
|
Name=testname
|
||||||
@@ -102,6 +103,7 @@ func TestBondInterfaceNetwork(t *testing.T) {
|
|||||||
name: "testbond1",
|
name: "testbond1",
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
},
|
},
|
||||||
&vlanInterface{
|
&vlanInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
@@ -120,6 +122,7 @@ func TestBondInterfaceNetwork(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
}
|
}
|
||||||
network := `[Match]
|
network := `[Match]
|
||||||
Name=testname
|
Name=testname
|
||||||
@@ -218,6 +221,61 @@ Gateway=1.2.3.4
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestType(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
i InterfaceGenerator
|
||||||
|
t string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
i: &physicalInterface{},
|
||||||
|
t: "physical",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
i: &vlanInterface{},
|
||||||
|
t: "vlan",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
i: &bondInterface{},
|
||||||
|
t: "bond",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if tp := tt.i.Type(); tp != tt.t {
|
||||||
|
t.Fatalf("bad type (%q): got %s, want %s", tt.i, tp, tt.t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModprobeParams(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
i InterfaceGenerator
|
||||||
|
p string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
i: &physicalInterface{},
|
||||||
|
p: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
i: &vlanInterface{},
|
||||||
|
p: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
i: &bondInterface{
|
||||||
|
logicalInterface{},
|
||||||
|
nil,
|
||||||
|
map[string]string{
|
||||||
|
"a": "1",
|
||||||
|
"b": "2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
p: "a=1 b=2",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if p := tt.i.ModprobeParams(); p != tt.p {
|
||||||
|
t.Fatalf("bad params (%q): got %s, want %s", tt.i, p, tt.p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildInterfacesLo(t *testing.T) {
|
func TestBuildInterfacesLo(t *testing.T) {
|
||||||
stanzas := []*stanzaInterface{
|
stanzas := []*stanzaInterface{
|
||||||
&stanzaInterface{
|
&stanzaInterface{
|
||||||
@@ -242,7 +300,7 @@ func TestBuildInterfacesBlindBond(t *testing.T) {
|
|||||||
auto: false,
|
auto: false,
|
||||||
configMethod: configMethodManual{},
|
configMethod: configMethodManual{},
|
||||||
options: map[string][]string{
|
options: map[string][]string{
|
||||||
"slaves": []string{"eth0"},
|
"bond-slaves": []string{"eth0"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -252,16 +310,17 @@ func TestBuildInterfacesBlindBond(t *testing.T) {
|
|||||||
name: "bond0",
|
name: "bond0",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []networkInterface{},
|
children: []networkInterface{},
|
||||||
configDepth: 1,
|
configDepth: 0,
|
||||||
},
|
},
|
||||||
[]string{"eth0"},
|
[]string{"eth0"},
|
||||||
|
map[string]string{},
|
||||||
}
|
}
|
||||||
eth0 := &physicalInterface{
|
eth0 := &physicalInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "eth0",
|
name: "eth0",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []networkInterface{bond0},
|
children: []networkInterface{bond0},
|
||||||
configDepth: 0,
|
configDepth: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
expect := []InterfaceGenerator{bond0, eth0}
|
expect := []InterfaceGenerator{bond0, eth0}
|
||||||
@@ -289,7 +348,7 @@ func TestBuildInterfacesBlindVLAN(t *testing.T) {
|
|||||||
name: "vlan0",
|
name: "vlan0",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []networkInterface{},
|
children: []networkInterface{},
|
||||||
configDepth: 1,
|
configDepth: 0,
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
"eth0",
|
"eth0",
|
||||||
@@ -299,7 +358,7 @@ func TestBuildInterfacesBlindVLAN(t *testing.T) {
|
|||||||
name: "eth0",
|
name: "eth0",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []networkInterface{vlan0},
|
children: []networkInterface{vlan0},
|
||||||
configDepth: 0,
|
configDepth: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
expect := []InterfaceGenerator{eth0, vlan0}
|
expect := []InterfaceGenerator{eth0, vlan0}
|
||||||
@@ -323,7 +382,9 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
auto: false,
|
auto: false,
|
||||||
configMethod: configMethodManual{},
|
configMethod: configMethodManual{},
|
||||||
options: map[string][]string{
|
options: map[string][]string{
|
||||||
"slaves": []string{"eth0"},
|
"bond-slaves": []string{"eth0"},
|
||||||
|
"bond-mode": []string{"4"},
|
||||||
|
"bond-miimon": []string{"100"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&stanzaInterface{
|
&stanzaInterface{
|
||||||
@@ -332,7 +393,7 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
auto: false,
|
auto: false,
|
||||||
configMethod: configMethodManual{},
|
configMethod: configMethodManual{},
|
||||||
options: map[string][]string{
|
options: map[string][]string{
|
||||||
"slaves": []string{"bond0"},
|
"bond-slaves": []string{"bond0"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&stanzaInterface{
|
&stanzaInterface{
|
||||||
@@ -362,7 +423,7 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
name: "vlan1",
|
name: "vlan1",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []networkInterface{},
|
children: []networkInterface{},
|
||||||
configDepth: 2,
|
configDepth: 0,
|
||||||
},
|
},
|
||||||
1,
|
1,
|
||||||
"bond0",
|
"bond0",
|
||||||
@@ -372,7 +433,7 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
name: "vlan0",
|
name: "vlan0",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []networkInterface{},
|
children: []networkInterface{},
|
||||||
configDepth: 1,
|
configDepth: 0,
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
"eth0",
|
"eth0",
|
||||||
@@ -382,9 +443,10 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
name: "bond1",
|
name: "bond1",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []networkInterface{},
|
children: []networkInterface{},
|
||||||
configDepth: 2,
|
configDepth: 0,
|
||||||
},
|
},
|
||||||
[]string{"bond0"},
|
[]string{"bond0"},
|
||||||
|
map[string]string{},
|
||||||
}
|
}
|
||||||
bond0 := &bondInterface{
|
bond0 := &bondInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
@@ -394,13 +456,17 @@ func TestBuildInterfaces(t *testing.T) {
|
|||||||
configDepth: 1,
|
configDepth: 1,
|
||||||
},
|
},
|
||||||
[]string{"eth0"},
|
[]string{"eth0"},
|
||||||
|
map[string]string{
|
||||||
|
"mode": "4",
|
||||||
|
"miimon": "100",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
eth0 := &physicalInterface{
|
eth0 := &physicalInterface{
|
||||||
logicalInterface{
|
logicalInterface{
|
||||||
name: "eth0",
|
name: "eth0",
|
||||||
config: configMethodManual{},
|
config: configMethodManual{},
|
||||||
children: []networkInterface{bond0, vlan0},
|
children: []networkInterface{bond0, vlan0},
|
||||||
configDepth: 0,
|
configDepth: 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1}
|
expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1}
|
||||||
|
@@ -293,7 +293,6 @@ func parseHwaddress(options map[string][]string, iface string) (net.HardwareAddr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
|
func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
|
||||||
options["slaves"] = options["bond-slaves"]
|
|
||||||
return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
|
return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -129,7 +129,7 @@ func TestParseBondStanzaNoSlaves(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
if bond.options["slaves"] != nil {
|
if bond.options["bond-slaves"] != nil {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,9 +152,6 @@ func TestParseBondStanza(t *testing.T) {
|
|||||||
if bond.configMethod != conf {
|
if bond.configMethod != conf {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(bond.options["slaves"], options["bond-slaves"]) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsePhysicalStanza(t *testing.T) {
|
func TestParsePhysicalStanza(t *testing.T) {
|
||||||
|
@@ -57,6 +57,11 @@ type HttpClient struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Getter interface {
|
||||||
|
Get(string) ([]byte, error)
|
||||||
|
GetRetry(string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
func NewHttpClient() *HttpClient {
|
func NewHttpClient() *HttpClient {
|
||||||
hc := &HttpClient{
|
hc := &HttpClient{
|
||||||
MaxBackoff: time.Second * 5,
|
MaxBackoff: time.Second * 5,
|
||||||
|
100
system/env_file.go
Normal file
100
system/env_file.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EnvFile struct {
|
||||||
|
Vars map[string]string
|
||||||
|
// mask File.Content, it shouldn't be used.
|
||||||
|
Content interface{} `json:"-" yaml:"-"`
|
||||||
|
*File
|
||||||
|
}
|
||||||
|
|
||||||
|
// only allow sh compatible identifiers
|
||||||
|
var validKey = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
|
||||||
|
|
||||||
|
// match each line, optionally capturing valid identifiers, discarding dos line endings
|
||||||
|
var lineLexer = regexp.MustCompile(`(?m)^((?:([a-zA-Z0-9_]+)=)?.*?)\r?\n`)
|
||||||
|
|
||||||
|
// mergeEnvContents: Update the existing file contents with new values,
|
||||||
|
// preserving variable ordering and all content this code doesn't understand.
|
||||||
|
// All new values are appended to the bottom of the old, sorted by key.
|
||||||
|
func mergeEnvContents(old []byte, pending map[string]string) []byte {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var match [][]byte
|
||||||
|
|
||||||
|
// it is awkward for the regex to handle a missing newline gracefully
|
||||||
|
if len(old) != 0 && !bytes.HasSuffix(old, []byte{'\n'}) {
|
||||||
|
old = append(old, byte('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match = range lineLexer.FindAllSubmatch(old, -1) {
|
||||||
|
key := string(match[2])
|
||||||
|
if value, ok := pending[key]; ok {
|
||||||
|
fmt.Fprintf(&buf, "%s=%s\n", key, value)
|
||||||
|
delete(pending, key)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&buf, "%s\n", match[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range keys(pending) {
|
||||||
|
value := pending[key]
|
||||||
|
fmt.Fprintf(&buf, "%s=%s\n", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteEnvFile updates an existing env `KEY=value` formated file with
|
||||||
|
// new values provided in EnvFile.Vars; File.Content is ignored.
|
||||||
|
// Existing ordering and any unknown formatting such as comments are
|
||||||
|
// preserved. If no changes are required the file is untouched.
|
||||||
|
func WriteEnvFile(ef *EnvFile, root string) error {
|
||||||
|
// validate new keys, mergeEnvContents uses pending to track writes
|
||||||
|
pending := make(map[string]string, len(ef.Vars))
|
||||||
|
for key, value := range ef.Vars {
|
||||||
|
if !validKey.MatchString(key) {
|
||||||
|
return fmt.Errorf("Invalid name %q for %s", key, ef.Path)
|
||||||
|
}
|
||||||
|
pending[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pending) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
oldContent, err := ioutil.ReadFile(path.Join(root, ef.Path))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
oldContent = []byte{}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent := mergeEnvContents(oldContent, pending)
|
||||||
|
if bytes.Equal(oldContent, newContent) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ef.File.Content = string(newContent)
|
||||||
|
_, err = WriteFile(ef.File, root)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// keys returns the keys of a map in sorted order
|
||||||
|
func keys(m map[string]string) (s []string) {
|
||||||
|
for k, _ := range m {
|
||||||
|
s = append(s, k)
|
||||||
|
}
|
||||||
|
sort.Strings(s)
|
||||||
|
return
|
||||||
|
}
|
426
system/env_file_test.go
Normal file
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -2,10 +2,11 @@ package system
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/network"
|
"github.com/coreos/coreos-cloudinit/network"
|
||||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
|
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
|
||||||
@@ -17,6 +18,13 @@ const (
|
|||||||
|
|
||||||
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
if e := restartNetworkd(); e != nil {
|
||||||
|
err = e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO(crawford): Get rid of this once networkd fixes the race
|
||||||
|
// https://bugs.freedesktop.org/show_bug.cgi?id=76077
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
if e := restartNetworkd(); e != nil {
|
if e := restartNetworkd(); e != nil {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
@@ -26,19 +34,18 @@ func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = probe8012q(); err != nil {
|
if err = maybeProbe8012q(interfaces); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return maybeProbeBonding(interfaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
||||||
sysInterfaceMap := make(map[string]*net.Interface)
|
sysInterfaceMap := make(map[string]*net.Interface)
|
||||||
if systemInterfaces, err := net.Interfaces(); err == nil {
|
if systemInterfaces, err := net.Interfaces(); err == nil {
|
||||||
for _, iface := range systemInterfaces {
|
for _, iface := range systemInterfaces {
|
||||||
// Need a copy of the interface so we can take the address
|
iface := iface
|
||||||
temp := iface
|
sysInterfaceMap[iface.Name] = &iface
|
||||||
sysInterfaceMap[temp.Name] = &temp
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return err
|
return err
|
||||||
@@ -46,6 +53,7 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
|||||||
|
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok {
|
if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok {
|
||||||
|
log.Printf("Taking down interface %q\n", systemInterface.Name)
|
||||||
if err := netlink.NetworkLinkDown(systemInterface); err != nil {
|
if err := netlink.NetworkLinkDown(systemInterface); err != nil {
|
||||||
fmt.Printf("Error while downing interface %q (%s). Continuing...\n", systemInterface.Name, err)
|
fmt.Printf("Error while downing interface %q (%s). Continuing...\n", systemInterface.Name, err)
|
||||||
}
|
}
|
||||||
@@ -55,26 +63,45 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func probe8012q() error {
|
func maybeProbe8012q(interfaces []network.InterfaceGenerator) error {
|
||||||
return exec.Command("modprobe", "8021q").Run()
|
for _, iface := range interfaces {
|
||||||
|
if iface.Type() == "vlan" {
|
||||||
|
log.Printf("Probing LKM %q (%q)\n", "8021q", "8021q")
|
||||||
|
return exec.Command("modprobe", "8021q").Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeProbeBonding(interfaces []network.InterfaceGenerator) error {
|
||||||
|
args := []string{"bonding"}
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
if iface.Type() == "bond" {
|
||||||
|
args = append(args, strings.Split(iface.ModprobeParams(), " ")...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("Probing LKM %q (%q)\n", "bonding", args)
|
||||||
|
return exec.Command("modprobe", args...).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartNetworkd() error {
|
func restartNetworkd() error {
|
||||||
_, err := RunUnitCommand("restart", "systemd-networkd.service")
|
log.Printf("Restarting networkd.service\n")
|
||||||
|
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
|
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
filename := path.Join(runtimeNetworkPath, fmt.Sprintf("%s.netdev", iface.Filename()))
|
filename := fmt.Sprintf("%s.netdev", iface.Filename())
|
||||||
if err := writeConfig(filename, iface.Netdev()); err != nil {
|
if err := writeConfig(filename, iface.Netdev()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.link", iface.Filename()))
|
filename = fmt.Sprintf("%s.link", iface.Filename())
|
||||||
if err := writeConfig(filename, iface.Link()); err != nil {
|
if err := writeConfig(filename, iface.Link()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.network", iface.Filename()))
|
filename = fmt.Sprintf("%s.network", iface.Filename())
|
||||||
if err := writeConfig(filename, iface.Network()); err != nil {
|
if err := writeConfig(filename, iface.Network()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -86,6 +113,7 @@ func writeConfig(filename string, config string) error {
|
|||||||
if config == "" {
|
if config == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
log.Printf("Writing networkd unit %q\n", filename)
|
||||||
return ioutil.WriteFile(filename, []byte(config), 0444)
|
_, err := WriteFile(&File{Content: config, Path: filename}, runtimeNetworkPath)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -13,63 +13,21 @@ import (
|
|||||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
|
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func NewUnitManager(root string) UnitManager {
|
||||||
|
return &systemd{root}
|
||||||
|
}
|
||||||
|
|
||||||
|
type systemd struct {
|
||||||
|
root string
|
||||||
|
}
|
||||||
|
|
||||||
// fakeMachineID is placed on non-usr CoreOS images and should
|
// fakeMachineID is placed on non-usr CoreOS images and should
|
||||||
// never be used as a true MachineID
|
// never be used as a true MachineID
|
||||||
const fakeMachineID = "42000000000000000000000000000042"
|
const fakeMachineID = "42000000000000000000000000000042"
|
||||||
|
|
||||||
// Name for drop-in service configuration files created by cloudconfig
|
|
||||||
const cloudConfigDropIn = "20-cloudinit.conf"
|
|
||||||
|
|
||||||
type Unit struct {
|
|
||||||
Name string
|
|
||||||
Mask bool
|
|
||||||
Enable bool
|
|
||||||
Runtime bool
|
|
||||||
Content string
|
|
||||||
Command string
|
|
||||||
|
|
||||||
// For drop-in units, a cloudinit.conf is generated.
|
|
||||||
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
|
|
||||||
// until the correct behaviour for multiple drop-in units is determined.
|
|
||||||
DropIn bool `yaml:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unit) Type() string {
|
|
||||||
ext := filepath.Ext(u.Name)
|
|
||||||
return strings.TrimLeft(ext, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unit) Group() (group string) {
|
|
||||||
t := u.Type()
|
|
||||||
if t == "network" || t == "netdev" || t == "link" {
|
|
||||||
group = "network"
|
|
||||||
} else {
|
|
||||||
group = "system"
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type Script []byte
|
|
||||||
|
|
||||||
// Destination builds the appropriate absolute file path for
|
|
||||||
// the Unit. The root argument indicates the effective base
|
|
||||||
// directory of the system (similar to a chroot).
|
|
||||||
func (u *Unit) Destination(root string) string {
|
|
||||||
dir := "etc"
|
|
||||||
if u.Runtime {
|
|
||||||
dir = "run"
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.DropIn {
|
|
||||||
return path.Join(root, dir, "systemd", u.Group(), fmt.Sprintf("%s.d", u.Name), cloudConfigDropIn)
|
|
||||||
} else {
|
|
||||||
return path.Join(root, dir, "systemd", u.Group(), u.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaceUnit writes a unit file at the provided destination, creating
|
// PlaceUnit writes a unit file at the provided destination, creating
|
||||||
// parent directories as necessary.
|
// parent directories as necessary.
|
||||||
func PlaceUnit(u *Unit, dst string) error {
|
func (s *systemd) PlaceUnit(u *Unit, dst string) error {
|
||||||
dir := filepath.Dir(dst)
|
dir := filepath.Dir(dst)
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
|
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
|
||||||
@@ -91,7 +49,7 @@ func PlaceUnit(u *Unit, dst string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnableUnitFile(unit string, runtime bool) error {
|
func (s *systemd) EnableUnitFile(unit string, runtime bool) error {
|
||||||
conn, err := dbus.New()
|
conn, err := dbus.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -102,7 +60,7 @@ func EnableUnitFile(unit string, runtime bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunUnitCommand(command, unit string) (string, error) {
|
func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
|
||||||
conn, err := dbus.New()
|
conn, err := dbus.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -131,7 +89,7 @@ func RunUnitCommand(command, unit string) (string, error) {
|
|||||||
return fn(unit, "replace")
|
return fn(unit, "replace")
|
||||||
}
|
}
|
||||||
|
|
||||||
func DaemonReload() error {
|
func (s *systemd) DaemonReload() error {
|
||||||
conn, err := dbus.New()
|
conn, err := dbus.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -140,6 +98,57 @@ func DaemonReload() error {
|
|||||||
return conn.Reload()
|
return conn.Reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaskUnit masks the given Unit by symlinking its unit file to
|
||||||
|
// /dev/null, analogous to `systemctl mask`.
|
||||||
|
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
||||||
|
// file at the location*, to ensure that the mask will succeed.
|
||||||
|
func (s *systemd) MaskUnit(unit *Unit) error {
|
||||||
|
masked := unit.Destination(s.root)
|
||||||
|
if _, err := os.Stat(masked); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err := os.Remove(masked); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Symlink("/dev/null", masked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
|
||||||
|
// associated with the given Unit is empty or appears to be a symlink to
|
||||||
|
// /dev/null, it is removed.
|
||||||
|
func (s *systemd) UnmaskUnit(unit *Unit) error {
|
||||||
|
masked := unit.Destination(s.root)
|
||||||
|
ne, err := nullOrEmpty(masked)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ne {
|
||||||
|
log.Printf("%s is not null or empty, refusing to unmask", masked)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.Remove(masked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nullOrEmpty checks whether a given path appears to be an empty regular file
|
||||||
|
// or a symlink to /dev/null
|
||||||
|
func nullOrEmpty(path string) (bool, error) {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
m := fi.Mode()
|
||||||
|
if m.IsRegular() && fi.Size() <= 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if m&os.ModeCharDevice > 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ExecuteScript(scriptPath string) (string, error) {
|
func ExecuteScript(scriptPath string) (string, error) {
|
||||||
props := []dbus.Property{
|
props := []dbus.Property{
|
||||||
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
|
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
|
||||||
@@ -178,54 +187,3 @@ func MachineID(root string) string {
|
|||||||
|
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaskUnit masks the given Unit by symlinking its unit file to
|
|
||||||
// /dev/null, analogous to `systemctl mask`.
|
|
||||||
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
|
||||||
// file at the location*, to ensure that the mask will succeed.
|
|
||||||
func MaskUnit(unit *Unit, root string) error {
|
|
||||||
masked := unit.Destination(root)
|
|
||||||
if _, err := os.Stat(masked); os.IsNotExist(err) {
|
|
||||||
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if err := os.Remove(masked); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.Symlink("/dev/null", masked)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
|
|
||||||
// associated with the given Unit is empty or appears to be a symlink to
|
|
||||||
// /dev/null, it is removed.
|
|
||||||
func UnmaskUnit(unit *Unit, root string) error {
|
|
||||||
masked := unit.Destination(root)
|
|
||||||
ne, err := nullOrEmpty(masked)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !ne {
|
|
||||||
log.Printf("%s is not null or empty, refusing to unmask", masked)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return os.Remove(masked)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nullOrEmpty checks whether a given path appears to be an empty regular file
|
|
||||||
// or a symlink to /dev/null
|
|
||||||
func nullOrEmpty(path string) (bool, error) {
|
|
||||||
fi, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
m := fi.Mode()
|
|
||||||
if m.IsRegular() && fi.Size() <= 0 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
if m&os.ModeCharDevice > 0 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
@@ -25,13 +25,15 @@ Address=10.209.171.177/19
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
sd := &systemd{dir}
|
||||||
|
|
||||||
dst := u.Destination(dir)
|
dst := u.Destination(dir)
|
||||||
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
||||||
if dst != expectDst {
|
if dst != expectDst {
|
||||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := PlaceUnit(&u, dst); err != nil {
|
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||||
t.Fatalf("PlaceUnit failed: %v", err)
|
t.Fatalf("PlaceUnit failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,13 +102,15 @@ Where=/media/state
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
sd := &systemd{dir}
|
||||||
|
|
||||||
dst := u.Destination(dir)
|
dst := u.Destination(dir)
|
||||||
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
||||||
if dst != expectDst {
|
if dst != expectDst {
|
||||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := PlaceUnit(&u, dst); err != nil {
|
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||||
t.Fatalf("PlaceUnit failed: %v", err)
|
t.Fatalf("PlaceUnit failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +159,11 @@ func TestMaskUnit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
sd := &systemd{dir}
|
||||||
|
|
||||||
// Ensure mask works with units that do not currently exist
|
// Ensure mask works with units that do not currently exist
|
||||||
uf := &Unit{Name: "foo.service"}
|
uf := &Unit{Name: "foo.service"}
|
||||||
if err := MaskUnit(uf, dir); err != nil {
|
if err := sd.MaskUnit(uf); err != nil {
|
||||||
t.Fatalf("Unable to mask new unit: %v", err)
|
t.Fatalf("Unable to mask new unit: %v", err)
|
||||||
}
|
}
|
||||||
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
|
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
|
||||||
@@ -175,7 +181,7 @@ func TestMaskUnit(t *testing.T) {
|
|||||||
if _, err := os.Create(barPath); err != nil {
|
if _, err := os.Create(barPath); err != nil {
|
||||||
t.Fatalf("Error creating new unit file: %v", err)
|
t.Fatalf("Error creating new unit file: %v", err)
|
||||||
}
|
}
|
||||||
if err := MaskUnit(ub, dir); err != nil {
|
if err := sd.MaskUnit(ub); err != nil {
|
||||||
t.Fatalf("Unable to mask existing unit: %v", err)
|
t.Fatalf("Unable to mask existing unit: %v", err)
|
||||||
}
|
}
|
||||||
barTgt, err := os.Readlink(barPath)
|
barTgt, err := os.Readlink(barPath)
|
||||||
@@ -194,8 +200,10 @@ func TestUnmaskUnit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
sd := &systemd{dir}
|
||||||
|
|
||||||
nilUnit := &Unit{Name: "null.service"}
|
nilUnit := &Unit{Name: "null.service"}
|
||||||
if err := UnmaskUnit(nilUnit, dir); err != nil {
|
if err := sd.UnmaskUnit(nilUnit); err != nil {
|
||||||
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
|
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +219,7 @@ func TestUnmaskUnit(t *testing.T) {
|
|||||||
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
|
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
|
||||||
t.Fatalf("Unable to write unit file: %v", err)
|
t.Fatalf("Unable to write unit file: %v", err)
|
||||||
}
|
}
|
||||||
if err := UnmaskUnit(uf, dir); err != nil {
|
if err := sd.UnmaskUnit(uf); err != nil {
|
||||||
t.Errorf("unmask of non-empty unit returned unexpected error: %v", err)
|
t.Errorf("unmask of non-empty unit returned unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
got, _ := ioutil.ReadFile(dst)
|
got, _ := ioutil.ReadFile(dst)
|
||||||
@@ -224,7 +232,7 @@ func TestUnmaskUnit(t *testing.T) {
|
|||||||
if err := os.Symlink("/dev/null", dst); err != nil {
|
if err := os.Symlink("/dev/null", dst); err != nil {
|
||||||
t.Fatalf("Unable to create masked unit: %v", err)
|
t.Fatalf("Unable to create masked unit: %v", err)
|
||||||
}
|
}
|
||||||
if err := UnmaskUnit(ub, dir); err != nil {
|
if err := sd.UnmaskUnit(ub); err != nil {
|
||||||
t.Errorf("unmask of unit returned unexpected error: %v", err)
|
t.Errorf("unmask of unit returned unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(dst); !os.IsNotExist(err) {
|
if _, err := os.Stat(dst); !os.IsNotExist(err) {
|
||||||
|
67
system/unit.go
Normal file
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)
|
||||||
|
}
|
||||||
|
}
|
11
test
11
test
@@ -13,7 +13,16 @@ COVER=${COVER:-"-cover"}
|
|||||||
|
|
||||||
source ./build
|
source ./build
|
||||||
|
|
||||||
declare -a TESTPKGS=(initialize system datasource pkg network)
|
declare -a TESTPKGS=(initialize
|
||||||
|
system
|
||||||
|
datasource
|
||||||
|
datasource/configdrive
|
||||||
|
datasource/file
|
||||||
|
datasource/metadata/ec2
|
||||||
|
datasource/proc_cmdline
|
||||||
|
datasource/url
|
||||||
|
pkg
|
||||||
|
network)
|
||||||
|
|
||||||
if [ -z "$PKG" ]; then
|
if [ -z "$PKG" ]; then
|
||||||
GOFMTPATH="$TESTPKGS coreos-cloudinit.go"
|
GOFMTPATH="$TESTPKGS coreos-cloudinit.go"
|
||||||
|
Reference in New Issue
Block a user