Compare commits
153 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0785840fe3 | ||
|
c10bfc2f56 | ||
|
2f954dcdc2 | ||
|
cdfc94f4e9 | ||
|
18e2f98414 | ||
|
4b472795c4 | ||
|
85b8d804c8 | ||
|
1fbbaaec19 | ||
|
667dbd8fb7 | ||
|
6730cb7227 | ||
|
9454522033 | ||
|
c255739a93 | ||
|
2051cd3e1c | ||
|
b52cb3fea3 | ||
|
da5f85b3fb | ||
|
9999178538 | ||
|
8f766e4666 | ||
|
2d28d16c92 | ||
|
e9cd09dd7b | ||
|
8370b30aa2 | ||
|
3e015cc3a1 | ||
|
a0fe6d0884 | ||
|
585ce5fcd9 | ||
|
72445796ca | ||
|
7342d91a85 | ||
|
db1bc51c98 | ||
|
c1f373e648 | ||
|
db49a16002 | ||
|
a4a6c281d9 | ||
|
17f8733121 | ||
|
7dec922618 | ||
|
54d3ae27af | ||
|
ee2416af64 | ||
|
cda037f9a5 | ||
|
549806cf64 | ||
|
56815a6756 | ||
|
24a6f7c49c | ||
|
98484be434 | ||
|
9024659296 | ||
|
fc6940f7ba | ||
|
f2fd95699b | ||
|
65db96cc7c | ||
|
c17b93b5c0 | ||
|
d352f8ce6a | ||
|
78aa2c56ec | ||
|
c5b3788282 | ||
|
5e98970bb5 | ||
|
cbdd446c55 | ||
|
316cadcf44 | ||
|
5a939be21b | ||
|
8d76c64386 | ||
|
1b854eb51e | ||
|
9fcf338bf3 | ||
|
fda72bdb5c | ||
|
685a38c6c8 | ||
|
9d15f2cfaf | ||
|
2134fce791 | ||
|
3abd6b2225 | ||
|
2a8e6c9566 | ||
|
abe43537da | ||
|
3a550af651 | ||
|
61c3a0eb2d | ||
|
480176bc11 | ||
|
01b18eb551 | ||
|
970ef435b6 | ||
|
e8d0021140 | ||
|
e9ec78ac6f | ||
|
4a2e417781 | ||
|
604ef7ecb4 | ||
|
c39dd5cc67 | ||
|
a923161f4a | ||
|
e59e2f6cd5 | ||
|
e90fe3eba8 | ||
|
fb0187b197 | ||
|
6babe74716 | ||
|
b1e88284ca | ||
|
18a65f7dac | ||
|
0c212c72c9 | ||
|
6a800d8cc0 | ||
|
5e112147bb | ||
|
7e78b1563f | ||
|
ecbe81f103 | ||
|
45c20c1dd3 | ||
|
8ce925a060 | ||
|
eadb6ef42c | ||
|
7518f0ec93 | ||
|
f0b9eaf2fe | ||
|
7320a2cbf2 | ||
|
57950b3ed9 | ||
|
85c6a2a16a | ||
|
24b44e86a6 | ||
|
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 |
@@ -1,5 +1,7 @@
|
||||
language: go
|
||||
go: 1.2
|
||||
go:
|
||||
- 1.3
|
||||
- 1.2
|
||||
|
||||
install:
|
||||
- go get code.google.com/p/go.tools/cmd/cover
|
||||
|
@@ -39,22 +39,25 @@ Thanks for your contributions!
|
||||
|
||||
### Format of the Commit Message
|
||||
|
||||
We follow a rough convention for commit messages borrowed from AngularJS. This
|
||||
is an example of a commit:
|
||||
We follow a rough convention for commit messages that is designed to answer two
|
||||
questions: what changed and why. The subject line should feature the what and
|
||||
the body of the commit should describe the why.
|
||||
|
||||
```
|
||||
feat(scripts/test-cluster): add a cluster test command
|
||||
environment: write new keys in consistent order
|
||||
|
||||
this uses tmux to setup a test cluster that you can easily kill and
|
||||
start for debugging.
|
||||
Go 1.3 randomizes the ordering of keys when iterating over a map.
|
||||
Sort the keys to make this ordering consistent.
|
||||
|
||||
Fixes #38
|
||||
```
|
||||
|
||||
The format can be described more formally as follows:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
<subsystem>: <what changed>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<why this change was made>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
@@ -63,25 +66,3 @@ The first line is the subject and should be no longer than 70 characters, the
|
||||
second line is always blank, and other lines should be wrapped at 80 characters.
|
||||
This allows the message to be easier to read on GitHub as well as in various
|
||||
git tools.
|
||||
|
||||
#### Subject Line
|
||||
|
||||
The subject line contains a succinct description of the change.
|
||||
|
||||
#### Allowed `<type>`s
|
||||
- *feat* (feature)
|
||||
- *fix* (bug fix)
|
||||
- *docs* (documentation)
|
||||
- *style* (formatting, missing semi colons, …)
|
||||
- *refactor*
|
||||
- *test* (when adding missing tests)
|
||||
- *chore* (maintain)
|
||||
|
||||
#### Allowed `<scope>`s
|
||||
|
||||
Scopes can anything specifying the place of the commit change in the code base -
|
||||
for example, "api", "store", etc.
|
||||
|
||||
|
||||
For more details on the commit format, see the [AngularJS commit style
|
||||
guide](https://docs.google.com/a/coreos.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#).
|
||||
|
@@ -13,7 +13,7 @@ If no **id** field is provided, coreos-cloudinit will ignore this section.
|
||||
|
||||
For example, the following cloud-config document...
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
coreos:
|
||||
oem:
|
||||
@@ -26,7 +26,7 @@ coreos:
|
||||
|
||||
...would be rendered to the following `/etc/oem-release`:
|
||||
|
||||
```
|
||||
```yaml
|
||||
ID=rackspace
|
||||
NAME="Rackspace Cloud Servers"
|
||||
VERSION_ID=168.0.0
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# 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
|
||||
|
||||
The file used by this system initialization program is called a "cloud-config" file. It is inspired by the [cloud-init][cloud-init] project's [cloud-config][cloud-config] file. which is "the defacto multi-distribution package that handles early initialization of a cloud instance" ([cloud-init docs][cloud-init-docs]). Because the cloud-init project includes tools which aren't used by CoreOS, only the relevant subset of its configuration items will be implemented in our cloud-config file. In addition to those, we added a few CoreOS-specific items, such as etcd configuration, OEM definition, and systemd units.
|
||||
The file used by this system initialization program is called a "cloud-config" file. It is inspired by the [cloud-init][cloud-init] project's [cloud-config][cloud-config] file, which is "the defacto multi-distribution package that handles early initialization of a cloud instance" ([cloud-init docs][cloud-init-docs]). Because the cloud-init project includes tools which aren't used by CoreOS, only the relevant subset of its configuration items will be implemented in our cloud-config file. In addition to those, we added a few CoreOS-specific items, such as etcd configuration, OEM definition, and systemd units.
|
||||
|
||||
We've designed our implementation to allow the same cloud-config file to work across all of our supported platforms.
|
||||
|
||||
@@ -16,7 +16,7 @@ We've designed our implementation to allow the same cloud-config file to work ac
|
||||
|
||||
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
|
||||
|
||||
A cloud-config file should contain an associative array which has zero or more of the following keys:
|
||||
A cloud-config file should contain `#cloud-config`, followed by an associative array which has zero or more of the following keys:
|
||||
|
||||
- `coreos`
|
||||
- `ssh_authorized_keys`
|
||||
@@ -40,9 +40,9 @@ CoreOS tries to conform to each platform's native method to provide user data. E
|
||||
#### etcd
|
||||
|
||||
The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
|
||||
We can use the templating feature of coreos-cloudinit to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
|
||||
If the platform environment supports the templating feature of coreos-cloudinit it is possible to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
@@ -57,7 +57,7 @@ coreos:
|
||||
|
||||
...will generate a systemd unit drop-in like this:
|
||||
|
||||
```
|
||||
```yaml
|
||||
[Service]
|
||||
Environment="ETCD_NAME=node001"
|
||||
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
|
||||
@@ -68,13 +68,15 @@ Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
|
||||
For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
|
||||
Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
|
||||
|
||||
_Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._
|
||||
|
||||
[etcd-config]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md
|
||||
|
||||
#### fleet
|
||||
|
||||
The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allow for the configuration of fleet through environment variables. For example, the following cloud-config document...
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
@@ -85,7 +87,7 @@ coreos:
|
||||
|
||||
...will generate a systemd unit drop-in like this:
|
||||
|
||||
```
|
||||
```yaml
|
||||
[Service]
|
||||
Environment="FLEET_PUBLIC_IP=203.0.113.29"
|
||||
Environment="FLEET_METADATA=region=us-west"
|
||||
@@ -93,7 +95,7 @@ Environment="FLEET_METADATA=region=us-west"
|
||||
|
||||
For more information on fleet configuration, see the [fleet documentation][fleet-config].
|
||||
|
||||
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/configuration.md
|
||||
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/deployment-and-configuration.md#configuration
|
||||
|
||||
#### update
|
||||
|
||||
@@ -114,7 +116,7 @@ The `reboot-strategy` parameter also affects the behaviour of [locksmith](https:
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
coreos:
|
||||
update:
|
||||
@@ -123,14 +125,16 @@ coreos:
|
||||
|
||||
#### units
|
||||
|
||||
The `coreos.units.*` parameters define a list of arbitrary systemd units to start. Each item is an object with the following fields:
|
||||
The `coreos.units.*` parameters define a list of arbitrary systemd units to start after booting. This feature is intended to help you start essential services required to mount storage and configure networking in order to join the CoreOS cluster. It is not intended to be a Chef/Puppet replacement.
|
||||
|
||||
Each item is an object with the following fields:
|
||||
|
||||
- **name**: String representing unit's name. Required.
|
||||
- **runtime**: Boolean indicating whether or not to persist the unit across reboots. This is analogous to the `--runtime` argument to `systemctl enable`. Default value is false.
|
||||
- **enable**: Boolean indicating whether or not to handle the [Install] section of the unit file. This is similar to running `systemctl enable <name>`. 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`. The default value is false.
|
||||
- **enable**: Boolean indicating whether or not to handle the [Install] section of the unit file. This is similar to running `systemctl enable <name>`. The default value is false.
|
||||
- **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already.
|
||||
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. Default value is restart.
|
||||
- **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. Default value is false.
|
||||
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands.
|
||||
- **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. The default value is false.
|
||||
|
||||
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
|
||||
|
||||
@@ -138,7 +142,7 @@ The `coreos.units.*` parameters define a list of arbitrary systemd units to star
|
||||
|
||||
Write a unit to disk, automatically starting it.
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
@@ -159,7 +163,7 @@ coreos:
|
||||
|
||||
Start the built-in `etcd` and `fleet` services:
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
@@ -177,7 +181,7 @@ The `ssh_authorized_keys` parameter adds public SSH keys which will be authorize
|
||||
The keys will be named "coreos-cloudinit" by default.
|
||||
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
ssh_authorized_keys:
|
||||
@@ -189,7 +193,7 @@ ssh_authorized_keys:
|
||||
The `hostname` parameter defines the system's hostname.
|
||||
This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
hostname: coreos1
|
||||
@@ -203,7 +207,7 @@ All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the use
|
||||
- **name**: Required. Login name of user
|
||||
- **gecos**: GECOS comment of user
|
||||
- **passwd**: Hash of the password to use for this user
|
||||
- **homedir**: User's home directory. Defaults to /home/<name>
|
||||
- **homedir**: User's home directory. Defaults to /home/\<name\>
|
||||
- **no-create-home**: Boolean. Skip home directory creation.
|
||||
- **primary-group**: Default group for the user. Defaults to a new group created named after the user.
|
||||
- **groups**: Add user to these additional groups
|
||||
@@ -222,7 +226,7 @@ The following fields are not yet implemented:
|
||||
- **selinux-user**: Corresponding SELinux user
|
||||
- **ssh-import-id**: Import SSH keys by ID from Launchpad.
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
@@ -261,7 +265,7 @@ Using a higher number of rounds will help create more secure passwords, but give
|
||||
|
||||
Using the `coreos-ssh-import-github` field, we can import public SSH keys from a GitHub user to use as authorized keys to a server.
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
@@ -274,7 +278,7 @@ users:
|
||||
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
|
||||
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
@@ -284,7 +288,7 @@ users:
|
||||
|
||||
You can also specify any URL whose response matches the JSON format for public keys:
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
@@ -294,7 +298,8 @@ users:
|
||||
|
||||
### write_files
|
||||
|
||||
The `write-file` parameter defines a list of files to create on the local filesystem. Each file is represented as an associative array which has the following keys:
|
||||
The `write_files` directive defines a set of files to create on the local filesystem.
|
||||
Each item in the list may have the following keys:
|
||||
|
||||
- **path**: Absolute location on disk where contents should be written
|
||||
- **content**: Data to write at the provided `path`
|
||||
@@ -304,14 +309,19 @@ The `write-file` parameter defines a list of files to create on the local filesy
|
||||
Explicitly not implemented is the **encoding** attribute.
|
||||
The **content** field must represent exactly what should be written to disk.
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
write_files:
|
||||
- path: /etc/fleet/fleet.conf
|
||||
- path: /etc/resolv.conf
|
||||
permissions: 0644
|
||||
owner: root
|
||||
content: |
|
||||
verbosity=1
|
||||
metadata="region=us-west,type=ssd"
|
||||
nameserver 8.8.8.8
|
||||
- path: /etc/motd
|
||||
permissions: 0644
|
||||
owner: root
|
||||
content: |
|
||||
Good news, everyone!
|
||||
```
|
||||
|
||||
### manage_etc_hosts
|
||||
@@ -321,7 +331,7 @@ Currently, the only supported value is "localhost" which will cause your system'
|
||||
to resolve to "127.0.0.1". This is helpful when the host does not have DNS
|
||||
infrastructure in place to resolve its own hostname, for example, when using Vagrant.
|
||||
|
||||
```
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
manage_etc_hosts: localhost
|
||||
|
@@ -14,17 +14,21 @@ The image should be a single FAT or ISO9660 file system with the label
|
||||
|
||||
For example, to wrap up a config named `user_data` in a config drive image:
|
||||
|
||||
mkdir -p /tmp/new-drive/openstack/latest
|
||||
cp user_data /tmp/new-drive/openstack/latest/user_data
|
||||
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
|
||||
rm -r /tmp/new-drive
|
||||
```sh
|
||||
mkdir -p /tmp/new-drive/openstack/latest
|
||||
cp user_data /tmp/new-drive/openstack/latest/user_data
|
||||
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
|
||||
rm -r /tmp/new-drive
|
||||
```
|
||||
|
||||
## QEMU virtfs
|
||||
|
||||
One exception to the above, when using QEMU it is possible to skip creating an
|
||||
image and use a plain directory containing the same contents:
|
||||
|
||||
qemu-system-x86_64 \
|
||||
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
|
||||
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
|
||||
[usual qemu options here...]
|
||||
```sh
|
||||
qemu-system-x86_64 \
|
||||
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
|
||||
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
|
||||
[usual qemu options here...]
|
||||
```
|
||||
|
3
MAINTAINERS
Normal file
3
MAINTAINERS
Normal file
@@ -0,0 +1,3 @@
|
||||
Alex Crawford <alex.crawford@coreos.com> (@crawford)
|
||||
Jonathan Boulle <jonathan.boulle@coreos.com> (@jonboulle)
|
||||
Brian Waldon <brian.waldon@coreos.com> (@bcwaldon)
|
198
config/config.go
Normal file
198
config/config.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/third_party/gopkg.in/yaml.v1"
|
||||
)
|
||||
|
||||
// CloudConfig encapsulates the entire cloud-config configuration file and maps
|
||||
// directly to YAML. Fields that cannot be set in the cloud-config (fields
|
||||
// used for internal use) have the YAML tag '-' so that they aren't marshalled.
|
||||
type CloudConfig struct {
|
||||
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
|
||||
Coreos struct {
|
||||
Etcd Etcd `yaml:"etcd"`
|
||||
Fleet Fleet `yaml:"fleet"`
|
||||
OEM OEM `yaml:"oem"`
|
||||
Update Update `yaml:"update"`
|
||||
Units []Unit `yaml:"units"`
|
||||
} `yaml:"coreos"`
|
||||
WriteFiles []File `yaml:"write_files"`
|
||||
Hostname string `yaml:"hostname"`
|
||||
Users []User `yaml:"users"`
|
||||
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
|
||||
NetworkConfigPath string `yaml:"-"`
|
||||
NetworkConfig string `yaml:"-"`
|
||||
}
|
||||
|
||||
// NewCloudConfig instantiates a new CloudConfig from the given contents (a
|
||||
// string of YAML), returning any error encountered. It will ignore unknown
|
||||
// fields but log encountering them.
|
||||
func NewCloudConfig(contents string) (*CloudConfig, error) {
|
||||
var cfg CloudConfig
|
||||
err := yaml.Unmarshal([]byte(contents), &cfg)
|
||||
if err != nil {
|
||||
return &cfg, err
|
||||
}
|
||||
warnOnUnrecognizedKeys(contents, log.Printf)
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (cc CloudConfig) String() string {
|
||||
bytes, err := yaml.Marshal(cc)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
stringified := string(bytes)
|
||||
stringified = fmt.Sprintf("#cloud-config\n%s", stringified)
|
||||
|
||||
return stringified
|
||||
}
|
||||
|
||||
// IsZero returns whether or not the parameter is the zero value for its type.
|
||||
// If the parameter is a struct, only the exported fields are considered.
|
||||
func IsZero(c interface{}) bool {
|
||||
return isZero(reflect.ValueOf(c))
|
||||
}
|
||||
|
||||
// AssertValid checks the fields in the structure and makes sure that they
|
||||
// contain valid values as specified by the 'valid' flag. Empty fields are
|
||||
// implicitly valid.
|
||||
func AssertValid(c interface{}) error {
|
||||
ct := reflect.TypeOf(c)
|
||||
cv := reflect.ValueOf(c)
|
||||
for i := 0; i < ct.NumField(); i++ {
|
||||
ft := ct.Field(i)
|
||||
if !isFieldExported(ft) {
|
||||
continue
|
||||
}
|
||||
|
||||
valid := ft.Tag.Get("valid")
|
||||
val := cv.Field(i)
|
||||
if !isValid(val, valid) {
|
||||
return fmt.Errorf("invalid value \"%v\" for option %q (valid options: %q)", val.Interface(), ft.Name, valid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isZero(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
vt := v.Type()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if isFieldExported(vt.Field(i)) && !isZero(v.Field(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return v.Interface() == reflect.Zero(v.Type()).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
func isFieldExported(f reflect.StructField) bool {
|
||||
return f.PkgPath == ""
|
||||
}
|
||||
|
||||
func isValid(v reflect.Value, valid string) bool {
|
||||
if valid == "" || isZero(v) {
|
||||
return true
|
||||
}
|
||||
vs := fmt.Sprintf("%v", v.Interface())
|
||||
for _, valid := range strings.Split(valid, ",") {
|
||||
if vs == valid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type warner func(format string, v ...interface{})
|
||||
|
||||
// warnOnUnrecognizedKeys parses the contents of a cloud-config file and calls
|
||||
// warn(msg, key) for every unrecognized key (i.e. those not present in CloudConfig)
|
||||
func warnOnUnrecognizedKeys(contents string, warn warner) {
|
||||
// Generate a map of all understood cloud config options
|
||||
var cc map[string]interface{}
|
||||
b, _ := yaml.Marshal(&CloudConfig{})
|
||||
yaml.Unmarshal(b, &cc)
|
||||
|
||||
// Now unmarshal the entire provided contents
|
||||
var c map[string]interface{}
|
||||
yaml.Unmarshal([]byte(contents), &c)
|
||||
|
||||
// Check that every key in the contents exists in the cloud config
|
||||
for k, _ := range c {
|
||||
if _, ok := cc[k]; !ok {
|
||||
warn("Warning: unrecognized key %q in provided cloud config - ignoring section", k)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unrecognized coreos options, if any are set
|
||||
if coreos, ok := c["coreos"]; ok {
|
||||
if set, ok := coreos.(map[interface{}]interface{}); ok {
|
||||
known := cc["coreos"].(map[interface{}]interface{})
|
||||
for k, _ := range set {
|
||||
if key, ok := k.(string); ok {
|
||||
if _, ok := known[key]; !ok {
|
||||
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", key)
|
||||
}
|
||||
} else {
|
||||
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any badly-specified users, if any are set
|
||||
if users, ok := c["users"]; ok {
|
||||
var known map[string]interface{}
|
||||
b, _ := yaml.Marshal(&User{})
|
||||
yaml.Unmarshal(b, &known)
|
||||
|
||||
if set, ok := users.([]interface{}); ok {
|
||||
for _, u := range set {
|
||||
if user, ok := u.(map[interface{}]interface{}); ok {
|
||||
for k, _ := range user {
|
||||
if key, ok := k.(string); ok {
|
||||
if _, ok := known[key]; !ok {
|
||||
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", key)
|
||||
}
|
||||
} else {
|
||||
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any badly-specified files, if any are set
|
||||
if files, ok := c["write_files"]; ok {
|
||||
var known map[string]interface{}
|
||||
b, _ := yaml.Marshal(&File{})
|
||||
yaml.Unmarshal(b, &known)
|
||||
|
||||
if set, ok := files.([]interface{}); ok {
|
||||
for _, f := range set {
|
||||
if file, ok := f.(map[interface{}]interface{}); ok {
|
||||
for k, _ := range file {
|
||||
if key, ok := k.(string); ok {
|
||||
if _, ok := known[key]; !ok {
|
||||
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", key)
|
||||
}
|
||||
} else {
|
||||
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
473
config/config_test.go
Normal file
473
config/config_test.go
Normal file
@@ -0,0 +1,473 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
c interface{}
|
||||
empty bool
|
||||
}{
|
||||
{struct{}{}, true},
|
||||
{struct{ a, b string }{}, true},
|
||||
{struct{ A, b string }{}, true},
|
||||
{struct{ A, B string }{}, true},
|
||||
{struct{ A string }{A: "hello"}, false},
|
||||
{struct{ A int }{}, true},
|
||||
{struct{ A int }{A: 1}, false},
|
||||
} {
|
||||
if empty := IsZero(tt.c); tt.empty != empty {
|
||||
t.Errorf("bad result (%q): want %q, got %q", tt.c, tt.empty, empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssertValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
c interface{}
|
||||
err error
|
||||
}{
|
||||
{struct{}{}, nil},
|
||||
{struct {
|
||||
A, b string `valid:"1,2"`
|
||||
}{}, nil},
|
||||
{struct {
|
||||
A, b string `valid:"1,2"`
|
||||
}{A: "1", b: "2"}, nil},
|
||||
{struct {
|
||||
A, b string `valid:"1,2"`
|
||||
}{A: "1", b: "hello"}, nil},
|
||||
{struct {
|
||||
A, b string `valid:"1,2"`
|
||||
}{A: "hello", b: "2"}, errors.New("invalid value \"hello\" for option \"A\" (valid options: \"1,2\")")},
|
||||
{struct {
|
||||
A, b int `valid:"1,2"`
|
||||
}{}, nil},
|
||||
{struct {
|
||||
A, b int `valid:"1,2"`
|
||||
}{A: 1, b: 2}, nil},
|
||||
{struct {
|
||||
A, b int `valid:"1,2"`
|
||||
}{A: 1, b: 9}, nil},
|
||||
{struct {
|
||||
A, b int `valid:"1,2"`
|
||||
}{A: 9, b: 2}, errors.New("invalid value \"9\" for option \"A\" (valid options: \"1,2\")")},
|
||||
} {
|
||||
if err := AssertValid(tt.c); !reflect.DeepEqual(tt.err, err) {
|
||||
t.Errorf("bad result (%q): want %q, got %q", tt.c, tt.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigInvalidKeys(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("panic while instantiating CloudConfig with nil keys: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, tt := range []struct {
|
||||
contents string
|
||||
}{
|
||||
{"coreos:"},
|
||||
{"ssh_authorized_keys:"},
|
||||
{"ssh_authorized_keys:\n -"},
|
||||
{"ssh_authorized_keys:\n - 0:"},
|
||||
{"write_files:"},
|
||||
{"write_files:\n -"},
|
||||
{"write_files:\n - 0:"},
|
||||
{"users:"},
|
||||
{"users:\n -"},
|
||||
{"users:\n - 0:"},
|
||||
} {
|
||||
_, err := NewCloudConfig(tt.contents)
|
||||
if err != nil {
|
||||
t.Fatalf("error instantiating CloudConfig with invalid keys: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUnknownKeys(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
etcd:
|
||||
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
||||
coreos_unknown:
|
||||
foo: "bar"
|
||||
section_unknown:
|
||||
dunno:
|
||||
something
|
||||
bare_unknown:
|
||||
bar
|
||||
write_files:
|
||||
- content: fun
|
||||
path: /var/party
|
||||
file_unknown: nofun
|
||||
users:
|
||||
- name: fry
|
||||
passwd: somehash
|
||||
user_unknown: philip
|
||||
hostname:
|
||||
foo
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("error instantiating CloudConfig with unknown keys: %v", err)
|
||||
}
|
||||
if cfg.Hostname != "foo" {
|
||||
t.Fatalf("hostname not correctly set when invalid keys are present")
|
||||
}
|
||||
if cfg.Coreos.Etcd.Discovery != "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" {
|
||||
t.Fatalf("etcd section not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.WriteFiles) < 1 || cfg.WriteFiles[0].Content != "fun" || cfg.WriteFiles[0].Path != "/var/party" {
|
||||
t.Fatalf("write_files section not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.Users) < 1 || cfg.Users[0].Name != "fry" || cfg.Users[0].PasswordHash != "somehash" {
|
||||
t.Fatalf("users section not correctly set when invalid keys are present")
|
||||
}
|
||||
|
||||
var warnings string
|
||||
catchWarn := func(f string, v ...interface{}) {
|
||||
warnings += fmt.Sprintf(f, v...)
|
||||
}
|
||||
|
||||
warnOnUnrecognizedKeys(contents, catchWarn)
|
||||
|
||||
if !strings.Contains(warnings, "coreos_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized coreos option coreos_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "bare_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized key bare_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "section_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized key section_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "user_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized user key user_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "file_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized file key file_unknown")
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the parsing of a cloud config file "generally works"
|
||||
func TestCloudConfigEmpty(t *testing.T) {
|
||||
cfg, err := NewCloudConfig("")
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error :%v", err)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
if len(keys) != 0 {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
}
|
||||
|
||||
if len(cfg.WriteFiles) != 0 {
|
||||
t.Error("Expected zero WriteFiles")
|
||||
}
|
||||
|
||||
if cfg.Hostname != "" {
|
||||
t.Errorf("Expected hostname to be empty, got '%s'", cfg.Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the parsing of a cloud config file "generally works"
|
||||
func TestCloudConfig(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
etcd:
|
||||
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
||||
update:
|
||||
reboot-strategy: reboot
|
||||
units:
|
||||
- name: 50-eth0.network
|
||||
runtime: yes
|
||||
content: '[Match]
|
||||
|
||||
Name=eth47
|
||||
|
||||
|
||||
[Network]
|
||||
|
||||
Address=10.209.171.177/19
|
||||
|
||||
'
|
||||
oem:
|
||||
id: rackspace
|
||||
name: Rackspace Cloud Servers
|
||||
version-id: 168.0.0
|
||||
home-url: https://www.rackspace.com/cloud/servers/
|
||||
bug-report-url: https://github.com/coreos/coreos-overlay
|
||||
ssh_authorized_keys:
|
||||
- foobar
|
||||
- foobaz
|
||||
write_files:
|
||||
- content: |
|
||||
penny
|
||||
elroy
|
||||
path: /etc/dogepack.conf
|
||||
permissions: '0644'
|
||||
owner: root:dogepack
|
||||
hostname: trontastic
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error :%v", err)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
if len(keys) != 2 {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
} else if keys[0] != "foobar" {
|
||||
t.Error("Expected first SSH key to be 'foobar'")
|
||||
} else if keys[1] != "foobaz" {
|
||||
t.Error("Expected first SSH key to be 'foobaz'")
|
||||
}
|
||||
|
||||
if len(cfg.WriteFiles) != 1 {
|
||||
t.Error("Failed to parse correct number of write_files")
|
||||
} else {
|
||||
wf := cfg.WriteFiles[0]
|
||||
if wf.Content != "penny\nelroy\n" {
|
||||
t.Errorf("WriteFile has incorrect contents '%s'", wf.Content)
|
||||
}
|
||||
if wf.Encoding != "" {
|
||||
t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding)
|
||||
}
|
||||
if wf.RawFilePermissions != "0644" {
|
||||
t.Errorf("WriteFile has incorrect permissions %s", wf.RawFilePermissions)
|
||||
}
|
||||
if wf.Path != "/etc/dogepack.conf" {
|
||||
t.Errorf("WriteFile has incorrect path %s", wf.Path)
|
||||
}
|
||||
if wf.Owner != "root:dogepack" {
|
||||
t.Errorf("WriteFile has incorrect owner %s", wf.Owner)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.Coreos.Units) != 1 {
|
||||
t.Error("Failed to parse correct number of units")
|
||||
} else {
|
||||
u := cfg.Coreos.Units[0]
|
||||
expect := `[Match]
|
||||
Name=eth47
|
||||
|
||||
[Network]
|
||||
Address=10.209.171.177/19
|
||||
`
|
||||
if u.Content != expect {
|
||||
t.Errorf("Unit has incorrect contents '%s'.\nExpected '%s'.", u.Content, expect)
|
||||
}
|
||||
if u.Runtime != true {
|
||||
t.Errorf("Unit has incorrect runtime value")
|
||||
}
|
||||
if u.Name != "50-eth0.network" {
|
||||
t.Errorf("Unit has incorrect name %s", u.Name)
|
||||
}
|
||||
if u.Type() != "network" {
|
||||
t.Errorf("Unit has incorrect type '%s'", u.Type())
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Coreos.OEM.ID != "rackspace" {
|
||||
t.Errorf("Failed parsing coreos.oem. Expected ID 'rackspace', got %q.", cfg.Coreos.OEM.ID)
|
||||
}
|
||||
|
||||
if cfg.Hostname != "trontastic" {
|
||||
t.Errorf("Failed to parse hostname")
|
||||
}
|
||||
if cfg.Coreos.Update.RebootStrategy != "reboot" {
|
||||
t.Errorf("Failed to parse locksmith strategy")
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that our interface conversion doesn't panic
|
||||
func TestCloudConfigKeysNotList(t *testing.T) {
|
||||
contents := `
|
||||
ssh_authorized_keys:
|
||||
- foo: bar
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
if len(keys) != 0 {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigSerializationHeader(t *testing.T) {
|
||||
cfg, _ := NewCloudConfig("")
|
||||
contents := cfg.String()
|
||||
header := strings.SplitN(contents, "\n", 2)[0]
|
||||
if header != "#cloud-config" {
|
||||
t.Fatalf("Serialized config did not have expected header")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropInIgnored asserts that users are unable to set DropIn=True on units
|
||||
func TestDropInIgnored(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
units:
|
||||
- name: test
|
||||
dropin: true
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil || len(cfg.Coreos.Units) != 1 {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
if len(cfg.Coreos.Units) != 1 || cfg.Coreos.Units[0].Name != "test" {
|
||||
t.Fatalf("Expected 1 unit, but got %d: %v", len(cfg.Coreos.Units), cfg.Coreos.Units)
|
||||
}
|
||||
if cfg.Coreos.Units[0].DropIn {
|
||||
t.Errorf("dropin option on unit in cloud-config was not ignored!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUsers(t *testing.T) {
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
passwd: somehash
|
||||
ssh-authorized-keys:
|
||||
- somekey
|
||||
gecos: arbitrary comment
|
||||
homedir: /home/place
|
||||
no-create-home: yes
|
||||
primary-group: things
|
||||
groups:
|
||||
- ping
|
||||
- pong
|
||||
no-user-group: true
|
||||
system: y
|
||||
no-log-init: True
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.PasswordHash != "somehash" {
|
||||
t.Errorf("User passwd is %q, expected 'somehash'", user.PasswordHash)
|
||||
}
|
||||
|
||||
if keys := user.SSHAuthorizedKeys; len(keys) != 1 {
|
||||
t.Errorf("Parsed %d ssh keys, expected 1", len(keys))
|
||||
} else {
|
||||
key := user.SSHAuthorizedKeys[0]
|
||||
if key != "somekey" {
|
||||
t.Errorf("User SSH key is %q, expected 'somekey'", key)
|
||||
}
|
||||
}
|
||||
|
||||
if user.GECOS != "arbitrary comment" {
|
||||
t.Errorf("Failed to parse gecos field, got %q", user.GECOS)
|
||||
}
|
||||
|
||||
if user.Homedir != "/home/place" {
|
||||
t.Errorf("Failed to parse homedir field, got %q", user.Homedir)
|
||||
}
|
||||
|
||||
if !user.NoCreateHome {
|
||||
t.Errorf("Failed to parse no-create-home field")
|
||||
}
|
||||
|
||||
if user.PrimaryGroup != "things" {
|
||||
t.Errorf("Failed to parse primary-group field, got %q", user.PrimaryGroup)
|
||||
}
|
||||
|
||||
if len(user.Groups) != 2 {
|
||||
t.Errorf("Failed to parse 2 goups, got %d", len(user.Groups))
|
||||
} else {
|
||||
if user.Groups[0] != "ping" {
|
||||
t.Errorf("First group was %q, not expected value 'ping'", user.Groups[0])
|
||||
}
|
||||
if user.Groups[1] != "pong" {
|
||||
t.Errorf("First group was %q, not expected value 'pong'", user.Groups[1])
|
||||
}
|
||||
}
|
||||
|
||||
if !user.NoUserGroup {
|
||||
t.Errorf("Failed to parse no-user-group field")
|
||||
}
|
||||
|
||||
if !user.System {
|
||||
t.Errorf("Failed to parse system field")
|
||||
}
|
||||
|
||||
if !user.NoLogInit {
|
||||
t.Errorf("Failed to parse no-log-init field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUsersGithubUser(t *testing.T) {
|
||||
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-github: bcwaldon
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.SSHImportGithubUser != "bcwaldon" {
|
||||
t.Errorf("github user is %q, expected 'bcwaldon'", user.SSHImportGithubUser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUsersSSHImportURL(t *testing.T) {
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-url: https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.SSHImportURL != "https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys" {
|
||||
t.Errorf("ssh import url is %q, expected 'https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys'", user.SSHImportURL)
|
||||
}
|
||||
}
|
3
config/etc_hosts.go
Normal file
3
config/etc_hosts.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package config
|
||||
|
||||
type EtcHosts string
|
32
config/etcd.go
Normal file
32
config/etcd.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package config
|
||||
|
||||
type Etcd struct {
|
||||
Addr string `yaml:"addr" env:"ETCD_ADDR"`
|
||||
BindAddr string `yaml:"bind-addr" env:"ETCD_BIND_ADDR"`
|
||||
CAFile string `yaml:"ca-file" env:"ETCD_CA_FILE"`
|
||||
CertFile string `yaml:"cert-file" env:"ETCD_CERT_FILE"`
|
||||
ClusterActiveSize string `yaml:"cluster-active-size" env:"ETCD_CLUSTER_ACTIVE_SIZE"`
|
||||
ClusterRemoveDelay string `yaml:"cluster-remove-delay" env:"ETCD_CLUSTER_REMOVE_DELAY"`
|
||||
ClusterSyncInterval string `yaml:"cluster-sync-interval" env:"ETCD_CLUSTER_SYNC_INTERVAL"`
|
||||
Cors string `yaml:"cors" env:"ETCD_CORS"`
|
||||
CPUProfileFile string `yaml:"cpu-profile-file" env:"ETCD_CPU_PROFILE_FILE"`
|
||||
DataDir string `yaml:"data-dir" env:"ETCD_DATA_DIR"`
|
||||
Discovery string `yaml:"discovery" env:"ETCD_DISCOVERY"`
|
||||
HTTPReadTimeout string `yaml:"http-read-timeout" env:"ETCD_HTTP_READ_TIMEOUT"`
|
||||
HTTPWriteTimeout string `yaml:"http-write-timeout" env:"ETCD_HTTP_WRITE_TIMEOUT"`
|
||||
KeyFile string `yaml:"key-file" env:"ETCD_KEY_FILE"`
|
||||
MaxClusterSize string `yaml:"max-cluster-size" env:"ETCD_MAX_CLUSTER_SIZE"`
|
||||
MaxResultBuffer string `yaml:"max-result-buffer" env:"ETCD_MAX_RESULT_BUFFER"`
|
||||
MaxRetryAttempts string `yaml:"max-retry-attempts" env:"ETCD_MAX_RETRY_ATTEMPTS"`
|
||||
Name string `yaml:"name" env:"ETCD_NAME"`
|
||||
PeerAddr string `yaml:"peer-addr" env:"ETCD_PEER_ADDR"`
|
||||
PeerBindAddr string `yaml:"peer-bind-addr" env:"ETCD_PEER_BIND_ADDR"`
|
||||
PeerCAFile string `yaml:"peer-ca-file" env:"ETCD_PEER_CA_FILE"`
|
||||
PeerCertFile string `yaml:"peer-cert-file" env:"ETCD_PEER_CERT_FILE"`
|
||||
PeerKeyFile string `yaml:"peer-key-file" env:"ETCD_PEER_KEY_FILE"`
|
||||
Peers string `yaml:"peers" env:"ETCD_PEERS"`
|
||||
PeersFile string `yaml:"peers-file" env:"ETCD_PEERS_FILE"`
|
||||
Snapshot string `yaml:"snapshot" env:"ETCD_SNAPSHOT"`
|
||||
Verbose string `yaml:"verbose" env:"ETCD_VERBOSE"`
|
||||
VeryVerbose string `yaml:"very-verbose" env:"ETCD_VERY_VERBOSE"`
|
||||
}
|
9
config/file.go
Normal file
9
config/file.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
type File struct {
|
||||
Encoding string `yaml:"-"`
|
||||
Content string `yaml:"content"`
|
||||
Owner string `yaml:"owner"`
|
||||
Path string `yaml:"path"`
|
||||
RawFilePermissions string `yaml:"permissions"`
|
||||
}
|
14
config/fleet.go
Normal file
14
config/fleet.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package config
|
||||
|
||||
type Fleet struct {
|
||||
AgentTTL string `yaml:"agent-ttl" env:"FLEET_AGENT_TTL"`
|
||||
EngineReconcileInterval string `yaml:"engine-reconcile-interval" env:"FLEET_ENGINE_RECONCILE_INTERVAL"`
|
||||
EtcdCAFile string `yaml:"etcd-cafile" env:"FLEET_ETCD_CAFILE"`
|
||||
EtcdCertFile string `yaml:"etcd-certfile" env:"FLEET_ETCD_CERTFILE"`
|
||||
EtcdKeyFile string `yaml:"etcd-keyfile" env:"FLEET_ETCD_KEYFILE"`
|
||||
EtcdRequestTimeout string `yaml:"etcd-request-timeout" env:"FLEET_ETCD_REQUEST_TIMEOUT"`
|
||||
EtcdServers string `yaml:"etcd-servers" env:"FLEET_ETCD_SERVERS"`
|
||||
Metadata string `yaml:"metadata" env:"FLEET_METADATA"`
|
||||
PublicIP string `yaml:"public-ip" env:"FLEET_PUBLIC_IP"`
|
||||
Verbosity string `yaml:"verbosity" env:"FLEET_VERBOSITY"`
|
||||
}
|
9
config/oem.go
Normal file
9
config/oem.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
type OEM struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
VersionID string `yaml:"version-id"`
|
||||
HomeURL string `yaml:"home-url"`
|
||||
BugReportURL string `yaml:"bug-report-url"`
|
||||
}
|
34
config/unit.go
Normal file
34
config/unit.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Unit struct {
|
||||
Name string `yaml:"name"`
|
||||
Mask bool `yaml:"mask"`
|
||||
Enable bool `yaml:"enable"`
|
||||
Runtime bool `yaml:"runtime"`
|
||||
Content string `yaml:"content"`
|
||||
Command string `yaml:"command"`
|
||||
|
||||
// For drop-in units, a cloudinit.conf is generated.
|
||||
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
|
||||
// until the correct behaviour for multiple drop-in units is determined.
|
||||
DropIn bool `yaml:"-"`
|
||||
}
|
||||
|
||||
func (u *Unit) Type() string {
|
||||
ext := filepath.Ext(u.Name)
|
||||
return strings.TrimLeft(ext, ".")
|
||||
}
|
||||
|
||||
func (u *Unit) Group() string {
|
||||
switch u.Type() {
|
||||
case "network", "netdev", "link":
|
||||
return "network"
|
||||
default:
|
||||
return "system"
|
||||
}
|
||||
}
|
7
config/update.go
Normal file
7
config/update.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
type Update struct {
|
||||
RebootStrategy string `yaml:"reboot-strategy" env:"REBOOT_STRATEGY" valid:"best-effort,etcd-lock,reboot,off"`
|
||||
Group string `yaml:"group" env:"GROUP"`
|
||||
Server string `yaml:"server" env:"SERVER"`
|
||||
}
|
17
config/user.go
Normal file
17
config/user.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package config
|
||||
|
||||
type User struct {
|
||||
Name string `yaml:"name"`
|
||||
PasswordHash string `yaml:"passwd"`
|
||||
SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"`
|
||||
SSHImportGithubUser string `yaml:"coreos-ssh-import-github"`
|
||||
SSHImportURL string `yaml:"coreos-ssh-import-url"`
|
||||
GECOS string `yaml:"gecos"`
|
||||
Homedir string `yaml:"homedir"`
|
||||
NoCreateHome bool `yaml:"no-create-home"`
|
||||
PrimaryGroup string `yaml:"primary-group"`
|
||||
Groups []string `yaml:"groups"`
|
||||
NoUserGroup bool `yaml:"no-user-group"`
|
||||
System bool `yaml:"system"`
|
||||
NoLogInit bool `yaml:"no-log-init"`
|
||||
}
|
@@ -7,99 +7,140 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"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/cloudsigma"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean"
|
||||
"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/pkg"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const (
|
||||
version = "0.8.5"
|
||||
version = "0.10.4"
|
||||
datasourceInterval = 100 * time.Millisecond
|
||||
datasourceMaxInterval = 30 * time.Second
|
||||
datasourceTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
printVersion bool
|
||||
ignoreFailure bool
|
||||
sources struct {
|
||||
file string
|
||||
configDrive string
|
||||
metadataService bool
|
||||
url string
|
||||
procCmdLine bool
|
||||
}
|
||||
convertNetconf string
|
||||
workspace string
|
||||
sshKeyName string
|
||||
flags = struct {
|
||||
printVersion bool
|
||||
ignoreFailure bool
|
||||
sources struct {
|
||||
file string
|
||||
configDrive string
|
||||
metadataService bool
|
||||
ec2MetadataService string
|
||||
cloudSigmaMetadataService bool
|
||||
digitalOceanMetadataService string
|
||||
url string
|
||||
procCmdLine bool
|
||||
}
|
||||
convertNetconf string
|
||||
workspace string
|
||||
sshKeyName string
|
||||
oem string
|
||||
}{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&printVersion, "version", false, "Print the version and exit")
|
||||
flag.BoolVar(&ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")
|
||||
flag.StringVar(&sources.file, "from-file", "", "Read user-data from provided file")
|
||||
flag.StringVar(&sources.configDrive, "from-configdrive", "", "Read data from provided cloud-drive directory")
|
||||
flag.BoolVar(&sources.metadataService, "from-metadata-service", false, "Download data from metadata service")
|
||||
flag.StringVar(&sources.url, "from-url", "", "Download user-data from provided url")
|
||||
flag.BoolVar(&sources.procCmdLine, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=<url>', using the cloud-config served by an HTTP GET to <url>", datasource.ProcCmdlineLocation, datasource.ProcCmdlineCloudConfigFlag))
|
||||
flag.StringVar(&convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files (requires the -from-configdrive flag)")
|
||||
flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
|
||||
flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
|
||||
flag.BoolVar(&flags.printVersion, "version", false, "Print the version and exit")
|
||||
flag.BoolVar(&flags.ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")
|
||||
flag.StringVar(&flags.sources.file, "from-file", "", "Read user-data from provided file")
|
||||
flag.StringVar(&flags.sources.configDrive, "from-configdrive", "", "Read data from provided cloud-drive directory")
|
||||
flag.BoolVar(&flags.sources.metadataService, "from-metadata-service", false, "[DEPRECATED - Use -from-ec2-metadata] Download data from metadata service")
|
||||
flag.StringVar(&flags.sources.ec2MetadataService, "from-ec2-metadata", "", "Download EC2 data from the provided url")
|
||||
flag.BoolVar(&flags.sources.cloudSigmaMetadataService, "from-cloudsigma-metadata", false, "Download data from CloudSigma server context")
|
||||
flag.StringVar(&flags.sources.digitalOceanMetadataService, "from-digitalocean-metadata", "", "Download DigitalOcean data from the provided url")
|
||||
flag.StringVar(&flags.sources.url, "from-url", "", "Download user-data from provided url")
|
||||
flag.BoolVar(&flags.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(&flags.oem, "oem", "", "Use the settings specific to the provided OEM")
|
||||
flag.StringVar(&flags.convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files")
|
||||
flag.StringVar(&flags.workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
|
||||
flag.StringVar(&flags.sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
|
||||
}
|
||||
|
||||
type oemConfig map[string]string
|
||||
|
||||
var (
|
||||
oemConfigs = map[string]oemConfig{
|
||||
"digitalocean": oemConfig{
|
||||
"from-digitalocean-metadata": "http://169.254.169.254/",
|
||||
"convert-netconf": "digitalocean",
|
||||
},
|
||||
"ec2-compat": oemConfig{
|
||||
"from-ec2-metadata": "http://169.254.169.254/",
|
||||
"from-configdrive": "/media/configdrive",
|
||||
},
|
||||
"rackspace-onmetal": oemConfig{
|
||||
"from-configdrive": "/media/configdrive",
|
||||
"convert-netconf": "debian",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
failure := false
|
||||
|
||||
flag.Parse()
|
||||
|
||||
die := func() {
|
||||
if ignoreFailure {
|
||||
os.Exit(0)
|
||||
if c, ok := oemConfigs[flags.oem]; ok {
|
||||
for k, v := range c {
|
||||
flag.Set(k, v)
|
||||
}
|
||||
os.Exit(1)
|
||||
} else if flags.oem != "" {
|
||||
oems := make([]string, 0, len(oemConfigs))
|
||||
for k := range oemConfigs {
|
||||
oems = append(oems, k)
|
||||
}
|
||||
fmt.Printf("Invalid option to --oem: %q. Supported options: %q\n", flags.oem, oems)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if printVersion == true {
|
||||
if flags.printVersion == true {
|
||||
fmt.Printf("coreos-cloudinit version %s\n", version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if convertNetconf != "" && sources.configDrive == "" {
|
||||
fmt.Println("-convert-netconf flag requires -from-configdrive")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch convertNetconf {
|
||||
switch flags.convertNetconf {
|
||||
case "":
|
||||
case "debian":
|
||||
case "digitalocean":
|
||||
default:
|
||||
fmt.Printf("Invalid option to -convert-netconf: '%s'. Supported options: 'debian'\n", convertNetconf)
|
||||
os.Exit(1)
|
||||
fmt.Printf("Invalid option to -convert-netconf: '%s'. Supported options: 'debian, digitalocean'\n", flags.convertNetconf)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
dss := getDatasources()
|
||||
if len(dss) == 0 {
|
||||
fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-metadata-service, --from-url or --from-proc-cmdline")
|
||||
os.Exit(1)
|
||||
fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --from-cloudsigma-metadata, --from-url or --from-proc-cmdline")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
ds := selectDatasource(dss)
|
||||
if ds == nil {
|
||||
fmt.Println("No datasources available in time")
|
||||
die()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Fetching user-data from datasource of type %q\n", ds.Type())
|
||||
userdataBytes, err := ds.FetchUserdata()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed fetching user-data from datasource: %v\n", err)
|
||||
die()
|
||||
fmt.Printf("Failed fetching user-data from datasource: %v\nContinuing...\n", err)
|
||||
failure = true
|
||||
}
|
||||
|
||||
fmt.Printf("Fetching meta-data from datasource of type %q\n", ds.Type())
|
||||
metadataBytes, err := ds.FetchMetadata()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed fetching meta-data from datasource: %v\n", err)
|
||||
die()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Extract IPv4 addresses from metadata if possible
|
||||
@@ -108,33 +149,44 @@ func main() {
|
||||
subs, err = initialize.ExtractIPsFromMetadata(metadataBytes)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed extracting IPs from meta-data: %v\n", err)
|
||||
die()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply environment to user-data
|
||||
env := initialize.NewEnvironment("/", ds.ConfigRoot(), workspace, convertNetconf, sshKeyName, subs)
|
||||
env := initialize.NewEnvironment("/", ds.ConfigRoot(), flags.workspace, flags.convertNetconf, flags.sshKeyName, subs)
|
||||
userdata := env.Apply(string(userdataBytes))
|
||||
|
||||
var ccm, ccu *initialize.CloudConfig
|
||||
var ccm, ccu *config.CloudConfig
|
||||
var script *system.Script
|
||||
if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil {
|
||||
fmt.Printf("Failed to parse meta-data: %v\n", err)
|
||||
die()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if ccm != nil && flags.convertNetconf != "" {
|
||||
fmt.Printf("Fetching network config from datasource of type %q\n", ds.Type())
|
||||
netconfBytes, err := ds.FetchNetworkConfig(ccm.NetworkConfigPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed fetching network config from datasource: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ccm.NetworkConfig = string(netconfBytes)
|
||||
}
|
||||
|
||||
if ud, err := initialize.ParseUserData(userdata); err != nil {
|
||||
fmt.Printf("Failed to parse user-data: %v\n", err)
|
||||
die()
|
||||
fmt.Printf("Failed to parse user-data: %v\nContinuing...\n", err)
|
||||
failure = true
|
||||
} else {
|
||||
switch t := ud.(type) {
|
||||
case *initialize.CloudConfig:
|
||||
case *config.CloudConfig:
|
||||
ccu = t
|
||||
case system.Script:
|
||||
script = &t
|
||||
}
|
||||
}
|
||||
|
||||
var cc *initialize.CloudConfig
|
||||
var cc *config.CloudConfig
|
||||
if ccm != nil && ccu != nil {
|
||||
fmt.Println("Merging cloud-config from meta-data and user-data")
|
||||
merged := mergeCloudConfig(*ccm, *ccu)
|
||||
@@ -152,16 +204,20 @@ func main() {
|
||||
if cc != nil {
|
||||
if err = initialize.Apply(*cc, env); err != nil {
|
||||
fmt.Printf("Failed to apply cloud-config: %v\n", err)
|
||||
die()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if script != nil {
|
||||
if err = runScript(*script, env); err != nil {
|
||||
fmt.Printf("Failed to run script: %v\n", err)
|
||||
die()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if failure && !flags.ignoreFailure {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// mergeCloudConfig merges certain options from mdcc (a CloudConfig derived from
|
||||
@@ -169,10 +225,10 @@ func main() {
|
||||
// not already set on udcc (i.e. user-data always takes precedence)
|
||||
// NB: This needs to be kept in sync with ParseMetadata so that it tracks all
|
||||
// elements of a CloudConfig which that function can populate.
|
||||
func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudConfig) {
|
||||
func mergeCloudConfig(mdcc, udcc config.CloudConfig) (cc config.CloudConfig) {
|
||||
if mdcc.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 {
|
||||
udcc.Hostname = mdcc.Hostname
|
||||
}
|
||||
@@ -183,11 +239,18 @@ func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudCon
|
||||
}
|
||||
if mdcc.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 {
|
||||
udcc.NetworkConfigPath = mdcc.NetworkConfigPath
|
||||
}
|
||||
}
|
||||
if mdcc.NetworkConfig != "" {
|
||||
if udcc.NetworkConfig != "" {
|
||||
fmt.Printf("Warning: user-data NetworkConfig %s overrides metadata NetworkConfig %s\n", udcc.NetworkConfig, mdcc.NetworkConfig)
|
||||
} else {
|
||||
udcc.NetworkConfig = mdcc.NetworkConfig
|
||||
}
|
||||
}
|
||||
return udcc
|
||||
}
|
||||
|
||||
@@ -195,20 +258,29 @@ func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudCon
|
||||
// on the different source command-line flags.
|
||||
func getDatasources() []datasource.Datasource {
|
||||
dss := make([]datasource.Datasource, 0, 5)
|
||||
if sources.file != "" {
|
||||
dss = append(dss, datasource.NewLocalFile(sources.file))
|
||||
if flags.sources.file != "" {
|
||||
dss = append(dss, file.NewDatasource(flags.sources.file))
|
||||
}
|
||||
if sources.url != "" {
|
||||
dss = append(dss, datasource.NewRemoteFile(sources.url))
|
||||
if flags.sources.url != "" {
|
||||
dss = append(dss, url.NewDatasource(flags.sources.url))
|
||||
}
|
||||
if sources.configDrive != "" {
|
||||
dss = append(dss, datasource.NewConfigDrive(sources.configDrive))
|
||||
if flags.sources.configDrive != "" {
|
||||
dss = append(dss, configdrive.NewDatasource(flags.sources.configDrive))
|
||||
}
|
||||
if sources.metadataService {
|
||||
dss = append(dss, datasource.NewMetadataService())
|
||||
if flags.sources.metadataService {
|
||||
dss = append(dss, ec2.NewDatasource(ec2.DefaultAddress))
|
||||
}
|
||||
if sources.procCmdLine {
|
||||
dss = append(dss, datasource.NewProcCmdline())
|
||||
if flags.sources.ec2MetadataService != "" {
|
||||
dss = append(dss, ec2.NewDatasource(flags.sources.ec2MetadataService))
|
||||
}
|
||||
if flags.sources.cloudSigmaMetadataService {
|
||||
dss = append(dss, cloudsigma.NewServerContextService())
|
||||
}
|
||||
if flags.sources.digitalOceanMetadataService != "" {
|
||||
dss = append(dss, digitalocean.NewDatasource(flags.sources.digitalOceanMetadataService))
|
||||
}
|
||||
if flags.sources.procCmdLine {
|
||||
dss = append(dss, proc_cmdline.NewDatasource())
|
||||
}
|
||||
return dss
|
||||
}
|
||||
@@ -240,7 +312,7 @@ func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-time.Tick(duration):
|
||||
case <-time.After(duration):
|
||||
duration = pkg.ExpBackoff(duration, datasourceMaxInterval)
|
||||
}
|
||||
}
|
||||
@@ -257,7 +329,7 @@ func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
||||
select {
|
||||
case s = <-ds:
|
||||
case <-done:
|
||||
case <-time.Tick(datasourceTimeout):
|
||||
case <-time.After(datasourceTimeout):
|
||||
}
|
||||
|
||||
close(stop)
|
||||
|
@@ -4,101 +4,111 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/initialize"
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestMergeCloudConfig(t *testing.T) {
|
||||
simplecc := initialize.CloudConfig{
|
||||
simplecc := config.CloudConfig{
|
||||
SSHAuthorizedKeys: []string{"abc", "def"},
|
||||
Hostname: "foobar",
|
||||
NetworkConfigPath: "/path/somewhere",
|
||||
NetworkConfig: `{}`,
|
||||
}
|
||||
for i, tt := range []struct {
|
||||
udcc initialize.CloudConfig
|
||||
mdcc initialize.CloudConfig
|
||||
want initialize.CloudConfig
|
||||
udcc config.CloudConfig
|
||||
mdcc config.CloudConfig
|
||||
want config.CloudConfig
|
||||
}{
|
||||
{
|
||||
// If mdcc is empty, udcc should be returned unchanged
|
||||
simplecc,
|
||||
initialize.CloudConfig{},
|
||||
config.CloudConfig{},
|
||||
simplecc,
|
||||
},
|
||||
{
|
||||
// If udcc is empty, mdcc should be returned unchanged(overridden)
|
||||
initialize.CloudConfig{},
|
||||
config.CloudConfig{},
|
||||
simplecc,
|
||||
simplecc,
|
||||
},
|
||||
{
|
||||
// user-data should override completely in the case of conflicts
|
||||
simplecc,
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
Hostname: "meta-hostname",
|
||||
NetworkConfigPath: "/path/meta",
|
||||
NetworkConfig: `{"hostname":"test"}`,
|
||||
},
|
||||
simplecc,
|
||||
},
|
||||
{
|
||||
// Mixed merge should succeed
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
SSHAuthorizedKeys: []string{"abc", "def"},
|
||||
Hostname: "user-hostname",
|
||||
NetworkConfigPath: "/path/somewhere",
|
||||
NetworkConfig: `{"hostname":"test"}`,
|
||||
},
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
SSHAuthorizedKeys: []string{"woof", "qux"},
|
||||
Hostname: "meta-hostname",
|
||||
},
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
SSHAuthorizedKeys: []string{"abc", "def", "woof", "qux"},
|
||||
Hostname: "user-hostname",
|
||||
NetworkConfigPath: "/path/somewhere",
|
||||
NetworkConfig: `{"hostname":"test"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Completely non-conflicting merge should be fine
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
Hostname: "supercool",
|
||||
},
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"},
|
||||
NetworkConfigPath: "/dev/fun",
|
||||
NetworkConfig: `{"hostname":"test"}`,
|
||||
},
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
Hostname: "supercool",
|
||||
SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"},
|
||||
NetworkConfigPath: "/dev/fun",
|
||||
NetworkConfig: `{"hostname":"test"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Non-mergeable settings in user-data should not be affected
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
Hostname: "mememe",
|
||||
ManageEtcHosts: initialize.EtcHosts("lolz"),
|
||||
ManageEtcHosts: config.EtcHosts("lolz"),
|
||||
},
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
Hostname: "youyouyou",
|
||||
NetworkConfigPath: "meta-meta-yo",
|
||||
NetworkConfig: `{"hostname":"test"}`,
|
||||
},
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
Hostname: "mememe",
|
||||
ManageEtcHosts: initialize.EtcHosts("lolz"),
|
||||
ManageEtcHosts: config.EtcHosts("lolz"),
|
||||
NetworkConfigPath: "meta-meta-yo",
|
||||
NetworkConfig: `{"hostname":"test"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Non-mergeable (unexpected) settings in meta-data are ignored
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
Hostname: "mememe",
|
||||
},
|
||||
initialize.CloudConfig{
|
||||
ManageEtcHosts: initialize.EtcHosts("lolz"),
|
||||
config.CloudConfig{
|
||||
ManageEtcHosts: config.EtcHosts("lolz"),
|
||||
NetworkConfigPath: "meta-meta-yo",
|
||||
NetworkConfig: `{"hostname":"test"}`,
|
||||
},
|
||||
initialize.CloudConfig{
|
||||
config.CloudConfig{
|
||||
Hostname: "mememe",
|
||||
NetworkConfigPath: "meta-meta-yo",
|
||||
NetworkConfig: `{"hostname":"test"}`,
|
||||
},
|
||||
},
|
||||
} {
|
||||
|
@@ -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
|
||||
}
|
70
datasource/configdrive/configdrive.go
Normal file
70
datasource/configdrive/configdrive.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package configdrive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
openstackApiVersion = "latest"
|
||||
)
|
||||
|
||||
type configDrive struct {
|
||||
root string
|
||||
readFile func(filename string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *configDrive {
|
||||
return &configDrive{root, ioutil.ReadFile}
|
||||
}
|
||||
|
||||
func (cd *configDrive) IsAvailable() bool {
|
||||
_, err := os.Stat(cd.root)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func (cd *configDrive) AvailabilityChanges() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (cd *configDrive) ConfigRoot() string {
|
||||
return cd.openstackRoot()
|
||||
}
|
||||
|
||||
func (cd *configDrive) FetchMetadata() ([]byte, error) {
|
||||
return cd.tryReadFile(path.Join(cd.openstackVersionRoot(), "meta_data.json"))
|
||||
}
|
||||
|
||||
func (cd *configDrive) FetchUserdata() ([]byte, error) {
|
||||
return cd.tryReadFile(path.Join(cd.openstackVersionRoot(), "user_data"))
|
||||
}
|
||||
|
||||
func (cd *configDrive) FetchNetworkConfig(filename string) ([]byte, error) {
|
||||
if filename == "" {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return cd.tryReadFile(path.Join(cd.openstackRoot(), filename))
|
||||
}
|
||||
|
||||
func (cd *configDrive) Type() string {
|
||||
return "cloud-drive"
|
||||
}
|
||||
|
||||
func (cd *configDrive) openstackRoot() string {
|
||||
return path.Join(cd.root, "openstack")
|
||||
}
|
||||
|
||||
func (cd *configDrive) openstackVersionRoot() string {
|
||||
return path.Join(cd.openstackRoot(), openstackApiVersion)
|
||||
}
|
||||
|
||||
func (cd *configDrive) tryReadFile(filename string) ([]byte, error) {
|
||||
fmt.Printf("Attempting to read from %q\n", filename)
|
||||
data, err := cd.readFile(filename)
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
}
|
||||
return data, err
|
||||
}
|
125
datasource/configdrive/configdrive_test.go
Normal file
125
datasource/configdrive/configdrive_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package configdrive
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockFilesystem []string
|
||||
|
||||
func (m mockFilesystem) readFile(filename string) ([]byte, error) {
|
||||
for _, file := range m {
|
||||
if file == filename {
|
||||
return []byte(filename), nil
|
||||
}
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func TestFetchMetadata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
filename string
|
||||
files mockFilesystem
|
||||
}{
|
||||
{
|
||||
"/",
|
||||
"",
|
||||
mockFilesystem{},
|
||||
},
|
||||
{
|
||||
"/",
|
||||
"/openstack/latest/meta_data.json",
|
||||
mockFilesystem([]string{"/openstack/latest/meta_data.json"}),
|
||||
},
|
||||
{
|
||||
"/media/configdrive",
|
||||
"/media/configdrive/openstack/latest/meta_data.json",
|
||||
mockFilesystem([]string{"/media/configdrive/openstack/latest/meta_data.json"}),
|
||||
},
|
||||
} {
|
||||
cd := configDrive{tt.root, tt.files.readFile}
|
||||
filename, err := cd.FetchMetadata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %q: want %q, got %q", tt, nil, err)
|
||||
}
|
||||
if string(filename) != tt.filename {
|
||||
t.Fatalf("bad path for %q: want %q, got %q", tt, tt.filename, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchUserdata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
filename string
|
||||
files mockFilesystem
|
||||
}{
|
||||
{
|
||||
"/",
|
||||
"",
|
||||
mockFilesystem{},
|
||||
},
|
||||
{
|
||||
"/",
|
||||
"/openstack/latest/user_data",
|
||||
mockFilesystem([]string{"/openstack/latest/user_data"}),
|
||||
},
|
||||
{
|
||||
"/media/configdrive",
|
||||
"/media/configdrive/openstack/latest/user_data",
|
||||
mockFilesystem([]string{"/media/configdrive/openstack/latest/user_data"}),
|
||||
},
|
||||
} {
|
||||
cd := configDrive{tt.root, tt.files.readFile}
|
||||
filename, err := cd.FetchUserdata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %q: want %q, got %q", tt, nil, err)
|
||||
}
|
||||
if string(filename) != tt.filename {
|
||||
t.Fatalf("bad path for %q: want %q, got %q", tt, tt.filename, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigRoot(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
configRoot string
|
||||
}{
|
||||
{
|
||||
"/",
|
||||
"/openstack",
|
||||
},
|
||||
{
|
||||
"/media/configdrive",
|
||||
"/media/configdrive/openstack",
|
||||
},
|
||||
} {
|
||||
cd := configDrive{tt.root, nil}
|
||||
if configRoot := cd.ConfigRoot(); configRoot != tt.configRoot {
|
||||
t.Fatalf("bad config root for %q: want %q, got %q", tt, tt.configRoot, configRoot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDatasource(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
expectRoot string
|
||||
}{
|
||||
{
|
||||
root: "",
|
||||
expectRoot: "",
|
||||
},
|
||||
{
|
||||
root: "/media/configdrive",
|
||||
expectRoot: "/media/configdrive",
|
||||
},
|
||||
} {
|
||||
service := NewDatasource(tt.root)
|
||||
if service.root != tt.expectRoot {
|
||||
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,5 +6,6 @@ type Datasource interface {
|
||||
ConfigRoot() string
|
||||
FetchMetadata() ([]byte, error)
|
||||
FetchUserdata() ([]byte, error)
|
||||
FetchNetworkConfig(string) ([]byte, error)
|
||||
Type() string
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package datasource
|
||||
package file
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
@@ -9,7 +9,7 @@ type localFile struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func NewLocalFile(path string) *localFile {
|
||||
func NewDatasource(path string) *localFile {
|
||||
return &localFile{path}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ func (f *localFile) FetchUserdata() ([]byte, error) {
|
||||
return ioutil.ReadFile(f.path)
|
||||
}
|
||||
|
||||
func (f *localFile) FetchNetworkConfig(filename string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *localFile) Type() string {
|
||||
return "local-file"
|
||||
}
|
145
datasource/metadata/cloudsigma/server_context.go
Normal file
145
datasource/metadata/cloudsigma/server_context.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package cloudsigma
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/cloudsigma/cepgo"
|
||||
)
|
||||
|
||||
const (
|
||||
userDataFieldName = "cloudinit-user-data"
|
||||
)
|
||||
|
||||
type serverContextService struct {
|
||||
client interface {
|
||||
All() (interface{}, error)
|
||||
Key(string) (interface{}, error)
|
||||
Meta() (map[string]string, error)
|
||||
FetchRaw(string) ([]byte, error)
|
||||
}
|
||||
}
|
||||
|
||||
func NewServerContextService() *serverContextService {
|
||||
return &serverContextService{
|
||||
client: cepgo.NewCepgo(),
|
||||
}
|
||||
}
|
||||
|
||||
func (_ *serverContextService) IsAvailable() bool {
|
||||
productNameFile, err := os.Open("/sys/class/dmi/id/product_name")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
productName := make([]byte, 10)
|
||||
_, err = productNameFile.Read(productName)
|
||||
return err == nil && string(productName) == "CloudSigma"
|
||||
}
|
||||
|
||||
func (_ *serverContextService) AvailabilityChanges() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (_ *serverContextService) ConfigRoot() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (_ *serverContextService) Type() string {
|
||||
return "server-context"
|
||||
}
|
||||
|
||||
func (scs *serverContextService) FetchMetadata() ([]byte, error) {
|
||||
var (
|
||||
inputMetadata struct {
|
||||
Name string `json:"name"`
|
||||
UUID string `json:"uuid"`
|
||||
Meta map[string]string `json:"meta"`
|
||||
Nics []struct {
|
||||
Runtime struct {
|
||||
InterfaceType string `json:"interface_type"`
|
||||
IPv4 struct {
|
||||
IP string `json:"uuid"`
|
||||
} `json:"ip_v4"`
|
||||
} `json:"runtime"`
|
||||
} `json:"nics"`
|
||||
}
|
||||
outputMetadata struct {
|
||||
Hostname string `json:"name"`
|
||||
PublicKeys map[string]string `json:"public_keys"`
|
||||
LocalIPv4 string `json:"local-ipv4"`
|
||||
PublicIPv4 string `json:"public-ipv4"`
|
||||
}
|
||||
)
|
||||
|
||||
rawMetadata, err := scs.client.FetchRaw("")
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(rawMetadata, &inputMetadata)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
if inputMetadata.Name != "" {
|
||||
outputMetadata.Hostname = inputMetadata.Name
|
||||
} else {
|
||||
outputMetadata.Hostname = inputMetadata.UUID
|
||||
}
|
||||
|
||||
if key, ok := inputMetadata.Meta["ssh_public_key"]; ok {
|
||||
splitted := strings.Split(key, " ")
|
||||
outputMetadata.PublicKeys = make(map[string]string)
|
||||
outputMetadata.PublicKeys[splitted[len(splitted)-1]] = key
|
||||
}
|
||||
|
||||
for _, nic := range inputMetadata.Nics {
|
||||
if nic.Runtime.IPv4.IP != "" {
|
||||
if nic.Runtime.InterfaceType == "public" {
|
||||
outputMetadata.PublicIPv4 = nic.Runtime.IPv4.IP
|
||||
} else {
|
||||
outputMetadata.LocalIPv4 = nic.Runtime.IPv4.IP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(outputMetadata)
|
||||
}
|
||||
|
||||
func (scs *serverContextService) FetchUserdata() ([]byte, error) {
|
||||
metadata, err := scs.client.Meta()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
userData, ok := metadata[userDataFieldName]
|
||||
if ok && isBase64Encoded(userDataFieldName, metadata) {
|
||||
if decodedUserData, err := base64.StdEncoding.DecodeString(userData); err == nil {
|
||||
return decodedUserData, nil
|
||||
} else {
|
||||
return []byte{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(userData), nil
|
||||
}
|
||||
|
||||
func (scs *serverContextService) FetchNetworkConfig(a string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func isBase64Encoded(field string, userdata map[string]string) bool {
|
||||
base64Fields, ok := userdata["base64_fields"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, base64Field := range strings.Split(base64Fields, ",") {
|
||||
if field == base64Field {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
152
datasource/metadata/cloudsigma/server_context_test.go
Normal file
152
datasource/metadata/cloudsigma/server_context_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package cloudsigma
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeCepgoClient struct {
|
||||
raw []byte
|
||||
meta map[string]string
|
||||
keys map[string]interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeCepgoClient) All() (interface{}, error) {
|
||||
return f.keys, f.err
|
||||
}
|
||||
|
||||
func (f *fakeCepgoClient) Key(key string) (interface{}, error) {
|
||||
return f.keys[key], f.err
|
||||
}
|
||||
|
||||
func (f *fakeCepgoClient) Meta() (map[string]string, error) {
|
||||
return f.meta, f.err
|
||||
}
|
||||
|
||||
func (f *fakeCepgoClient) FetchRaw(key string) ([]byte, error) {
|
||||
return f.raw, f.err
|
||||
}
|
||||
|
||||
func TestServerContextFetchMetadata(t *testing.T) {
|
||||
var metadata struct {
|
||||
Hostname string `json:"name"`
|
||||
PublicKeys map[string]string `json:"public_keys"`
|
||||
LocalIPv4 string `json:"local-ipv4"`
|
||||
PublicIPv4 string `json:"public-ipv4"`
|
||||
}
|
||||
client := new(fakeCepgoClient)
|
||||
scs := NewServerContextService()
|
||||
scs.client = client
|
||||
client.raw = []byte(`{
|
||||
"context": true,
|
||||
"cpu": 4000,
|
||||
"cpu_model": null,
|
||||
"cpus_instead_of_cores": false,
|
||||
"enable_numa": false,
|
||||
"grantees": [],
|
||||
"hv_relaxed": false,
|
||||
"hv_tsc": false,
|
||||
"jobs": [],
|
||||
"mem": 4294967296,
|
||||
"meta": {
|
||||
"base64_fields": "cloudinit-user-data",
|
||||
"cloudinit-user-data": "I2Nsb3VkLWNvbmZpZwoKaG9zdG5hbWU6IGNvcmVvczE=",
|
||||
"ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2E.../hQ5D5 john@doe"
|
||||
},
|
||||
"name": "coreos",
|
||||
"nics": [
|
||||
{
|
||||
"runtime": {
|
||||
"interface_type": "public",
|
||||
"ip_v4": {
|
||||
"uuid": "31.171.251.74"
|
||||
},
|
||||
"ip_v6": null
|
||||
},
|
||||
"vlan": null
|
||||
}
|
||||
],
|
||||
"smp": 2,
|
||||
"status": "running",
|
||||
"uuid": "20a0059b-041e-4d0c-bcc6-9b2852de48b3"
|
||||
}`)
|
||||
|
||||
metadataBytes, err := scs.FetchMetadata()
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if metadata.Hostname != "coreos" {
|
||||
t.Errorf("Hostname is not 'coreos' but %s instead", metadata.Hostname)
|
||||
}
|
||||
|
||||
if metadata.PublicKeys["john@doe"] != "ssh-rsa AAAAB3NzaC1yc2E.../hQ5D5 john@doe" {
|
||||
t.Error("Public SSH Keys are not being read properly")
|
||||
}
|
||||
|
||||
if metadata.LocalIPv4 != "" {
|
||||
t.Errorf("Local IP is not empty but %s instead", metadata.LocalIPv4)
|
||||
}
|
||||
|
||||
if metadata.PublicIPv4 != "31.171.251.74" {
|
||||
t.Errorf("Local IP is not 31.171.251.74 but %s instead", metadata.PublicIPv4)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerContextFetchUserdata(t *testing.T) {
|
||||
client := new(fakeCepgoClient)
|
||||
scs := NewServerContextService()
|
||||
scs.client = client
|
||||
userdataSets := []struct {
|
||||
in map[string]string
|
||||
err bool
|
||||
out []byte
|
||||
}{
|
||||
{map[string]string{
|
||||
"base64_fields": "cloudinit-user-data",
|
||||
"cloudinit-user-data": "aG9zdG5hbWU6IGNvcmVvc190ZXN0",
|
||||
}, false, []byte("hostname: coreos_test")},
|
||||
{map[string]string{
|
||||
"cloudinit-user-data": "#cloud-config\\nhostname: coreos1",
|
||||
}, false, []byte("#cloud-config\\nhostname: coreos1")},
|
||||
{map[string]string{}, false, []byte{}},
|
||||
}
|
||||
|
||||
for i, set := range userdataSets {
|
||||
client.meta = set.in
|
||||
got, err := scs.FetchUserdata()
|
||||
if (err != nil) != set.err {
|
||||
t.Errorf("case %d: bad error state (got %t, want %t)", i, err != nil, set.err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, set.out) {
|
||||
t.Errorf("case %d: got %s, want %s", i, got, set.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerContextDecodingBase64UserData(t *testing.T) {
|
||||
base64Sets := []struct {
|
||||
in string
|
||||
out bool
|
||||
}{
|
||||
{"cloudinit-user-data,foo,bar", true},
|
||||
{"bar,cloudinit-user-data,foo,bar", true},
|
||||
{"cloudinit-user-data", true},
|
||||
{"", false},
|
||||
{"foo", false},
|
||||
}
|
||||
|
||||
for _, set := range base64Sets {
|
||||
userdata := map[string]string{"base64_fields": set.in}
|
||||
if isBase64Encoded("cloudinit-user-data", userdata) != set.out {
|
||||
t.Errorf("isBase64Encoded(cloudinit-user-data, %s) should be %t", userdata, set.out)
|
||||
}
|
||||
}
|
||||
}
|
107
datasource/metadata/digitalocean/metadata.go
Normal file
107
datasource/metadata/digitalocean/metadata.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAddress = "http://169.254.169.254/"
|
||||
apiVersion = "metadata/v1"
|
||||
userdataUrl = apiVersion + "/user-data"
|
||||
metadataPath = apiVersion + ".json"
|
||||
)
|
||||
|
||||
type Address struct {
|
||||
IPAddress string `json:"ip_address"`
|
||||
Netmask string `json:"netmask"`
|
||||
Cidr int `json:"cidr"`
|
||||
Gateway string `json:"gateway"`
|
||||
}
|
||||
|
||||
type Interface struct {
|
||||
IPv4 *Address `json:"ipv4"`
|
||||
IPv6 *Address `json:"ipv6"`
|
||||
MAC string `json:"mac"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Interfaces struct {
|
||||
Public []Interface `json:"public"`
|
||||
Private []Interface `json:"private"`
|
||||
}
|
||||
|
||||
type DNS struct {
|
||||
Nameservers []string `json:"nameservers"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Interfaces Interfaces `json:"interfaces"`
|
||||
PublicKeys []string `json:"public_keys"`
|
||||
DNS DNS `json:"dns"`
|
||||
}
|
||||
|
||||
type metadataService struct {
|
||||
interfaces Interfaces
|
||||
dns DNS
|
||||
metadata.MetadataService
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *metadataService {
|
||||
return &metadataService{MetadataService: metadata.NewDatasource(root, apiVersion, userdataUrl, metadataPath)}
|
||||
}
|
||||
|
||||
func (ms *metadataService) FetchMetadata() ([]byte, error) {
|
||||
data, err := ms.FetchData(ms.MetadataUrl())
|
||||
if err != nil || len(data) == 0 {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
var metadata Metadata
|
||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
ms.interfaces = metadata.Interfaces
|
||||
ms.dns = metadata.DNS
|
||||
|
||||
attrs := make(map[string]interface{})
|
||||
if len(metadata.Interfaces.Public) > 0 {
|
||||
if metadata.Interfaces.Public[0].IPv4 != nil {
|
||||
attrs["public-ipv4"] = metadata.Interfaces.Public[0].IPv4.IPAddress
|
||||
}
|
||||
if metadata.Interfaces.Public[0].IPv6 != nil {
|
||||
attrs["public-ipv6"] = metadata.Interfaces.Public[0].IPv6.IPAddress
|
||||
}
|
||||
}
|
||||
if len(metadata.Interfaces.Private) > 0 {
|
||||
if metadata.Interfaces.Private[0].IPv4 != nil {
|
||||
attrs["local-ipv4"] = metadata.Interfaces.Private[0].IPv4.IPAddress
|
||||
}
|
||||
if metadata.Interfaces.Private[0].IPv6 != nil {
|
||||
attrs["local-ipv6"] = metadata.Interfaces.Private[0].IPv6.IPAddress
|
||||
}
|
||||
}
|
||||
attrs["hostname"] = metadata.Hostname
|
||||
keys := make(map[string]string)
|
||||
for i, key := range metadata.PublicKeys {
|
||||
keys[strconv.Itoa(i)] = key
|
||||
}
|
||||
attrs["public_keys"] = keys
|
||||
|
||||
return json.Marshal(attrs)
|
||||
}
|
||||
|
||||
func (ms metadataService) FetchNetworkConfig(filename string) ([]byte, error) {
|
||||
return json.Marshal(Metadata{
|
||||
Interfaces: ms.interfaces,
|
||||
DNS: ms.dns,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms metadataService) Type() string {
|
||||
return "digitalocean-metadata-service"
|
||||
}
|
99
datasource/metadata/digitalocean/metadata_test.go
Normal file
99
datasource/metadata/digitalocean/metadata_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/test"
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
want := "digitalocean-metadata-service"
|
||||
if kind := (metadataService{}).Type(); kind != want {
|
||||
t.Fatalf("bad type: want %q, got %q", want, kind)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchMetadata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
metadataPath string
|
||||
resources map[string]string
|
||||
expect []byte
|
||||
clientErr error
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
metadataPath: "v1.json",
|
||||
resources: map[string]string{
|
||||
"/v1.json": "bad",
|
||||
},
|
||||
expectErr: fmt.Errorf("invalid character 'b' looking for beginning of value"),
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
metadataPath: "v1.json",
|
||||
resources: map[string]string{
|
||||
"/v1.json": `{
|
||||
"droplet_id": 1,
|
||||
"user_data": "hello",
|
||||
"vendor_data": "hello",
|
||||
"public_keys": [
|
||||
"publickey1",
|
||||
"publickey2"
|
||||
],
|
||||
"region": "nyc2",
|
||||
"interfaces": {
|
||||
"public": [
|
||||
{
|
||||
"ipv4": {
|
||||
"ip_address": "192.168.1.2",
|
||||
"netmask": "255.255.255.0",
|
||||
"gateway": "192.168.1.1"
|
||||
},
|
||||
"ipv6": {
|
||||
"ip_address": "fe00::",
|
||||
"cidr": 126,
|
||||
"gateway": "fe00::"
|
||||
},
|
||||
"mac": "ab:cd:ef:gh:ij",
|
||||
"type": "public"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expect: []byte(`{"hostname":"","public-ipv4":"192.168.1.2","public-ipv6":"fe00::","public_keys":{"0":"publickey1","1":"publickey2"}}`),
|
||||
},
|
||||
{
|
||||
clientErr: pkg.ErrTimeout{fmt.Errorf("test error")},
|
||||
expectErr: pkg.ErrTimeout{fmt.Errorf("test error")},
|
||||
},
|
||||
} {
|
||||
service := &metadataService{
|
||||
MetadataService: metadata.MetadataService{
|
||||
Root: tt.root,
|
||||
Client: &test.HttpClient{tt.resources, tt.clientErr},
|
||||
MetadataPath: tt.metadataPath,
|
||||
},
|
||||
}
|
||||
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 Error(err error) string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
107
datasource/metadata/ec2/metadata.go
Normal file
107
datasource/metadata/ec2/metadata.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package ec2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata"
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAddress = "http://169.254.169.254/"
|
||||
apiVersion = "2009-04-04/"
|
||||
userdataPath = apiVersion + "user-data"
|
||||
metadataPath = apiVersion + "meta-data"
|
||||
)
|
||||
|
||||
type metadataService struct {
|
||||
metadata.MetadataService
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *metadataService {
|
||||
return &metadataService{metadata.NewDatasource(root, apiVersion, userdataPath, metadataPath)}
|
||||
}
|
||||
|
||||
func (ms metadataService) FetchMetadata() ([]byte, error) {
|
||||
attrs := make(map[string]interface{})
|
||||
if keynames, err := ms.fetchAttributes(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 := ms.fetchAttribute(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 := ms.fetchAttribute(fmt.Sprintf("%s/hostname", ms.MetadataUrl())); err == nil {
|
||||
attrs["hostname"] = hostname
|
||||
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if localAddr, err := ms.fetchAttribute(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 := ms.fetchAttribute(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 := ms.fetchAttribute(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) Type() string {
|
||||
return "ec2-metadata-service"
|
||||
}
|
||||
|
||||
func (ms metadataService) fetchAttributes(url string) ([]string, error) {
|
||||
resp, err := ms.FetchData(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 (ms metadataService) fetchAttribute(url string) (string, error) {
|
||||
if attrs, err := ms.fetchAttributes(url); err == nil && len(attrs) > 0 {
|
||||
return attrs[0], nil
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
185
datasource/metadata/ec2/metadata_test.go
Normal file
185
datasource/metadata/ec2/metadata_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package ec2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/test"
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
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 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: fmt.Errorf("test error"),
|
||||
tests: []struct {
|
||||
path string
|
||||
val []string
|
||||
}{
|
||||
{"", nil},
|
||||
},
|
||||
},
|
||||
} {
|
||||
service := metadataService{metadata.MetadataService{
|
||||
Client: &test.HttpClient{s.resources, s.err},
|
||||
}}
|
||||
for _, tt := range s.tests {
|
||||
attrs, err := service.fetchAttributes(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: fmt.Errorf("test error"),
|
||||
tests: []struct {
|
||||
path string
|
||||
val string
|
||||
}{
|
||||
{"", ""},
|
||||
},
|
||||
},
|
||||
} {
|
||||
service := metadataService{metadata.MetadataService{
|
||||
Client: &test.HttpClient{s.resources, s.err},
|
||||
}}
|
||||
for _, tt := range s.tests {
|
||||
attr, err := service.fetchAttribute(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
|
||||
metadataPath string
|
||||
resources map[string]string
|
||||
expect []byte
|
||||
clientErr error
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
metadataPath: "2009-04-04/meta-data",
|
||||
resources: map[string]string{
|
||||
"/2009-04-04/meta-data/public-keys": "bad\n",
|
||||
},
|
||||
expectErr: fmt.Errorf("malformed public key: \"bad\""),
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
metadataPath: "2009-04-04/meta-data",
|
||||
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{metadata.MetadataService{
|
||||
Root: tt.root,
|
||||
Client: &test.HttpClient{tt.resources, tt.clientErr},
|
||||
MetadataPath: tt.metadataPath,
|
||||
}}
|
||||
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 Error(err error) string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
61
datasource/metadata/metadata.go
Normal file
61
datasource/metadata/metadata.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
type MetadataService struct {
|
||||
Root string
|
||||
Client pkg.Getter
|
||||
ApiVersion string
|
||||
UserdataPath string
|
||||
MetadataPath string
|
||||
}
|
||||
|
||||
func NewDatasource(root, apiVersion, userdataPath, metadataPath string) MetadataService {
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root += "/"
|
||||
}
|
||||
return MetadataService{root, pkg.NewHttpClient(), apiVersion, userdataPath, metadataPath}
|
||||
}
|
||||
|
||||
func (ms MetadataService) IsAvailable() bool {
|
||||
_, err := ms.Client.Get(ms.Root + ms.ApiVersion)
|
||||
return (err == nil)
|
||||
}
|
||||
|
||||
func (ms MetadataService) AvailabilityChanges() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ms MetadataService) ConfigRoot() string {
|
||||
return ms.Root
|
||||
}
|
||||
|
||||
func (ms MetadataService) FetchUserdata() ([]byte, error) {
|
||||
return ms.FetchData(ms.UserdataUrl())
|
||||
}
|
||||
|
||||
func (ms MetadataService) FetchNetworkConfig(filename string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (ms MetadataService) FetchData(url string) ([]byte, error) {
|
||||
if data, err := ms.Client.GetRetry(url); err == nil {
|
||||
return data, err
|
||||
} else if _, ok := err.(pkg.ErrNotFound); ok {
|
||||
return []byte{}, nil
|
||||
} else {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
func (ms MetadataService) MetadataUrl() string {
|
||||
return (ms.Root + ms.MetadataPath)
|
||||
}
|
||||
|
||||
func (ms MetadataService) UserdataUrl() string {
|
||||
return (ms.Root + ms.UserdataPath)
|
||||
}
|
171
datasource/metadata/metadata_test.go
Normal file
171
datasource/metadata/metadata_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/test"
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
func TestAvailabilityChanges(t *testing.T) {
|
||||
want := true
|
||||
if ac := (MetadataService{}).AvailabilityChanges(); ac != want {
|
||||
t.Fatalf("bad AvailabilityChanges: want %q, got %q", want, ac)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAvailable(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
apiVersion string
|
||||
resources map[string]string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
apiVersion: "2009-04-04",
|
||||
resources: map[string]string{
|
||||
"/2009-04-04": "",
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
resources: map[string]string{},
|
||||
expect: false,
|
||||
},
|
||||
} {
|
||||
service := &MetadataService{
|
||||
Root: tt.root,
|
||||
Client: &test.HttpClient{tt.resources, nil},
|
||||
ApiVersion: tt.apiVersion,
|
||||
}
|
||||
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
|
||||
userdataPath string
|
||||
resources map[string]string
|
||||
userdata []byte
|
||||
clientErr error
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
userdataPath: "2009-04-04/user-data",
|
||||
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{
|
||||
Root: tt.root,
|
||||
Client: &test.HttpClient{tt.resources, tt.clientErr},
|
||||
UserdataPath: tt.userdataPath,
|
||||
}
|
||||
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
|
||||
userdataPath string
|
||||
metadataPath string
|
||||
expectRoot string
|
||||
userdata string
|
||||
metadata string
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
userdataPath: "2009-04-04/user-data",
|
||||
metadataPath: "2009-04-04/meta-data",
|
||||
expectRoot: "/",
|
||||
userdata: "/2009-04-04/user-data",
|
||||
metadata: "/2009-04-04/meta-data",
|
||||
},
|
||||
{
|
||||
root: "http://169.254.169.254/",
|
||||
userdataPath: "2009-04-04/user-data",
|
||||
metadataPath: "2009-04-04/meta-data",
|
||||
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{
|
||||
Root: tt.root,
|
||||
UserdataPath: tt.userdataPath,
|
||||
MetadataPath: tt.metadataPath,
|
||||
}
|
||||
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 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 ""
|
||||
}
|
27
datasource/metadata/test/test.go
Normal file
27
datasource/metadata/test/test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
type HttpClient struct {
|
||||
Resources map[string]string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (t *HttpClient) 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 *HttpClient) Get(url string) ([]byte, error) {
|
||||
return t.GetRetry(url)
|
||||
}
|
@@ -1,155 +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 localAddr, err := fetchAttribute(client, fmt.Sprintf("%s/local-ipv4", Ec2MetadataUrl)); err == nil {
|
||||
attrs["local-ipv4"] = localAddr
|
||||
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if publicAddr, err := fetchAttribute(client, fmt.Sprintf("%s/public-ipv4", Ec2MetadataUrl)); err == nil {
|
||||
attrs["public-ipv4"] = publicAddr
|
||||
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if content_path, err := fetchAttribute(client, fmt.Sprintf("%s/network_config/content_path", Ec2MetadataUrl)); err == nil {
|
||||
attrs["network_config"] = map[string]string{
|
||||
"content_path": content_path,
|
||||
}
|
||||
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(attrs)
|
||||
}
|
||||
|
||||
func fetchAttributes(client getter, url string) ([]string, error) {
|
||||
resp, err := client.GetRetry(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scanner := bufio.NewScanner(bytes.NewBuffer(resp))
|
||||
data := make([]string, 0)
|
||||
for scanner.Scan() {
|
||||
data = append(data, scanner.Text())
|
||||
}
|
||||
return data, scanner.Err()
|
||||
}
|
||||
|
||||
func fetchAttribute(client getter, url string) (string, error) {
|
||||
if attrs, err := fetchAttributes(client, url); err == nil && len(attrs) > 0 {
|
||||
return attrs[0], nil
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
@@ -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 (
|
||||
"errors"
|
||||
@@ -18,7 +18,7 @@ type procCmdline struct {
|
||||
Location string
|
||||
}
|
||||
|
||||
func NewProcCmdline() *procCmdline {
|
||||
func NewDatasource() *procCmdline {
|
||||
return &procCmdline{Location: ProcCmdlineLocation}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ func (c *procCmdline) FetchUserdata() ([]byte, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c *procCmdline) FetchNetworkConfig(filename string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *procCmdline) Type() string {
|
||||
return "proc-cmdline"
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package datasource
|
||||
package proc_cmdline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -75,7 +75,7 @@ func TestProcCmdlineAndFetchConfig(t *testing.T) {
|
||||
t.Errorf("Test produced error: %v", err)
|
||||
}
|
||||
|
||||
p := NewProcCmdline()
|
||||
p := NewDatasource()
|
||||
p.Location = file.Name()
|
||||
cfg, err := p.FetchUserdata()
|
||||
if err != nil {
|
@@ -1,12 +1,14 @@
|
||||
package datasource
|
||||
package url
|
||||
|
||||
import "github.com/coreos/coreos-cloudinit/pkg"
|
||||
import (
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
type remoteFile struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func NewRemoteFile(url string) *remoteFile {
|
||||
func NewDatasource(url string) *remoteFile {
|
||||
return &remoteFile{url}
|
||||
}
|
||||
|
||||
@@ -33,6 +35,10 @@ func (f *remoteFile) FetchUserdata() ([]byte, error) {
|
||||
return client.GetRetry(f.url)
|
||||
}
|
||||
|
||||
func (f *remoteFile) FetchNetworkConfig(filename string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *remoteFile) Type() string {
|
||||
return "url"
|
||||
}
|
@@ -3,12 +3,10 @@ package initialize
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/network"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
@@ -18,134 +16,19 @@ import (
|
||||
type CloudConfigFile interface {
|
||||
// File should either return (*system.File, error), or (nil, nil) if nothing
|
||||
// needs to be done for this configuration option.
|
||||
File(root string) (*system.File, error)
|
||||
File() (*system.File, error)
|
||||
}
|
||||
|
||||
// CloudConfigUnit represents a CoreOS specific configuration option that can generate
|
||||
// associated system.Units to be created/enabled appropriately
|
||||
type CloudConfigUnit interface {
|
||||
Units(root string) ([]system.Unit, error)
|
||||
}
|
||||
|
||||
// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
|
||||
type CloudConfig struct {
|
||||
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
|
||||
Coreos struct {
|
||||
Etcd EtcdEnvironment
|
||||
Fleet FleetEnvironment
|
||||
OEM OEMRelease
|
||||
Update UpdateConfig
|
||||
Units []system.Unit
|
||||
}
|
||||
WriteFiles []system.File `yaml:"write_files"`
|
||||
Hostname string
|
||||
Users []system.User
|
||||
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
|
||||
NetworkConfigPath string
|
||||
}
|
||||
|
||||
type warner func(format string, v ...interface{})
|
||||
|
||||
// warnOnUnrecognizedKeys parses the contents of a cloud-config file and calls
|
||||
// warn(msg, key) for every unrecognized key (i.e. those not present in CloudConfig)
|
||||
func warnOnUnrecognizedKeys(contents string, warn warner) {
|
||||
// Generate a map of all understood cloud config options
|
||||
var cc map[string]interface{}
|
||||
b, _ := goyaml.Marshal(&CloudConfig{})
|
||||
goyaml.Unmarshal(b, &cc)
|
||||
|
||||
// Now unmarshal the entire provided contents
|
||||
var c map[string]interface{}
|
||||
goyaml.Unmarshal([]byte(contents), &c)
|
||||
|
||||
// Check that every key in the contents exists in the cloud config
|
||||
for k, _ := range c {
|
||||
if _, ok := cc[k]; !ok {
|
||||
warn("Warning: unrecognized key %q in provided cloud config - ignoring section", k)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unrecognized coreos options, if any are set
|
||||
coreos, ok := c["coreos"]
|
||||
if ok {
|
||||
set := coreos.(map[interface{}]interface{})
|
||||
known := cc["coreos"].(map[interface{}]interface{})
|
||||
for k, _ := range set {
|
||||
key := k.(string)
|
||||
if _, ok := known[key]; !ok {
|
||||
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any badly-specified users, if any are set
|
||||
users, ok := c["users"]
|
||||
if ok {
|
||||
var known map[string]interface{}
|
||||
b, _ := goyaml.Marshal(&system.User{})
|
||||
goyaml.Unmarshal(b, &known)
|
||||
|
||||
set := users.([]interface{})
|
||||
for _, u := range set {
|
||||
user := u.(map[interface{}]interface{})
|
||||
for k, _ := range user {
|
||||
key := k.(string)
|
||||
if _, ok := known[key]; !ok {
|
||||
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any badly-specified files, if any are set
|
||||
files, ok := c["write_files"]
|
||||
if ok {
|
||||
var known map[string]interface{}
|
||||
b, _ := goyaml.Marshal(&system.File{})
|
||||
goyaml.Unmarshal(b, &known)
|
||||
|
||||
set := files.([]interface{})
|
||||
for _, f := range set {
|
||||
file := f.(map[interface{}]interface{})
|
||||
for k, _ := range file {
|
||||
key := k.(string)
|
||||
if _, ok := known[key]; !ok {
|
||||
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewCloudConfig instantiates a new CloudConfig from the given contents (a
|
||||
// string of YAML), returning any error encountered. It will ignore unknown
|
||||
// fields but log encountering them.
|
||||
func NewCloudConfig(contents string) (*CloudConfig, error) {
|
||||
var cfg CloudConfig
|
||||
err := goyaml.Unmarshal([]byte(contents), &cfg)
|
||||
if err != nil {
|
||||
return &cfg, err
|
||||
}
|
||||
warnOnUnrecognizedKeys(contents, log.Printf)
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (cc CloudConfig) String() string {
|
||||
bytes, err := goyaml.Marshal(cc)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
stringified := string(bytes)
|
||||
stringified = fmt.Sprintf("#cloud-config\n%s", stringified)
|
||||
|
||||
return stringified
|
||||
Units() ([]system.Unit, error)
|
||||
}
|
||||
|
||||
// Apply renders a CloudConfig to an Environment. This can involve things like
|
||||
// configuring the hostname, adding new users, writing various configuration
|
||||
// files to disk, and manipulating systemd services.
|
||||
func Apply(cfg CloudConfig, env *Environment) error {
|
||||
func Apply(cfg config.CloudConfig, env *Environment) error {
|
||||
if cfg.Hostname != "" {
|
||||
if err := system.SetHostname(cfg.Hostname); err != nil {
|
||||
return err
|
||||
@@ -205,42 +88,73 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, ccf := range []CloudConfigFile{cfg.Coreos.OEM, cfg.Coreos.Update, cfg.ManageEtcHosts} {
|
||||
f, err := ccf.File(env.Root())
|
||||
var writeFiles []system.File
|
||||
for _, file := range cfg.WriteFiles {
|
||||
writeFiles = append(writeFiles, system.File{file})
|
||||
}
|
||||
|
||||
for _, ccf := range []CloudConfigFile{
|
||||
system.OEM{cfg.Coreos.OEM},
|
||||
system.Update{cfg.Coreos.Update, system.DefaultReadConfig},
|
||||
system.EtcHosts{cfg.ManageEtcHosts},
|
||||
} {
|
||||
f, err := ccf.File()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f != nil {
|
||||
cfg.WriteFiles = append(cfg.WriteFiles, *f)
|
||||
writeFiles = append(writeFiles, *f)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
|
||||
u, err := ccu.Units(env.Root())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
|
||||
var units []system.Unit
|
||||
for _, u := range cfg.Coreos.Units {
|
||||
units = append(units, system.Unit{u})
|
||||
}
|
||||
|
||||
for _, file := range cfg.WriteFiles {
|
||||
path, err := system.WriteFile(&file, env.Root())
|
||||
for _, ccu := range []CloudConfigUnit{
|
||||
system.Etcd{cfg.Coreos.Etcd},
|
||||
system.Fleet{cfg.Coreos.Fleet},
|
||||
system.Update{cfg.Coreos.Update, system.DefaultReadConfig},
|
||||
} {
|
||||
u, err := ccu.Units()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Wrote file %s to filesystem", path)
|
||||
units = append(units, u...)
|
||||
}
|
||||
|
||||
wroteEnvironment := false
|
||||
for _, file := range writeFiles {
|
||||
fullPath, err := system.WriteFile(&file, env.Root())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if path.Clean(file.Path) == "/etc/environment" {
|
||||
wroteEnvironment = true
|
||||
}
|
||||
log.Printf("Wrote file %s to filesystem", fullPath)
|
||||
}
|
||||
|
||||
if !wroteEnvironment {
|
||||
ef := env.DefaultEnvironmentFile()
|
||||
if ef != nil {
|
||||
err := system.WriteEnvFile(ef, env.Root())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Updated /etc/environment")
|
||||
}
|
||||
}
|
||||
|
||||
if env.NetconfType() != "" {
|
||||
netconfBytes, err := ioutil.ReadFile(path.Join(env.ConfigRoot(), cfg.NetworkConfigPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var interfaces []network.InterfaceGenerator
|
||||
var err error
|
||||
switch env.NetconfType() {
|
||||
case "debian":
|
||||
interfaces, err = network.ProcessDebianNetconf(string(netconfBytes))
|
||||
interfaces, err = network.ProcessDebianNetconf(cfg.NetworkConfig)
|
||||
case "digitalocean":
|
||||
interfaces, err = network.ProcessDigitalOceanNetconf(cfg.NetworkConfig)
|
||||
default:
|
||||
return fmt.Errorf("Unsupported network config format %q", env.NetconfType())
|
||||
}
|
||||
@@ -257,13 +171,27 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
}
|
||||
}
|
||||
|
||||
commands := make(map[string]string, 0)
|
||||
um := system.NewUnitManager(env.Root())
|
||||
return processUnits(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 {
|
||||
type action struct {
|
||||
unit string
|
||||
command string
|
||||
}
|
||||
actions := make([]action, 0, len(units))
|
||||
reload := false
|
||||
for _, unit := range cfg.Coreos.Units {
|
||||
dst := unit.Destination(env.Root())
|
||||
for _, unit := range units {
|
||||
dst := unit.Destination(root)
|
||||
if unit.Content != "" {
|
||||
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
|
||||
if err := system.PlaceUnit(&unit, dst); err != nil {
|
||||
if err := um.PlaceUnit(&unit, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Placed unit %s at %s", unit.Name, dst)
|
||||
@@ -272,12 +200,12 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
|
||||
if unit.Mask {
|
||||
log.Printf("Masking unit file %s", unit.Name)
|
||||
if err := system.MaskUnit(&unit, env.Root()); err != nil {
|
||||
if err := um.MaskUnit(&unit); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if unit.Runtime {
|
||||
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
|
||||
if err := system.UnmaskUnit(&unit, env.Root()); err != nil {
|
||||
if err := um.UnmaskUnit(&unit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -285,7 +213,7 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
if unit.Enable {
|
||||
if unit.Group() != "network" {
|
||||
log.Printf("Enabling unit file %s", unit.Name)
|
||||
if err := system.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
|
||||
if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Enabled unit %s", unit.Name)
|
||||
@@ -295,25 +223,25 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
}
|
||||
|
||||
if unit.Group() == "network" {
|
||||
commands["systemd-networkd.service"] = "restart"
|
||||
actions = append(actions, action{"systemd-networkd.service", "restart"})
|
||||
} else if unit.Command != "" {
|
||||
commands[unit.Name] = unit.Command
|
||||
actions = append(actions, action{unit.Name, unit.Command})
|
||||
}
|
||||
}
|
||||
|
||||
if reload {
|
||||
if err := system.DaemonReload(); err != nil {
|
||||
if err := um.DaemonReload(); err != nil {
|
||||
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
for unit, command := range commands {
|
||||
log.Printf("Calling unit command '%s %s'", command, unit)
|
||||
res, err := system.RunUnitCommand(command, unit)
|
||||
for _, action := range actions {
|
||||
log.Printf("Calling unit command '%s %s'", action.command, action.unit)
|
||||
res, err := um.RunUnitCommand(action.command, action.unit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Result of '%s %s': %s", command, unit, res)
|
||||
log.Printf("Result of '%s %s': %s", action.command, action.unit, res)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@@ -1,334 +1,114 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestCloudConfigUnknownKeys(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
etcd:
|
||||
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
||||
coreos_unknown:
|
||||
foo: "bar"
|
||||
section_unknown:
|
||||
dunno:
|
||||
something
|
||||
bare_unknown:
|
||||
bar
|
||||
write_files:
|
||||
- content: fun
|
||||
path: /var/party
|
||||
file_unknown: nofun
|
||||
users:
|
||||
- name: fry
|
||||
passwd: somehash
|
||||
user_unknown: philip
|
||||
hostname:
|
||||
foo
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("error instantiating CloudConfig with unknown keys: %v", err)
|
||||
}
|
||||
if cfg.Hostname != "foo" {
|
||||
t.Fatalf("hostname not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.Coreos.Etcd) < 1 {
|
||||
t.Fatalf("etcd section not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.WriteFiles) < 1 || cfg.WriteFiles[0].Content != "fun" || cfg.WriteFiles[0].Path != "/var/party" {
|
||||
t.Fatalf("write_files section not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.Users) < 1 || cfg.Users[0].Name != "fry" || cfg.Users[0].PasswordHash != "somehash" {
|
||||
t.Fatalf("users section not correctly set when invalid keys are present")
|
||||
}
|
||||
|
||||
var warnings string
|
||||
catchWarn := func(f string, v ...interface{}) {
|
||||
warnings += fmt.Sprintf(f, v...)
|
||||
}
|
||||
|
||||
warnOnUnrecognizedKeys(contents, catchWarn)
|
||||
|
||||
if !strings.Contains(warnings, "coreos_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized coreos option coreos_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "bare_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized key bare_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "section_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized key section_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "user_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized user key user_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "file_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized file key file_unknown")
|
||||
}
|
||||
type TestUnitManager struct {
|
||||
placed []string
|
||||
enabled []string
|
||||
masked []string
|
||||
unmasked []string
|
||||
commands map[string]string
|
||||
reload bool
|
||||
}
|
||||
|
||||
// Assert that the parsing of a cloud config file "generally works"
|
||||
func TestCloudConfigEmpty(t *testing.T) {
|
||||
cfg, err := NewCloudConfig("")
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error :%v", err)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
if len(keys) != 0 {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
}
|
||||
|
||||
if len(cfg.WriteFiles) != 0 {
|
||||
t.Error("Expected zero WriteFiles")
|
||||
}
|
||||
|
||||
if cfg.Hostname != "" {
|
||||
t.Errorf("Expected hostname to be empty, got '%s'", cfg.Hostname)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Assert that the parsing of a cloud config file "generally works"
|
||||
func TestCloudConfig(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
etcd:
|
||||
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
||||
update:
|
||||
reboot-strategy: reboot
|
||||
units:
|
||||
- name: 50-eth0.network
|
||||
runtime: yes
|
||||
content: '[Match]
|
||||
|
||||
Name=eth47
|
||||
|
||||
|
||||
[Network]
|
||||
|
||||
Address=10.209.171.177/19
|
||||
|
||||
'
|
||||
oem:
|
||||
id: rackspace
|
||||
name: Rackspace Cloud Servers
|
||||
version-id: 168.0.0
|
||||
home-url: https://www.rackspace.com/cloud/servers/
|
||||
bug-report-url: https://github.com/coreos/coreos-overlay
|
||||
ssh_authorized_keys:
|
||||
- foobar
|
||||
- foobaz
|
||||
write_files:
|
||||
- content: |
|
||||
penny
|
||||
elroy
|
||||
path: /etc/dogepack.conf
|
||||
permissions: '0644'
|
||||
owner: root:dogepack
|
||||
hostname: trontastic
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error :%v", err)
|
||||
func TestProcessUnits(t *testing.T) {
|
||||
tum := &TestUnitManager{}
|
||||
units := []system.Unit{
|
||||
system.Unit{config.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)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
if len(keys) != 2 {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
} else if keys[0] != "foobar" {
|
||||
t.Error("Expected first SSH key to be 'foobar'")
|
||||
} else if keys[1] != "foobaz" {
|
||||
t.Error("Expected first SSH key to be 'foobaz'")
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{config.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!")
|
||||
}
|
||||
|
||||
if len(cfg.WriteFiles) != 1 {
|
||||
t.Error("Failed to parse correct number of write_files")
|
||||
} else {
|
||||
wf := cfg.WriteFiles[0]
|
||||
if wf.Content != "penny\nelroy\n" {
|
||||
t.Errorf("WriteFile has incorrect contents '%s'", wf.Content)
|
||||
}
|
||||
if wf.Encoding != "" {
|
||||
t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding)
|
||||
}
|
||||
if perm, _ := wf.Permissions(); perm != 0644 {
|
||||
t.Errorf("WriteFile has incorrect permissions %s", perm)
|
||||
}
|
||||
if wf.Path != "/etc/dogepack.conf" {
|
||||
t.Errorf("WriteFile has incorrect path %s", wf.Path)
|
||||
}
|
||||
if wf.Owner != "root:dogepack" {
|
||||
t.Errorf("WriteFile has incorrect owner %s", wf.Owner)
|
||||
}
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{config.Unit{
|
||||
Name: "baz.service",
|
||||
Content: "[Service]\nExecStart=/bin/true",
|
||||
}},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.placed) != 1 || tum.placed[0] != "baz.service" {
|
||||
t.Fatalf("expected baz.service to be written, but got %v", tum.placed)
|
||||
}
|
||||
|
||||
if len(cfg.Coreos.Units) != 1 {
|
||||
t.Error("Failed to parse correct number of units")
|
||||
} else {
|
||||
u := cfg.Coreos.Units[0]
|
||||
expect := `[Match]
|
||||
Name=eth47
|
||||
|
||||
[Network]
|
||||
Address=10.209.171.177/19
|
||||
`
|
||||
if u.Content != expect {
|
||||
t.Errorf("Unit has incorrect contents '%s'.\nExpected '%s'.", u.Content, expect)
|
||||
}
|
||||
if u.Runtime != true {
|
||||
t.Errorf("Unit has incorrect runtime value")
|
||||
}
|
||||
if u.Name != "50-eth0.network" {
|
||||
t.Errorf("Unit has incorrect name %s", u.Name)
|
||||
}
|
||||
if u.Type() != "network" {
|
||||
t.Errorf("Unit has incorrect type '%s'", u.Type())
|
||||
}
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{config.Unit{
|
||||
Name: "locksmithd.service",
|
||||
Runtime: true,
|
||||
}},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.unmasked) != 1 || tum.unmasked[0] != "locksmithd.service" {
|
||||
t.Fatalf("expected locksmithd.service to be unmasked, but got %v", tum.unmasked)
|
||||
}
|
||||
|
||||
if cfg.Coreos.OEM.ID != "rackspace" {
|
||||
t.Errorf("Failed parsing coreos.oem. Expected ID 'rackspace', got %q.", cfg.Coreos.OEM.ID)
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{config.Unit{
|
||||
Name: "woof",
|
||||
Enable: true,
|
||||
}},
|
||||
}
|
||||
|
||||
if cfg.Hostname != "trontastic" {
|
||||
t.Errorf("Failed to parse hostname")
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if cfg.Coreos.Update["reboot-strategy"] != "reboot" {
|
||||
t.Errorf("Failed to parse locksmith strategy")
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that our interface conversion doesn't panic
|
||||
func TestCloudConfigKeysNotList(t *testing.T) {
|
||||
contents := `
|
||||
ssh_authorized_keys:
|
||||
- foo: bar
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
if len(keys) != 0 {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigSerializationHeader(t *testing.T) {
|
||||
cfg, _ := NewCloudConfig("")
|
||||
contents := cfg.String()
|
||||
header := strings.SplitN(contents, "\n", 2)[0]
|
||||
if header != "#cloud-config" {
|
||||
t.Fatalf("Serialized config did not have expected header")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropInIgnored asserts that users are unable to set DropIn=True on units
|
||||
func TestDropInIgnored(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
units:
|
||||
- name: test
|
||||
dropin: true
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil || len(cfg.Coreos.Units) != 1 {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
if len(cfg.Coreos.Units) != 1 || cfg.Coreos.Units[0].Name != "test" {
|
||||
t.Fatalf("Expected 1 unit, but got %d: %v", len(cfg.Coreos.Units), cfg.Coreos.Units)
|
||||
}
|
||||
if cfg.Coreos.Units[0].DropIn {
|
||||
t.Errorf("dropin option on unit in cloud-config was not ignored!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUsers(t *testing.T) {
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
passwd: somehash
|
||||
ssh-authorized-keys:
|
||||
- somekey
|
||||
gecos: arbitrary comment
|
||||
homedir: /home/place
|
||||
no-create-home: yes
|
||||
primary-group: things
|
||||
groups:
|
||||
- ping
|
||||
- pong
|
||||
no-user-group: true
|
||||
system: y
|
||||
no-log-init: True
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.PasswordHash != "somehash" {
|
||||
t.Errorf("User passwd is %q, expected 'somehash'", user.PasswordHash)
|
||||
}
|
||||
|
||||
if keys := user.SSHAuthorizedKeys; len(keys) != 1 {
|
||||
t.Errorf("Parsed %d ssh keys, expected 1", len(keys))
|
||||
} else {
|
||||
key := user.SSHAuthorizedKeys[0]
|
||||
if key != "somekey" {
|
||||
t.Errorf("User SSH key is %q, expected 'somekey'", key)
|
||||
}
|
||||
}
|
||||
|
||||
if user.GECOS != "arbitrary comment" {
|
||||
t.Errorf("Failed to parse gecos field, got %q", user.GECOS)
|
||||
}
|
||||
|
||||
if user.Homedir != "/home/place" {
|
||||
t.Errorf("Failed to parse homedir field, got %q", user.Homedir)
|
||||
}
|
||||
|
||||
if !user.NoCreateHome {
|
||||
t.Errorf("Failed to parse no-create-home field")
|
||||
}
|
||||
|
||||
if user.PrimaryGroup != "things" {
|
||||
t.Errorf("Failed to parse primary-group field, got %q", user.PrimaryGroup)
|
||||
}
|
||||
|
||||
if len(user.Groups) != 2 {
|
||||
t.Errorf("Failed to parse 2 goups, got %d", len(user.Groups))
|
||||
} else {
|
||||
if user.Groups[0] != "ping" {
|
||||
t.Errorf("First group was %q, not expected value 'ping'", user.Groups[0])
|
||||
}
|
||||
if user.Groups[1] != "pong" {
|
||||
t.Errorf("First group was %q, not expected value 'pong'", user.Groups[1])
|
||||
}
|
||||
}
|
||||
|
||||
if !user.NoUserGroup {
|
||||
t.Errorf("Failed to parse no-user-group field")
|
||||
}
|
||||
|
||||
if !user.System {
|
||||
t.Errorf("Failed to parse system field")
|
||||
}
|
||||
|
||||
if !user.NoLogInit {
|
||||
t.Errorf("Failed to parse no-log-init field")
|
||||
if len(tum.enabled) != 1 || tum.enabled[0] != "woof" {
|
||||
t.Fatalf("expected woof to be enabled, but got %v", tum.enabled)
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,11 @@ package initialize
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const DefaultSSHKeyName = "coreos-cloudinit"
|
||||
@@ -26,6 +30,8 @@ func NewEnvironment(root, configRoot, workspace, netconfType, sshKeyName string,
|
||||
for k, v := range map[string]string{
|
||||
"$public_ipv4": os.Getenv("COREOS_PUBLIC_IPV4"),
|
||||
"$private_ipv4": os.Getenv("COREOS_PRIVATE_IPV4"),
|
||||
"$public_ipv6": os.Getenv("COREOS_PUBLIC_IPV6"),
|
||||
"$private_ipv6": os.Getenv("COREOS_PRIVATE_IPV6"),
|
||||
} {
|
||||
if _, ok := substitutions[k]; !ok {
|
||||
substitutions[k] = v
|
||||
@@ -58,22 +64,44 @@ func (e *Environment) SetSSHKeyName(name string) {
|
||||
e.sshKeyName = name
|
||||
}
|
||||
|
||||
// Apply goes through the map of substitutions and replaces all instances of
|
||||
// the keys with their respective values. It supports escaping substitutions
|
||||
// with a leading '\'.
|
||||
func (e *Environment) Apply(data string) string {
|
||||
for key, val := range e.substitutions {
|
||||
data = strings.Replace(data, key, val, -1)
|
||||
matchKey := strings.Replace(key, `$`, `\$`, -1)
|
||||
replKey := strings.Replace(key, `$`, `$$`, -1)
|
||||
|
||||
// "key" -> "val"
|
||||
data = regexp.MustCompile(`([^\\]|^)`+matchKey).ReplaceAllString(data, `${1}`+val)
|
||||
// "\key" -> "key"
|
||||
data = regexp.MustCompile(`\\`+matchKey).ReplaceAllString(data, replKey)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// normalizeSvcEnv standardizes the keys of the map (environment variables for a service)
|
||||
// by replacing any dashes with underscores and ensuring they are entirely upper case.
|
||||
// For example, "some-env" --> "SOME_ENV"
|
||||
func normalizeSvcEnv(m map[string]string) map[string]string {
|
||||
out := make(map[string]string, len(m))
|
||||
for key, val := range m {
|
||||
key = strings.ToUpper(key)
|
||||
key = strings.Replace(key, "-", "_", -1)
|
||||
out[key] = val
|
||||
func (e *Environment) DefaultEnvironmentFile() *system.EnvFile {
|
||||
ef := system.EnvFile{
|
||||
File: &system.File{config.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 ip, ok := e.substitutions["$public_ipv6"]; ok && len(ip) > 0 {
|
||||
ef.Vars["COREOS_PUBLIC_IPV6"] = ip
|
||||
}
|
||||
if ip, ok := e.substitutions["$private_ipv6"]; ok && len(ip) > 0 {
|
||||
ef.Vars["COREOS_PRIVATE_IPV6"] = ip
|
||||
}
|
||||
if len(ef.Vars) == 0 {
|
||||
return nil
|
||||
} else {
|
||||
return &ef
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
@@ -1,13 +1,19 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestEnvironmentApply(t *testing.T) {
|
||||
os.Setenv("COREOS_PUBLIC_IPV4", "1.2.3.4")
|
||||
os.Setenv("COREOS_PRIVATE_IPV4", "5.6.7.8")
|
||||
os.Setenv("COREOS_PUBLIC_IPV6", "1234::")
|
||||
os.Setenv("COREOS_PRIVATE_IPV6", "5678::")
|
||||
for _, tt := range []struct {
|
||||
subs map[string]string
|
||||
input string
|
||||
@@ -19,14 +25,16 @@ func TestEnvironmentApply(t *testing.T) {
|
||||
map[string]string{
|
||||
"$public_ipv4": "192.0.2.3",
|
||||
"$private_ipv4": "192.0.2.203",
|
||||
"$public_ipv6": "fe00:1234::",
|
||||
"$private_ipv6": "fe00:5678::",
|
||||
},
|
||||
`[Service]
|
||||
ExecStart=/usr/bin/echo "$public_ipv4"
|
||||
ExecStop=/usr/bin/echo $private_ipv4
|
||||
ExecStart=/usr/bin/echo "$public_ipv4 $public_ipv6"
|
||||
ExecStop=/usr/bin/echo $private_ipv4 $private_ipv6
|
||||
ExecStop=/usr/bin/echo $unknown`,
|
||||
`[Service]
|
||||
ExecStart=/usr/bin/echo "192.0.2.3"
|
||||
ExecStop=/usr/bin/echo 192.0.2.203
|
||||
ExecStart=/usr/bin/echo "192.0.2.3 fe00:1234::"
|
||||
ExecStop=/usr/bin/echo 192.0.2.203 fe00:5678::
|
||||
ExecStop=/usr/bin/echo $unknown`,
|
||||
},
|
||||
{
|
||||
@@ -47,6 +55,24 @@ ExecStop=/usr/bin/echo $unknown`,
|
||||
"$private_ipv4\nfoobar",
|
||||
"5.6.7.8\nfoobar",
|
||||
},
|
||||
{
|
||||
// Escaping substitutions
|
||||
map[string]string{"$private_ipv4": "127.0.0.1"},
|
||||
`\$private_ipv4
|
||||
$private_ipv4
|
||||
addr: \$private_ipv4
|
||||
\\$private_ipv4`,
|
||||
`$private_ipv4
|
||||
127.0.0.1
|
||||
addr: $private_ipv4
|
||||
\$private_ipv4`,
|
||||
},
|
||||
{
|
||||
// No substitutions with escaping
|
||||
nil,
|
||||
"\\$test\n$test",
|
||||
"\\$test\n$test",
|
||||
},
|
||||
} {
|
||||
|
||||
env := NewEnvironment("./", "./", "./", "", "", tt.subs)
|
||||
@@ -56,3 +82,51 @@ 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",
|
||||
"$public_ipv6": "1234::",
|
||||
"$private_ipv6": "5678::",
|
||||
}
|
||||
expect := "COREOS_PRIVATE_IPV4=5.6.7.8\nCOREOS_PRIVATE_IPV6=5678::\nCOREOS_PUBLIC_IPV4=1.2.3.4\nCOREOS_PUBLIC_IPV6=1234::\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": "",
|
||||
"$public_ipv6": "",
|
||||
"$private_ipv6": "",
|
||||
}
|
||||
|
||||
env := NewEnvironment("./", "./", "./", "", "", subs)
|
||||
ef := env.DefaultEnvironmentFile()
|
||||
if ef != nil {
|
||||
t.Fatalf("Environment file not nil: %v", ef)
|
||||
}
|
||||
}
|
||||
|
@@ -1,63 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
type EtcdEnvironment map[string]string
|
||||
|
||||
func (ee EtcdEnvironment) String() (out string) {
|
||||
norm := normalizeSvcEnv(ee)
|
||||
|
||||
if val, ok := norm["DISCOVERY_URL"]; ok {
|
||||
delete(norm, "DISCOVERY_URL")
|
||||
if _, ok := norm["DISCOVERY"]; !ok {
|
||||
norm["DISCOVERY"] = val
|
||||
}
|
||||
}
|
||||
|
||||
var sorted sort.StringSlice
|
||||
for k, _ := range norm {
|
||||
sorted = append(sorted, k)
|
||||
}
|
||||
sorted.Sort()
|
||||
|
||||
out += "[Service]\n"
|
||||
|
||||
for _, key := range sorted {
|
||||
val := norm[key]
|
||||
out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Units creates a Unit file drop-in for etcd, using any configured
|
||||
// options and adding a default MachineID if unset.
|
||||
func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
|
||||
if ee == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if _, ok := ee["name"]; !ok {
|
||||
if machineID := system.MachineID(root); machineID != "" {
|
||||
ee["name"] = machineID
|
||||
} else if hostname, err := system.Hostname(); err == nil {
|
||||
ee["name"] = hostname
|
||||
} else {
|
||||
return nil, errors.New("Unable to determine default etcd name")
|
||||
}
|
||||
}
|
||||
|
||||
etcd := system.Unit{
|
||||
Name: "etcd.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: ee.String(),
|
||||
}
|
||||
return []system.Unit{etcd}, nil
|
||||
}
|
@@ -1,168 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestEtcdEnvironment(t *testing.T) {
|
||||
cfg := make(EtcdEnvironment, 0)
|
||||
cfg["discovery"] = "http://disco.example.com/foobar"
|
||||
cfg["peer-bind-addr"] = "127.0.0.1:7002"
|
||||
|
||||
env := cfg.String()
|
||||
expect := `[Service]
|
||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
`
|
||||
|
||||
if env != expect {
|
||||
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdEnvironmentDiscoveryURLTranslated(t *testing.T) {
|
||||
cfg := make(EtcdEnvironment, 0)
|
||||
cfg["discovery_url"] = "http://disco.example.com/foobar"
|
||||
cfg["peer-bind-addr"] = "127.0.0.1:7002"
|
||||
|
||||
env := cfg.String()
|
||||
expect := `[Service]
|
||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
`
|
||||
|
||||
if env != expect {
|
||||
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdEnvironmentDiscoveryOverridesDiscoveryURL(t *testing.T) {
|
||||
cfg := make(EtcdEnvironment, 0)
|
||||
cfg["discovery_url"] = "ping"
|
||||
cfg["discovery"] = "pong"
|
||||
cfg["peer-bind-addr"] = "127.0.0.1:7002"
|
||||
|
||||
env := cfg.String()
|
||||
expect := `[Service]
|
||||
Environment="ETCD_DISCOVERY=pong"
|
||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
`
|
||||
|
||||
if env != expect {
|
||||
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
||||
ee := EtcdEnvironment{
|
||||
"name": "node001",
|
||||
"discovery": "http://disco.example.com/foobar",
|
||||
"peer-bind-addr": "127.0.0.1:7002",
|
||||
}
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
uu, err := ee.Units(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Generating etcd unit failed: %v", err)
|
||||
}
|
||||
if len(uu) != 1 {
|
||||
t.Fatalf("Expected 1 unit to be returned, got %d", len(uu))
|
||||
}
|
||||
u := uu[0]
|
||||
|
||||
dst := u.Destination(dir)
|
||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
||||
if err := system.PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode() != os.FileMode(0644) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
expect := `[Service]
|
||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||
Environment="ETCD_NAME=node001"
|
||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
`
|
||||
if string(contents) != expect {
|
||||
t.Fatalf("File has incorrect contents")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
|
||||
ee := EtcdEnvironment{}
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
|
||||
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed writing out /etc/machine-id: %v", err)
|
||||
}
|
||||
|
||||
uu, err := ee.Units(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Generating etcd unit failed: %v", err)
|
||||
}
|
||||
if len(uu) == 0 {
|
||||
t.Fatalf("Returned empty etcd units unexpectedly")
|
||||
}
|
||||
u := uu[0]
|
||||
|
||||
dst := u.Destination(dir)
|
||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
||||
if err := system.PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
expect := `[Service]
|
||||
Environment="ETCD_NAME=node007"
|
||||
`
|
||||
if string(contents) != expect {
|
||||
t.Fatalf("File has incorrect contents")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdEnvironmentWhenNil(t *testing.T) {
|
||||
// EtcdEnvironment will be a nil map if it wasn't in the yaml
|
||||
var ee EtcdEnvironment
|
||||
if ee != nil {
|
||||
t.Fatalf("EtcdEnvironment is not nil")
|
||||
}
|
||||
uu, err := ee.Units("")
|
||||
if len(uu) != 0 || err != nil {
|
||||
t.Fatalf("Units returned value for nil input")
|
||||
}
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
type FleetEnvironment map[string]string
|
||||
|
||||
func (fe FleetEnvironment) String() (out string) {
|
||||
norm := normalizeSvcEnv(fe)
|
||||
out += "[Service]\n"
|
||||
|
||||
for key, val := range norm {
|
||||
out += fmt.Sprintf("Environment=\"FLEET_%s=%s\"\n", key, val)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Units generates a Unit file drop-in for fleet, if any fleet options were
|
||||
// configured in cloud-config
|
||||
func (fe FleetEnvironment) Units(root string) ([]system.Unit, error) {
|
||||
if len(fe) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
fleet := system.Unit{
|
||||
Name: "fleet.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: fe.String(),
|
||||
}
|
||||
return []system.Unit{fleet}, nil
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFleetEnvironment(t *testing.T) {
|
||||
cfg := make(FleetEnvironment, 0)
|
||||
cfg["public-ip"] = "12.34.56.78"
|
||||
|
||||
env := cfg.String()
|
||||
|
||||
expect := `[Service]
|
||||
Environment="FLEET_PUBLIC_IP=12.34.56.78"
|
||||
`
|
||||
|
||||
if env != expect {
|
||||
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetUnit(t *testing.T) {
|
||||
cfg := make(FleetEnvironment, 0)
|
||||
uu, err := cfg.Units("/")
|
||||
if len(uu) != 0 {
|
||||
t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
|
||||
}
|
||||
|
||||
cfg["public-ip"] = "12.34.56.78"
|
||||
|
||||
uu, err = cfg.Units("/")
|
||||
if err != nil {
|
||||
t.Errorf("error generating fleet unit: %v", err)
|
||||
}
|
||||
if len(uu) != 1 {
|
||||
t.Fatalf("expected 1 unit generated, got %d", len(uu))
|
||||
}
|
||||
u := uu[0]
|
||||
if !u.Runtime {
|
||||
t.Errorf("bad Runtime for generated fleet unit!")
|
||||
}
|
||||
if !u.DropIn {
|
||||
t.Errorf("bad DropIn for generated fleet unit!")
|
||||
}
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCloudConfigUsersGithubUser(t *testing.T) {
|
||||
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-github: bcwaldon
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.SSHImportGithubUser != "bcwaldon" {
|
||||
t.Errorf("github user is %q, expected 'bcwaldon'", user.SSHImportGithubUser)
|
||||
}
|
||||
}
|
@@ -1,83 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestCloudConfigManageEtcHosts(t *testing.T) {
|
||||
contents := `
|
||||
manage_etc_hosts: localhost
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
manageEtcHosts := cfg.ManageEtcHosts
|
||||
|
||||
if manageEtcHosts != "localhost" {
|
||||
t.Errorf("ManageEtcHosts value is %q, expected 'localhost'", manageEtcHosts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManageEtcHostsInvalidValue(t *testing.T) {
|
||||
eh := EtcHosts("invalid")
|
||||
if f, err := eh.File(""); err == nil || f != nil {
|
||||
t.Fatalf("EtcHosts File succeeded with invalid value!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcHostsWrittenToDisk(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)
|
||||
|
||||
eh := EtcHosts("localhost")
|
||||
|
||||
f, err := eh.File(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error calling File on EtcHosts: %v", err)
|
||||
}
|
||||
if f == nil {
|
||||
t.Fatalf("manageEtcHosts returned nil file unexpectedly")
|
||||
}
|
||||
|
||||
if _, err := system.WriteFile(f, dir); err != nil {
|
||||
t.Fatalf("Error writing EtcHosts: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "hosts")
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode() != os.FileMode(0644) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read OS hostname: %v", err)
|
||||
}
|
||||
|
||||
expect := fmt.Sprintf("%s %s\n", DefaultIpv4Address, hostname)
|
||||
|
||||
if string(contents) != expect {
|
||||
t.Fatalf("File has incorrect contents")
|
||||
}
|
||||
}
|
@@ -1,10 +1,15 @@
|
||||
package initialize
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
|
||||
// ParseMetaData parses a JSON blob in the OpenStack metadata service format, and
|
||||
// converts it to a partially hydrated CloudConfig
|
||||
func ParseMetaData(contents string) (*CloudConfig, error) {
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
// ParseMetaData parses a JSON blob in the OpenStack metadata service format,
|
||||
// and converts it to a partially hydrated CloudConfig.
|
||||
func ParseMetaData(contents string) (*config.CloudConfig, error) {
|
||||
if len(contents) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -19,11 +24,11 @@ func ParseMetaData(contents string) (*CloudConfig, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg CloudConfig
|
||||
var cfg config.CloudConfig
|
||||
if len(metadata.SSHAuthorizedKeyMap) > 0 {
|
||||
cfg.SSHAuthorizedKeys = make([]string, 0, len(metadata.SSHAuthorizedKeyMap))
|
||||
for _, key := range metadata.SSHAuthorizedKeyMap {
|
||||
cfg.SSHAuthorizedKeys = append(cfg.SSHAuthorizedKeys, key)
|
||||
for _, name := range sortedKeys(metadata.SSHAuthorizedKeyMap) {
|
||||
cfg.SSHAuthorizedKeys = append(cfg.SSHAuthorizedKeys, metadata.SSHAuthorizedKeyMap[name])
|
||||
}
|
||||
}
|
||||
cfg.Hostname = metadata.Hostname
|
||||
@@ -31,22 +36,39 @@ func ParseMetaData(contents string) (*CloudConfig, error) {
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// ExtractIPsFromMetaData parses a JSON blob in the OpenStack metadata service format,
|
||||
// and returns a substitution map possibly containing private_ipv4 and public_ipv4 addresses
|
||||
// ExtractIPsFromMetaData parses a JSON blob in the OpenStack metadata service
|
||||
// format and returns a substitution map possibly containing private_ipv4,
|
||||
// public_ipv4, private_ipv6, and public_ipv6 addresses.
|
||||
func ExtractIPsFromMetadata(contents []byte) (map[string]string, error) {
|
||||
var ips struct {
|
||||
Public string `json:"public-ipv4"`
|
||||
Private string `json:"local-ipv4"`
|
||||
PublicIPv4 string `json:"public-ipv4"`
|
||||
PrivateIPv4 string `json:"local-ipv4"`
|
||||
PublicIPv6 string `json:"public-ipv6"`
|
||||
PrivateIPv6 string `json:"local-ipv6"`
|
||||
}
|
||||
if err := json.Unmarshal(contents, &ips); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]string)
|
||||
if ips.Private != "" {
|
||||
m["$private_ipv4"] = ips.Private
|
||||
if ips.PrivateIPv4 != "" {
|
||||
m["$private_ipv4"] = ips.PrivateIPv4
|
||||
}
|
||||
if ips.Public != "" {
|
||||
m["$public_ipv4"] = ips.Public
|
||||
if ips.PublicIPv4 != "" {
|
||||
m["$public_ipv4"] = ips.PublicIPv4
|
||||
}
|
||||
if ips.PrivateIPv6 != "" {
|
||||
m["$private_ipv6"] = ips.PrivateIPv6
|
||||
}
|
||||
if ips.PublicIPv6 != "" {
|
||||
m["$public_ipv6"] = ips.PublicIPv6
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]string) (keys []string) {
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return
|
||||
}
|
||||
|
@@ -1,21 +1,25 @@
|
||||
package initialize
|
||||
|
||||
import "reflect"
|
||||
import "testing"
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
for i, tt := range []struct {
|
||||
in string
|
||||
want *CloudConfig
|
||||
want *config.CloudConfig
|
||||
err bool
|
||||
}{
|
||||
{"", nil, false},
|
||||
{`garbage, invalid json`, nil, true},
|
||||
{`{"foo": "bar"}`, &CloudConfig{}, false},
|
||||
{`{"network_config": {"content_path": "asdf"}}`, &CloudConfig{NetworkConfigPath: "asdf"}, false},
|
||||
{`{"hostname": "turkleton"}`, &CloudConfig{Hostname: "turkleton"}, false},
|
||||
{`{"public_keys": {"jack": "jill", "bob": "alice"}}`, &CloudConfig{SSHAuthorizedKeys: []string{"jill", "alice"}}, false},
|
||||
{`{"unknown": "thing", "hostname": "my_host", "public_keys": {"do": "re", "mi": "fa"}, "network_config": {"content_path": "/root", "blah": "zzz"}}`, &CloudConfig{SSHAuthorizedKeys: []string{"re", "fa"}, Hostname: "my_host", NetworkConfigPath: "/root"}, false},
|
||||
{`{"foo": "bar"}`, &config.CloudConfig{}, false},
|
||||
{`{"network_config": {"content_path": "asdf"}}`, &config.CloudConfig{NetworkConfigPath: "asdf"}, false},
|
||||
{`{"hostname": "turkleton"}`, &config.CloudConfig{Hostname: "turkleton"}, false},
|
||||
{`{"public_keys": {"jack": "jill", "bob": "alice"}}`, &config.CloudConfig{SSHAuthorizedKeys: []string{"alice", "jill"}}, false},
|
||||
{`{"unknown": "thing", "hostname": "my_host", "public_keys": {"do": "re", "mi": "fa"}, "network_config": {"content_path": "/root", "blah": "zzz"}}`, &config.CloudConfig{SSHAuthorizedKeys: []string{"re", "fa"}, Hostname: "my_host", NetworkConfigPath: "/root"}, false},
|
||||
} {
|
||||
got, err := ParseMetaData(tt.in)
|
||||
if tt.err != (err != nil) {
|
||||
@@ -43,9 +47,9 @@ func TestExtractIPsFromMetadata(t *testing.T) {
|
||||
out map[string]string
|
||||
}{
|
||||
{
|
||||
[]byte(`{"public-ipv4": "12.34.56.78", "local-ipv4": "1.2.3.4"}`),
|
||||
[]byte(`{"public-ipv4": "12.34.56.78", "local-ipv4": "1.2.3.4", "public-ipv6": "1234::", "local-ipv6": "5678::"}`),
|
||||
false,
|
||||
map[string]string{"$public_ipv4": "12.34.56.78", "$private_ipv4": "1.2.3.4"},
|
||||
map[string]string{"$public_ipv4": "12.34.56.78", "$private_ipv4": "1.2.3.4", "$public_ipv6": "1234::", "$private_ipv6": "5678::"},
|
||||
},
|
||||
{
|
||||
[]byte(`{"local-ipv4": "127.0.0.1", "something_else": "don't care"}`),
|
||||
|
@@ -1,41 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
type OEMRelease struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
VersionID string `yaml:"version-id"`
|
||||
HomeURL string `yaml:"home-url"`
|
||||
BugReportURL string `yaml:"bug-report-url"`
|
||||
}
|
||||
|
||||
func (oem OEMRelease) String() string {
|
||||
fields := []string{
|
||||
fmt.Sprintf("ID=%s", oem.ID),
|
||||
fmt.Sprintf("VERSION_ID=%s", oem.VersionID),
|
||||
fmt.Sprintf("NAME=%q", oem.Name),
|
||||
fmt.Sprintf("HOME_URL=%q", oem.HomeURL),
|
||||
fmt.Sprintf("BUG_REPORT_URL=%q", oem.BugReportURL),
|
||||
}
|
||||
|
||||
return strings.Join(fields, "\n") + "\n"
|
||||
}
|
||||
|
||||
func (oem OEMRelease) File(root string) (*system.File, error) {
|
||||
if oem.ID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &system.File{
|
||||
Path: path.Join("etc", "oem-release"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: oem.String(),
|
||||
}, nil
|
||||
}
|
@@ -1,63 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestOEMReleaseWrittenToDisk(t *testing.T) {
|
||||
oem := OEMRelease{
|
||||
ID: "rackspace",
|
||||
Name: "Rackspace Cloud Servers",
|
||||
VersionID: "168.0.0",
|
||||
HomeURL: "https://www.rackspace.com/cloud/servers/",
|
||||
BugReportURL: "https://github.com/coreos/coreos-overlay",
|
||||
}
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
f, err := oem.File(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Processing of OEMRelease failed: %v", err)
|
||||
}
|
||||
if f == nil {
|
||||
t.Fatalf("OEMRelease returned nil file unexpectedly")
|
||||
}
|
||||
|
||||
if _, err := system.WriteFile(f, dir); err != nil {
|
||||
t.Fatalf("Writing of OEMRelease failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "oem-release")
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode() != os.FileMode(0644) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
expect := `ID=rackspace
|
||||
VERSION_ID=168.0.0
|
||||
NAME="Rackspace Cloud Servers"
|
||||
HOME_URL="https://www.rackspace.com/cloud/servers/"
|
||||
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
|
||||
`
|
||||
if string(contents) != expect {
|
||||
t.Fatalf("File has incorrect contents")
|
||||
}
|
||||
}
|
@@ -39,31 +39,4 @@ func TestCloudConfigUsersUrlMarshal(t *testing.T) {
|
||||
if keys[2] != expected {
|
||||
t.Fatalf("expected %s, got %s", expected, keys[2])
|
||||
}
|
||||
|
||||
}
|
||||
func TestCloudConfigUsersSSHImportURL(t *testing.T) {
|
||||
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-url: https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.SSHImportURL != "https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys" {
|
||||
t.Errorf("ssh import url is %q, expected 'https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys'", user.SSHImportURL)
|
||||
}
|
||||
}
|
||||
|
@@ -1,165 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const (
|
||||
locksmithUnit = "locksmithd.service"
|
||||
updateEngineUnit = "update-engine.service"
|
||||
)
|
||||
|
||||
// updateOption represents a configurable update option, which, if set, will be
|
||||
// written into update.conf, replacing any existing value for the option
|
||||
type updateOption struct {
|
||||
key string // key used to configure this option in cloud-config
|
||||
valid []string // valid values for the option
|
||||
prefix string // prefix for the option in the update.conf file
|
||||
value string // used to store the new value in update.conf (including prefix)
|
||||
seen bool // whether the option has been seen in any existing update.conf
|
||||
}
|
||||
|
||||
// updateOptions defines the update options understood by cloud-config.
|
||||
// The keys represent the string used in cloud-config to configure the option.
|
||||
var updateOptions = []*updateOption{
|
||||
&updateOption{
|
||||
key: "reboot-strategy",
|
||||
prefix: "REBOOT_STRATEGY=",
|
||||
valid: []string{"best-effort", "etcd-lock", "reboot", "off"},
|
||||
},
|
||||
&updateOption{
|
||||
key: "group",
|
||||
prefix: "GROUP=",
|
||||
},
|
||||
&updateOption{
|
||||
key: "server",
|
||||
prefix: "SERVER=",
|
||||
},
|
||||
}
|
||||
|
||||
// isValid checks whether a supplied value is valid for this option
|
||||
func (uo updateOption) isValid(val string) bool {
|
||||
if len(uo.valid) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, v := range uo.valid {
|
||||
if val == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type UpdateConfig map[string]string
|
||||
|
||||
// File generates an `/etc/coreos/update.conf` file (if any update
|
||||
// configuration options are set in cloud-config) by either rewriting the
|
||||
// existing file on disk, or starting from `/usr/share/coreos/update.conf`
|
||||
func (uc UpdateConfig) File(root string) (*system.File, error) {
|
||||
if len(uc) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var out string
|
||||
|
||||
// Generate the list of possible substitutions to be performed based on the options that are configured
|
||||
subs := make([]*updateOption, 0)
|
||||
for _, uo := range updateOptions {
|
||||
val, ok := uc[uo.key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !uo.isValid(val) {
|
||||
return nil, errors.New(fmt.Sprintf("invalid value %v for option %v (valid options: %v)", val, uo.key, uo.valid))
|
||||
}
|
||||
uo.value = uo.prefix + val
|
||||
subs = append(subs, uo)
|
||||
}
|
||||
|
||||
etcUpdate := path.Join(root, "etc", "coreos", "update.conf")
|
||||
usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf")
|
||||
|
||||
conf, err := os.Open(etcUpdate)
|
||||
if os.IsNotExist(err) {
|
||||
conf, err = os.Open(usrUpdate)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(conf)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
for _, s := range subs {
|
||||
if strings.HasPrefix(line, s.prefix) {
|
||||
line = s.value
|
||||
s.seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
out += line
|
||||
out += "\n"
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range subs {
|
||||
if !s.seen {
|
||||
out += s.value
|
||||
out += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
return &system.File{
|
||||
Path: path.Join("etc", "coreos", "update.conf"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: out,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Units generates units for the cloud-init initializer to act on:
|
||||
// - a locksmith system.Unit, if "reboot-strategy" was set in cloud-config
|
||||
// - an update_engine system.Unit, if "group" was set in cloud-config
|
||||
func (uc UpdateConfig) Units(root string) ([]system.Unit, error) {
|
||||
var units []system.Unit
|
||||
if strategy, ok := uc["reboot-strategy"]; ok {
|
||||
ls := &system.Unit{
|
||||
Name: locksmithUnit,
|
||||
Command: "restart",
|
||||
Mask: false,
|
||||
Runtime: true,
|
||||
}
|
||||
|
||||
if strategy == "off" {
|
||||
ls.Command = "stop"
|
||||
ls.Mask = true
|
||||
}
|
||||
units = append(units, *ls)
|
||||
}
|
||||
|
||||
rue := false
|
||||
if _, ok := uc["group"]; ok {
|
||||
rue = true
|
||||
}
|
||||
if _, ok := uc["server"]; ok {
|
||||
rue = true
|
||||
}
|
||||
if rue {
|
||||
ue := system.Unit{
|
||||
Name: updateEngineUnit,
|
||||
Command: "restart",
|
||||
}
|
||||
units = append(units, ue)
|
||||
}
|
||||
|
||||
return units, nil
|
||||
}
|
@@ -1,232 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const (
|
||||
base = `SERVER=https://example.com
|
||||
GROUP=thegroupc`
|
||||
configured = base + `
|
||||
REBOOT_STRATEGY=awesome
|
||||
`
|
||||
expected = base + `
|
||||
REBOOT_STRATEGY=etcd-lock
|
||||
`
|
||||
)
|
||||
|
||||
func setupFixtures(dir string) {
|
||||
os.MkdirAll(path.Join(dir, "usr", "share", "coreos"), 0755)
|
||||
os.MkdirAll(path.Join(dir, "run", "systemd", "system"), 0755)
|
||||
|
||||
ioutil.WriteFile(path.Join(dir, "usr", "share", "coreos", "update.conf"), []byte(base), 0644)
|
||||
}
|
||||
|
||||
func TestEmptyUpdateConfig(t *testing.T) {
|
||||
uc := &UpdateConfig{}
|
||||
f, err := uc.File("")
|
||||
if err != nil {
|
||||
t.Error("unexpected error getting file from empty UpdateConfig")
|
||||
}
|
||||
if f != nil {
|
||||
t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f)
|
||||
}
|
||||
uu, err := uc.Units("")
|
||||
if err != nil {
|
||||
t.Error("unexpected error getting unit from empty UpdateConfig")
|
||||
}
|
||||
if len(uu) != 0 {
|
||||
t.Errorf("getting unit from empty UpdateConfig should have returned zero units, got %d", len(uu))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidUpdateOptions(t *testing.T) {
|
||||
uon := &updateOption{
|
||||
key: "numbers",
|
||||
prefix: "numero_",
|
||||
valid: []string{"one", "two"},
|
||||
}
|
||||
uoa := &updateOption{
|
||||
key: "any_will_do",
|
||||
prefix: "any_",
|
||||
}
|
||||
|
||||
if !uon.isValid("one") {
|
||||
t.Error("update option did not accept valid option \"one\"")
|
||||
}
|
||||
if uon.isValid("three") {
|
||||
t.Error("update option accepted invalid option \"three\"")
|
||||
}
|
||||
for _, s := range []string{"one", "asdf", "foobarbaz"} {
|
||||
if !uoa.isValid(s) {
|
||||
t.Errorf("update option with no \"valid\" field did not accept %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
uc := &UpdateConfig{"reboot-strategy": "wizzlewazzle"}
|
||||
f, err := uc.File("")
|
||||
if err == nil {
|
||||
t.Errorf("File did not give an error on invalid UpdateOption")
|
||||
}
|
||||
if f != nil {
|
||||
t.Errorf("File did not return a nil file on invalid UpdateOption")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerGroupOptions(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)
|
||||
setupFixtures(dir)
|
||||
u := &UpdateConfig{"group": "master", "server": "http://foo.com"}
|
||||
|
||||
want := `
|
||||
GROUP=master
|
||||
SERVER=http://foo.com`
|
||||
|
||||
f, err := u.File(dir)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error getting file from UpdateConfig: %v", err)
|
||||
} else if f == nil {
|
||||
t.Error("unexpectedly got empty file from UpdateConfig")
|
||||
} else {
|
||||
out := strings.Split(f.Content, "\n")
|
||||
sort.Strings(out)
|
||||
got := strings.Join(out, "\n")
|
||||
if got != want {
|
||||
t.Errorf("File has incorrect contents, got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
uu, err := u.Units(dir)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error getting units from UpdateConfig: %v", err)
|
||||
} else if len(uu) != 1 {
|
||||
t.Errorf("unexpected number of files returned from UpdateConfig: want 1, got %d", len(uu))
|
||||
} else {
|
||||
unit := uu[0]
|
||||
if unit.Name != "update-engine.service" {
|
||||
t.Errorf("bad name for generated unit: want update-engine.service, got %s", unit.Name)
|
||||
}
|
||||
if unit.Command != "restart" {
|
||||
t.Errorf("bad command for generated unit: want restart, got %s", unit.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebootStrategies(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
setupFixtures(dir)
|
||||
strategies := []struct {
|
||||
name string
|
||||
line string
|
||||
uMask bool
|
||||
uCommand string
|
||||
}{
|
||||
{"best-effort", "REBOOT_STRATEGY=best-effort", false, "restart"},
|
||||
{"etcd-lock", "REBOOT_STRATEGY=etcd-lock", false, "restart"},
|
||||
{"reboot", "REBOOT_STRATEGY=reboot", false, "restart"},
|
||||
{"off", "REBOOT_STRATEGY=off", true, "stop"},
|
||||
}
|
||||
for _, s := range strategies {
|
||||
uc := &UpdateConfig{"reboot-strategy": s.name}
|
||||
f, err := uc.File(dir)
|
||||
if err != nil {
|
||||
t.Errorf("update failed to generate file for reboot-strategy=%v: %v", s.name, err)
|
||||
} else if f == nil {
|
||||
t.Errorf("generated empty file for reboot-strategy=%v", s.name)
|
||||
} else {
|
||||
seen := false
|
||||
for _, line := range strings.Split(f.Content, "\n") {
|
||||
if line == s.line {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line)
|
||||
}
|
||||
}
|
||||
uu, err := uc.Units(dir)
|
||||
if err != nil {
|
||||
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
|
||||
} else if len(uu) != 1 {
|
||||
t.Errorf("unexpected number of units for reboot-strategy=%v: %d", s.name, len(uu))
|
||||
} else {
|
||||
u := uu[0]
|
||||
if u.Name != locksmithUnit {
|
||||
t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name)
|
||||
}
|
||||
if u.Mask != s.uMask {
|
||||
t.Errorf("unit generated for reboot strategy=%v had bad mask: %t", s.name, u.Mask)
|
||||
}
|
||||
if u.Command != s.uCommand {
|
||||
t.Errorf("unit generated for reboot strategy=%v had bad command: %v", s.name, u.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestUpdateConfWrittenToDisk(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)
|
||||
setupFixtures(dir)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
if i == 1 {
|
||||
err = ioutil.WriteFile(path.Join(dir, "etc", "coreos", "update.conf"), []byte(configured), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
uc := &UpdateConfig{"reboot-strategy": "etcd-lock"}
|
||||
|
||||
f, err := uc.File(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Processing UpdateConfig failed: %v", err)
|
||||
} else if f == nil {
|
||||
t.Fatal("Unexpectedly got nil updateconfig file")
|
||||
}
|
||||
|
||||
if _, err := system.WriteFile(f, dir); err != nil {
|
||||
t.Fatalf("Error writing update config: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "coreos", "update.conf")
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode() != os.FileMode(0644) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != expected {
|
||||
t.Fatalf("File has incorrect contents, got %v, wanted %v", string(contents), expected)
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
@@ -16,7 +17,7 @@ func ParseUserData(contents string) (interface{}, error) {
|
||||
|
||||
// Explicitly trim the header so we can handle user-data from
|
||||
// non-unix operating systems. The rest of the file is parsed
|
||||
// by goyaml, which correctly handles CRLF.
|
||||
// by yaml, which correctly handles CRLF.
|
||||
header = strings.TrimSpace(header)
|
||||
|
||||
if strings.HasPrefix(header, "#!") {
|
||||
@@ -24,7 +25,7 @@ func ParseUserData(contents string) (interface{}, error) {
|
||||
return system.Script(contents), nil
|
||||
} else if header == "#cloud-config" {
|
||||
log.Printf("Parsing user-data as cloud-config")
|
||||
return NewCloudConfig(contents)
|
||||
return config.NewCloudConfig(contents)
|
||||
} else {
|
||||
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@ package initialize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestParseHeaderCRLF(t *testing.T) {
|
||||
@@ -37,7 +39,7 @@ func TestParseConfigCRLF(t *testing.T) {
|
||||
t.Fatalf("Failed parsing config: %v", err)
|
||||
}
|
||||
|
||||
cfg := ud.(*CloudConfig)
|
||||
cfg := ud.(*config.CloudConfig)
|
||||
|
||||
if cfg.Hostname != "foo" {
|
||||
t.Error("Failed parsing hostname from config")
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
@@ -31,21 +32,21 @@ func PersistScriptInWorkspace(script system.Script, workspace string) (string, e
|
||||
|
||||
relpath := strings.TrimPrefix(tmp.Name(), workspace)
|
||||
|
||||
file := system.File{
|
||||
file := system.File{config.File{
|
||||
Path: relpath,
|
||||
RawFilePermissions: "0744",
|
||||
Content: string(script),
|
||||
}
|
||||
}}
|
||||
|
||||
return system.WriteFile(&file, workspace)
|
||||
}
|
||||
|
||||
func PersistUnitNameInWorkspace(name string, workspace string) error {
|
||||
file := system.File{
|
||||
file := system.File{config.File{
|
||||
Path: path.Join("scripts", "unit-name"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: name,
|
||||
}
|
||||
}}
|
||||
_, err := system.WriteFile(&file, workspace)
|
||||
return err
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ProcessDebianNetconf(config string) ([]InterfaceGenerator, error) {
|
||||
log.Println("Processing Debian network config")
|
||||
lines := formatConfig(config)
|
||||
stanzas, err := parseStanzas(lines)
|
||||
if err != nil {
|
||||
@@ -18,7 +20,9 @@ func ProcessDebianNetconf(config string) ([]InterfaceGenerator, error) {
|
||||
interfaces = append(interfaces, s)
|
||||
}
|
||||
}
|
||||
log.Printf("Parsed %d network interfaces\n", len(interfaces))
|
||||
|
||||
log.Println("Processed Debian network config")
|
||||
return buildInterfaces(interfaces), nil
|
||||
}
|
||||
|
142
network/digitalocean.go
Normal file
142
network/digitalocean.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean"
|
||||
)
|
||||
|
||||
func ProcessDigitalOceanNetconf(config string) ([]InterfaceGenerator, error) {
|
||||
log.Println("Processing DigitalOcean network config")
|
||||
if config == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var cfg digitalocean.Metadata
|
||||
if err := json.Unmarshal([]byte(config), &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Println("Parsing nameservers")
|
||||
nameservers, err := parseNameservers(cfg.DNS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Parsed %d nameservers\n", len(nameservers))
|
||||
|
||||
log.Println("Parsing interfaces")
|
||||
generators, err := parseInterfaces(cfg.Interfaces, nameservers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Parsed %d network interfaces\n", len(generators))
|
||||
|
||||
log.Println("Processed DigitalOcean network config")
|
||||
return generators, nil
|
||||
}
|
||||
|
||||
func parseNameservers(cfg digitalocean.DNS) ([]net.IP, error) {
|
||||
nameservers := make([]net.IP, 0, len(cfg.Nameservers))
|
||||
for _, ns := range cfg.Nameservers {
|
||||
if ip := net.ParseIP(ns); ip == nil {
|
||||
return nil, fmt.Errorf("could not parse %q as nameserver IP address", ns)
|
||||
} else {
|
||||
nameservers = append(nameservers, ip)
|
||||
}
|
||||
}
|
||||
return nameservers, nil
|
||||
}
|
||||
|
||||
func parseInterfaces(cfg digitalocean.Interfaces, nameservers []net.IP) ([]InterfaceGenerator, error) {
|
||||
generators := make([]InterfaceGenerator, 0, len(cfg.Public)+len(cfg.Private))
|
||||
for _, iface := range cfg.Public {
|
||||
if generator, err := parseInterface(iface, nameservers, true); err == nil {
|
||||
generators = append(generators, &physicalInterface{*generator})
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, iface := range cfg.Private {
|
||||
if generator, err := parseInterface(iface, []net.IP{}, false); err == nil {
|
||||
generators = append(generators, &physicalInterface{*generator})
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return generators, nil
|
||||
}
|
||||
|
||||
func parseInterface(iface digitalocean.Interface, nameservers []net.IP, useRoute bool) (*logicalInterface, error) {
|
||||
routes := make([]route, 0)
|
||||
addresses := make([]net.IPNet, 0)
|
||||
if iface.IPv4 != nil {
|
||||
var ip, mask, gateway net.IP
|
||||
if ip = net.ParseIP(iface.IPv4.IPAddress); ip == nil {
|
||||
return nil, fmt.Errorf("could not parse %q as IPv4 address", iface.IPv4.IPAddress)
|
||||
}
|
||||
if mask = net.ParseIP(iface.IPv4.Netmask); mask == nil {
|
||||
return nil, fmt.Errorf("could not parse %q as IPv4 mask", iface.IPv4.Netmask)
|
||||
}
|
||||
addresses = append(addresses, net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.IPMask(mask),
|
||||
})
|
||||
|
||||
if useRoute {
|
||||
if gateway = net.ParseIP(iface.IPv4.Gateway); gateway == nil {
|
||||
return nil, fmt.Errorf("could not parse %q as IPv4 gateway", iface.IPv4.Gateway)
|
||||
}
|
||||
routes = append(routes, route{
|
||||
destination: net.IPNet{
|
||||
IP: net.IPv4zero,
|
||||
Mask: net.IPMask(net.IPv4zero),
|
||||
},
|
||||
gateway: gateway,
|
||||
})
|
||||
}
|
||||
}
|
||||
if iface.IPv6 != nil {
|
||||
var ip, gateway net.IP
|
||||
if ip = net.ParseIP(iface.IPv6.IPAddress); ip == nil {
|
||||
return nil, fmt.Errorf("could not parse %q as IPv6 address", iface.IPv6.IPAddress)
|
||||
}
|
||||
addresses = append(addresses, net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.CIDRMask(iface.IPv6.Cidr, net.IPv6len*8),
|
||||
})
|
||||
|
||||
if useRoute {
|
||||
if gateway = net.ParseIP(iface.IPv6.Gateway); gateway == nil {
|
||||
return nil, fmt.Errorf("could not parse %q as IPv6 gateway", iface.IPv6.Gateway)
|
||||
}
|
||||
routes = append(routes, route{
|
||||
destination: net.IPNet{
|
||||
IP: net.IPv6zero,
|
||||
Mask: net.IPMask(net.IPv6zero),
|
||||
},
|
||||
gateway: gateway,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
hwaddr, err := net.ParseMAC(iface.MAC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if nameservers == nil {
|
||||
nameservers = []net.IP{}
|
||||
}
|
||||
|
||||
return &logicalInterface{
|
||||
hwaddr: hwaddr,
|
||||
config: configMethodStatic{
|
||||
addresses: addresses,
|
||||
nameservers: nameservers,
|
||||
routes: routes,
|
||||
},
|
||||
}, nil
|
||||
}
|
367
network/digitalocean_test.go
Normal file
367
network/digitalocean_test.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean"
|
||||
)
|
||||
|
||||
func TestParseNameservers(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
dns digitalocean.DNS
|
||||
nss []net.IP
|
||||
err error
|
||||
}{
|
||||
{
|
||||
dns: digitalocean.DNS{},
|
||||
nss: []net.IP{},
|
||||
},
|
||||
{
|
||||
dns: digitalocean.DNS{[]string{"1.2.3.4"}},
|
||||
nss: []net.IP{net.ParseIP("1.2.3.4")},
|
||||
},
|
||||
{
|
||||
dns: digitalocean.DNS{[]string{"bad"}},
|
||||
err: errors.New("could not parse \"bad\" as nameserver IP address"),
|
||||
},
|
||||
} {
|
||||
nss, err := parseNameservers(tt.dns)
|
||||
if !errorsEqual(tt.err, err) {
|
||||
t.Fatalf("bad error (%+v): want %q, got %q", tt.dns, tt.err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.nss, nss) {
|
||||
t.Fatalf("bad nameservers (%+v): want %#v, got %#v", tt.dns, tt.nss, nss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterface(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
cfg digitalocean.Interface
|
||||
nss []net.IP
|
||||
useRoute bool
|
||||
iface *logicalInterface
|
||||
err error
|
||||
}{
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "bad",
|
||||
},
|
||||
err: errors.New("invalid MAC address: bad"),
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
},
|
||||
nss: []net.IP{},
|
||||
iface: &logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{},
|
||||
nameservers: []net.IP{},
|
||||
routes: []route{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
},
|
||||
useRoute: true,
|
||||
nss: []net.IP{net.ParseIP("1.2.3.4")},
|
||||
iface: &logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{},
|
||||
nameservers: []net.IP{net.ParseIP("1.2.3.4")},
|
||||
routes: []route{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
IPv4: &digitalocean.Address{
|
||||
IPAddress: "bad",
|
||||
Netmask: "255.255.0.0",
|
||||
},
|
||||
},
|
||||
nss: []net.IP{},
|
||||
err: errors.New("could not parse \"bad\" as IPv4 address"),
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
IPv4: &digitalocean.Address{
|
||||
IPAddress: "1.2.3.4",
|
||||
Netmask: "bad",
|
||||
},
|
||||
},
|
||||
nss: []net.IP{},
|
||||
err: errors.New("could not parse \"bad\" as IPv4 mask"),
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
IPv4: &digitalocean.Address{
|
||||
IPAddress: "1.2.3.4",
|
||||
Netmask: "255.255.0.0",
|
||||
Gateway: "ignoreme",
|
||||
},
|
||||
},
|
||||
nss: []net.IP{},
|
||||
iface: &logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{net.IPNet{net.ParseIP("1.2.3.4"), net.IPMask(net.ParseIP("255.255.0.0"))}},
|
||||
nameservers: []net.IP{},
|
||||
routes: []route{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
IPv4: &digitalocean.Address{
|
||||
IPAddress: "1.2.3.4",
|
||||
Netmask: "255.255.0.0",
|
||||
Gateway: "bad",
|
||||
},
|
||||
},
|
||||
useRoute: true,
|
||||
nss: []net.IP{},
|
||||
err: errors.New("could not parse \"bad\" as IPv4 gateway"),
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
IPv4: &digitalocean.Address{
|
||||
IPAddress: "1.2.3.4",
|
||||
Netmask: "255.255.0.0",
|
||||
Gateway: "5.6.7.8",
|
||||
},
|
||||
},
|
||||
useRoute: true,
|
||||
nss: []net.IP{},
|
||||
iface: &logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{net.IPNet{net.ParseIP("1.2.3.4"), net.IPMask(net.ParseIP("255.255.0.0"))}},
|
||||
nameservers: []net.IP{},
|
||||
routes: []route{route{net.IPNet{net.IPv4zero, net.IPMask(net.IPv4zero)}, net.ParseIP("5.6.7.8")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
IPv6: &digitalocean.Address{
|
||||
IPAddress: "bad",
|
||||
Cidr: 16,
|
||||
},
|
||||
},
|
||||
nss: []net.IP{},
|
||||
err: errors.New("could not parse \"bad\" as IPv6 address"),
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
IPv6: &digitalocean.Address{
|
||||
IPAddress: "fe00::",
|
||||
Cidr: 16,
|
||||
Gateway: "ignoreme",
|
||||
},
|
||||
},
|
||||
nss: []net.IP{},
|
||||
iface: &logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{net.IPNet{net.ParseIP("fe00::"), net.IPMask(net.ParseIP("ffff::"))}},
|
||||
nameservers: []net.IP{},
|
||||
routes: []route{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
IPv6: &digitalocean.Address{
|
||||
IPAddress: "fe00::",
|
||||
Cidr: 16,
|
||||
Gateway: "bad",
|
||||
},
|
||||
},
|
||||
useRoute: true,
|
||||
nss: []net.IP{},
|
||||
err: errors.New("could not parse \"bad\" as IPv6 gateway"),
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interface{
|
||||
MAC: "01:23:45:67:89:AB",
|
||||
IPv6: &digitalocean.Address{
|
||||
IPAddress: "fe00::",
|
||||
Cidr: 16,
|
||||
Gateway: "fe00:1234::",
|
||||
},
|
||||
},
|
||||
useRoute: true,
|
||||
nss: []net.IP{},
|
||||
iface: &logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{net.IPNet{net.ParseIP("fe00::"), net.IPMask(net.ParseIP("ffff::"))}},
|
||||
nameservers: []net.IP{},
|
||||
routes: []route{route{net.IPNet{net.IPv6zero, net.IPMask(net.IPv6zero)}, net.ParseIP("fe00:1234::")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
iface, err := parseInterface(tt.cfg, tt.nss, tt.useRoute)
|
||||
if !errorsEqual(tt.err, err) {
|
||||
t.Fatalf("bad error (%+v): want %q, got %q", tt.cfg, tt.err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.iface, iface) {
|
||||
t.Fatalf("bad interface (%+v): want %#v, got %#v", tt.cfg, tt.iface, iface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaces(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
cfg digitalocean.Interfaces
|
||||
nss []net.IP
|
||||
ifaces []InterfaceGenerator
|
||||
err error
|
||||
}{
|
||||
{
|
||||
ifaces: []InterfaceGenerator{},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interfaces{
|
||||
Public: []digitalocean.Interface{{MAC: "01:23:45:67:89:AB"}},
|
||||
},
|
||||
ifaces: []InterfaceGenerator{
|
||||
&physicalInterface{logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{},
|
||||
nameservers: []net.IP{},
|
||||
routes: []route{},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interfaces{
|
||||
Private: []digitalocean.Interface{{MAC: "01:23:45:67:89:AB"}},
|
||||
},
|
||||
ifaces: []InterfaceGenerator{
|
||||
&physicalInterface{logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{},
|
||||
nameservers: []net.IP{},
|
||||
routes: []route{},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interfaces{
|
||||
Public: []digitalocean.Interface{{MAC: "01:23:45:67:89:AB"}},
|
||||
},
|
||||
nss: []net.IP{net.ParseIP("1.2.3.4")},
|
||||
ifaces: []InterfaceGenerator{
|
||||
&physicalInterface{logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{},
|
||||
nameservers: []net.IP{net.ParseIP("1.2.3.4")},
|
||||
routes: []route{},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interfaces{
|
||||
Private: []digitalocean.Interface{{MAC: "01:23:45:67:89:AB"}},
|
||||
},
|
||||
nss: []net.IP{net.ParseIP("1.2.3.4")},
|
||||
ifaces: []InterfaceGenerator{
|
||||
&physicalInterface{logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}),
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{},
|
||||
nameservers: []net.IP{},
|
||||
routes: []route{},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interfaces{
|
||||
Public: []digitalocean.Interface{{MAC: "bad"}},
|
||||
},
|
||||
err: errors.New("invalid MAC address: bad"),
|
||||
},
|
||||
{
|
||||
cfg: digitalocean.Interfaces{
|
||||
Private: []digitalocean.Interface{{MAC: "bad"}},
|
||||
},
|
||||
err: errors.New("invalid MAC address: bad"),
|
||||
},
|
||||
} {
|
||||
ifaces, err := parseInterfaces(tt.cfg, tt.nss)
|
||||
if !errorsEqual(tt.err, err) {
|
||||
t.Fatalf("bad error (%+v): want %q, got %q", tt.cfg, tt.err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.ifaces, ifaces) {
|
||||
t.Fatalf("bad interfaces (%+v): want %#v, got %#v", tt.cfg, tt.ifaces, ifaces)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessDigitalOceanNetconf(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
cfg string
|
||||
ifaces []InterfaceGenerator
|
||||
err error
|
||||
}{
|
||||
{
|
||||
cfg: ``,
|
||||
},
|
||||
{
|
||||
cfg: `{"dns":{"nameservers":["bad"]}}`,
|
||||
err: errors.New("could not parse \"bad\" as nameserver IP address"),
|
||||
},
|
||||
{
|
||||
cfg: `{"interfaces":{"public":[{"ipv4":{"ip_address":"bad"}}]}}`,
|
||||
err: errors.New("could not parse \"bad\" as IPv4 address"),
|
||||
},
|
||||
{
|
||||
cfg: `{}`,
|
||||
ifaces: []InterfaceGenerator{},
|
||||
},
|
||||
} {
|
||||
ifaces, err := ProcessDigitalOceanNetconf(tt.cfg)
|
||||
if !errorsEqual(tt.err, err) {
|
||||
t.Fatalf("bad error (%q): want %q, got %q", tt.cfg, tt.err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.ifaces, ifaces) {
|
||||
t.Fatalf("bad interfaces (%q): want %#v, got %#v", tt.cfg, tt.ifaces, ifaces)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func errorsEqual(a, b error) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if (a != nil && b == nil) || (a == nil && b != nil) {
|
||||
return false
|
||||
}
|
||||
return (a.Error() == b.Error())
|
||||
}
|
@@ -2,7 +2,10 @@ package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type InterfaceGenerator interface {
|
||||
@@ -11,6 +14,8 @@ type InterfaceGenerator interface {
|
||||
Netdev() string
|
||||
Link() string
|
||||
Network() string
|
||||
Type() string
|
||||
ModprobeParams() string
|
||||
}
|
||||
|
||||
type networkInterface interface {
|
||||
@@ -21,13 +26,25 @@ type networkInterface interface {
|
||||
|
||||
type logicalInterface struct {
|
||||
name string
|
||||
hwaddr net.HardwareAddr
|
||||
config configMethod
|
||||
children []networkInterface
|
||||
configDepth int
|
||||
}
|
||||
|
||||
func (i *logicalInterface) Name() string {
|
||||
return i.name
|
||||
}
|
||||
|
||||
func (i *logicalInterface) Network() string {
|
||||
config := fmt.Sprintf("[Match]\nName=%s\n\n[Network]\n", i.name)
|
||||
config := fmt.Sprintln("[Match]")
|
||||
if i.name != "" {
|
||||
config += fmt.Sprintf("Name=%s\n", i.name)
|
||||
}
|
||||
if i.hwaddr != nil {
|
||||
config += fmt.Sprintf("MACAddress=%s\n", i.hwaddr)
|
||||
}
|
||||
config += "\n[Network]\n"
|
||||
|
||||
for _, child := range i.children {
|
||||
switch iface := child.(type) {
|
||||
@@ -43,8 +60,8 @@ func (i *logicalInterface) Network() string {
|
||||
for _, nameserver := range conf.nameservers {
|
||||
config += fmt.Sprintf("DNS=%s\n", nameserver)
|
||||
}
|
||||
if conf.address.IP != nil {
|
||||
config += fmt.Sprintf("\n[Address]\nAddress=%s\n", conf.address.String())
|
||||
for _, addr := range conf.addresses {
|
||||
config += fmt.Sprintf("\n[Address]\nAddress=%s\n", addr.String())
|
||||
}
|
||||
for _, route := range conf.routes {
|
||||
config += fmt.Sprintf("\n[Route]\nDestination=%s\nGateway=%s\n", route.destination.String(), route.gateway)
|
||||
@@ -60,14 +77,26 @@ func (i *logicalInterface) Link() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (i *logicalInterface) Netdev() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (i *logicalInterface) Filename() string {
|
||||
return fmt.Sprintf("%02x-%s", i.configDepth, i.name)
|
||||
name := i.name
|
||||
if name == "" {
|
||||
name = i.hwaddr.String()
|
||||
}
|
||||
return fmt.Sprintf("%02x-%s", i.configDepth, name)
|
||||
}
|
||||
|
||||
func (i *logicalInterface) Children() []networkInterface {
|
||||
return i.children
|
||||
}
|
||||
|
||||
func (i *logicalInterface) ModprobeParams() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (i *logicalInterface) setConfigDepth(depth int) {
|
||||
i.configDepth = depth
|
||||
}
|
||||
@@ -76,37 +105,39 @@ type physicalInterface struct {
|
||||
logicalInterface
|
||||
}
|
||||
|
||||
func (p *physicalInterface) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *physicalInterface) Netdev() string {
|
||||
return ""
|
||||
func (p *physicalInterface) Type() string {
|
||||
return "physical"
|
||||
}
|
||||
|
||||
type bondInterface struct {
|
||||
logicalInterface
|
||||
slaves []string
|
||||
}
|
||||
|
||||
func (b *bondInterface) Name() string {
|
||||
return b.name
|
||||
slaves []string
|
||||
options map[string]string
|
||||
}
|
||||
|
||||
func (b *bondInterface) Netdev() string {
|
||||
return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name)
|
||||
}
|
||||
|
||||
func (b *bondInterface) Type() string {
|
||||
return "bond"
|
||||
}
|
||||
|
||||
func (b *bondInterface) ModprobeParams() string {
|
||||
params := ""
|
||||
for _, name := range sortedKeys(b.options) {
|
||||
params += fmt.Sprintf("%s=%s ", name, b.options[name])
|
||||
}
|
||||
params = strings.TrimSuffix(params, " ")
|
||||
return params
|
||||
}
|
||||
|
||||
type vlanInterface struct {
|
||||
logicalInterface
|
||||
id int
|
||||
rawDevice string
|
||||
}
|
||||
|
||||
func (v *vlanInterface) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func (v *vlanInterface) Netdev() string {
|
||||
config := fmt.Sprintf("[NetDev]\nKind=vlan\nName=%s\n", v.name)
|
||||
switch c := v.config.(type) {
|
||||
@@ -123,14 +154,18 @@ func (v *vlanInterface) Netdev() string {
|
||||
return config
|
||||
}
|
||||
|
||||
func (v *vlanInterface) Type() string {
|
||||
return "vlan"
|
||||
}
|
||||
|
||||
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
|
||||
interfaceMap := createInterfaces(stanzas)
|
||||
linkAncestors(interfaceMap)
|
||||
markConfigDepths(interfaceMap)
|
||||
|
||||
interfaces := make([]InterfaceGenerator, 0, len(interfaceMap))
|
||||
for _, iface := range interfaceMap {
|
||||
interfaces = append(interfaces, iface)
|
||||
for _, name := range sortedInterfaces(interfaceMap) {
|
||||
interfaces = append(interfaces, interfaceMap[name])
|
||||
}
|
||||
|
||||
return interfaces
|
||||
@@ -141,15 +176,22 @@ func createInterfaces(stanzas []*stanzaInterface) map[string]networkInterface {
|
||||
for _, iface := range stanzas {
|
||||
switch iface.kind {
|
||||
case interfaceBond:
|
||||
bondOptions := make(map[string]string)
|
||||
for _, k := range []string{"mode", "miimon", "lacp-rate"} {
|
||||
if v, ok := iface.options["bond-"+k]; ok && len(v) > 0 {
|
||||
bondOptions[k] = v[0]
|
||||
}
|
||||
}
|
||||
interfaceMap[iface.name] = &bondInterface{
|
||||
logicalInterface{
|
||||
name: iface.name,
|
||||
config: iface.configMethod,
|
||||
children: []networkInterface{},
|
||||
},
|
||||
iface.options["slaves"],
|
||||
iface.options["bond-slaves"],
|
||||
bondOptions,
|
||||
}
|
||||
for _, slave := range iface.options["slaves"] {
|
||||
for _, slave := range iface.options["bond-slaves"] {
|
||||
if _, ok := interfaceMap[slave]; !ok {
|
||||
interfaceMap[slave] = &physicalInterface{
|
||||
logicalInterface{
|
||||
@@ -203,7 +245,8 @@ func createInterfaces(stanzas []*stanzaInterface) map[string]networkInterface {
|
||||
}
|
||||
|
||||
func linkAncestors(interfaceMap map[string]networkInterface) {
|
||||
for _, iface := range interfaceMap {
|
||||
for _, name := range sortedInterfaces(interfaceMap) {
|
||||
iface := interfaceMap[name]
|
||||
switch i := iface.(type) {
|
||||
case *vlanInterface:
|
||||
if parent, ok := interfaceMap[i.rawDevice]; ok {
|
||||
@@ -241,13 +284,33 @@ func markConfigDepths(interfaceMap map[string]networkInterface) {
|
||||
}
|
||||
}
|
||||
for _, iface := range rootInterfaceMap {
|
||||
setDepth(iface, 0)
|
||||
setDepth(iface)
|
||||
}
|
||||
}
|
||||
|
||||
func setDepth(iface networkInterface, depth int) {
|
||||
iface.setConfigDepth(depth)
|
||||
func setDepth(iface networkInterface) int {
|
||||
maxDepth := 0
|
||||
for _, child := range iface.Children() {
|
||||
setDepth(child, depth+1)
|
||||
if depth := setDepth(child); depth > maxDepth {
|
||||
maxDepth = depth
|
||||
}
|
||||
}
|
||||
iface.setConfigDepth(maxDepth)
|
||||
return (maxDepth + 1)
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]string) (keys []string) {
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return
|
||||
}
|
||||
|
||||
func sortedInterfaces(m map[string]networkInterface) (keys []string) {
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return
|
||||
}
|
||||
|
@@ -6,215 +6,133 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPhysicalInterfaceName(t *testing.T) {
|
||||
p := physicalInterface{logicalInterface{name: "testname"}}
|
||||
if p.Name() != "testname" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhysicalInterfaceNetdev(t *testing.T) {
|
||||
p := physicalInterface{}
|
||||
if p.Netdev() != "" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhysicalInterfaceLink(t *testing.T) {
|
||||
p := physicalInterface{}
|
||||
if p.Link() != "" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhysicalInterfaceNetwork(t *testing.T) {
|
||||
p := physicalInterface{logicalInterface{
|
||||
name: "testname",
|
||||
children: []networkInterface{
|
||||
&bondInterface{
|
||||
logicalInterface{
|
||||
name: "testbond1",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
&vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testvlan1",
|
||||
},
|
||||
1,
|
||||
"",
|
||||
},
|
||||
&vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testvlan2",
|
||||
},
|
||||
1,
|
||||
"",
|
||||
},
|
||||
},
|
||||
}}
|
||||
network := `[Match]
|
||||
Name=testname
|
||||
|
||||
[Network]
|
||||
Bond=testbond1
|
||||
VLAN=testvlan1
|
||||
VLAN=testvlan2
|
||||
`
|
||||
if p.Network() != network {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondInterfaceName(t *testing.T) {
|
||||
b := bondInterface{logicalInterface{name: "testname"}, nil}
|
||||
if b.Name() != "testname" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondInterfaceNetdev(t *testing.T) {
|
||||
b := bondInterface{logicalInterface{name: "testname"}, nil}
|
||||
netdev := `[NetDev]
|
||||
Kind=bond
|
||||
Name=testname
|
||||
`
|
||||
if b.Netdev() != netdev {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondInterfaceLink(t *testing.T) {
|
||||
b := bondInterface{}
|
||||
if b.Link() != "" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondInterfaceNetwork(t *testing.T) {
|
||||
b := bondInterface{
|
||||
logicalInterface{
|
||||
name: "testname",
|
||||
config: configMethodDHCP{},
|
||||
children: []networkInterface{
|
||||
&bondInterface{
|
||||
logicalInterface{
|
||||
name: "testbond1",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
&vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testvlan1",
|
||||
},
|
||||
1,
|
||||
"",
|
||||
},
|
||||
&vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testvlan2",
|
||||
},
|
||||
1,
|
||||
"",
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
}
|
||||
network := `[Match]
|
||||
Name=testname
|
||||
|
||||
[Network]
|
||||
Bond=testbond1
|
||||
VLAN=testvlan1
|
||||
VLAN=testvlan2
|
||||
DHCP=true
|
||||
`
|
||||
if b.Network() != network {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestVLANInterfaceName(t *testing.T) {
|
||||
v := vlanInterface{logicalInterface{name: "testname"}, 1, ""}
|
||||
if v.Name() != "testname" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestVLANInterfaceNetdev(t *testing.T) {
|
||||
func TestInterfaceGenerators(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
i vlanInterface
|
||||
l string
|
||||
name string
|
||||
netdev string
|
||||
link string
|
||||
network string
|
||||
kind string
|
||||
iface InterfaceGenerator
|
||||
}{
|
||||
{
|
||||
vlanInterface{logicalInterface{name: "testname"}, 1, ""},
|
||||
"[NetDev]\nKind=vlan\nName=testname\n\n[VLAN]\nId=1\n",
|
||||
name: "",
|
||||
network: "[Match]\nMACAddress=00:01:02:03:04:05\n\n[Network]\n",
|
||||
kind: "physical",
|
||||
iface: &physicalInterface{logicalInterface{
|
||||
hwaddr: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}),
|
||||
}},
|
||||
},
|
||||
{
|
||||
vlanInterface{logicalInterface{name: "testname", config: configMethodStatic{hwaddress: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5})}}, 1, ""},
|
||||
"[NetDev]\nKind=vlan\nName=testname\nMACAddress=00:01:02:03:04:05\n\n[VLAN]\nId=1\n",
|
||||
name: "testname",
|
||||
network: "[Match]\nName=testname\n\n[Network]\nBond=testbond1\nVLAN=testvlan1\nVLAN=testvlan2\n",
|
||||
kind: "physical",
|
||||
iface: &physicalInterface{logicalInterface{
|
||||
name: "testname",
|
||||
children: []networkInterface{
|
||||
&bondInterface{logicalInterface: logicalInterface{name: "testbond1"}},
|
||||
&vlanInterface{logicalInterface: logicalInterface{name: "testvlan1"}, id: 1},
|
||||
&vlanInterface{logicalInterface: logicalInterface{name: "testvlan2"}, id: 1},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
vlanInterface{logicalInterface{name: "testname", config: configMethodDHCP{hwaddress: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5})}}, 1, ""},
|
||||
"[NetDev]\nKind=vlan\nName=testname\nMACAddress=00:01:02:03:04:05\n\n[VLAN]\nId=1\n",
|
||||
name: "testname",
|
||||
netdev: "[NetDev]\nKind=bond\nName=testname\n",
|
||||
network: "[Match]\nName=testname\n\n[Network]\nBond=testbond1\nVLAN=testvlan1\nVLAN=testvlan2\nDHCP=true\n",
|
||||
kind: "bond",
|
||||
iface: &bondInterface{logicalInterface: logicalInterface{
|
||||
name: "testname",
|
||||
config: configMethodDHCP{},
|
||||
children: []networkInterface{
|
||||
&bondInterface{logicalInterface: logicalInterface{name: "testbond1"}},
|
||||
&vlanInterface{logicalInterface: logicalInterface{name: "testvlan1"}, id: 1},
|
||||
&vlanInterface{logicalInterface: logicalInterface{name: "testvlan2"}, id: 1},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "testname",
|
||||
netdev: "[NetDev]\nKind=vlan\nName=testname\n\n[VLAN]\nId=1\n",
|
||||
network: "[Match]\nName=testname\n\n[Network]\n",
|
||||
kind: "vlan",
|
||||
iface: &vlanInterface{logicalInterface{name: "testname"}, 1, ""},
|
||||
},
|
||||
{
|
||||
name: "testname",
|
||||
netdev: "[NetDev]\nKind=vlan\nName=testname\nMACAddress=00:01:02:03:04:05\n\n[VLAN]\nId=1\n",
|
||||
network: "[Match]\nName=testname\n\n[Network]\n",
|
||||
kind: "vlan",
|
||||
iface: &vlanInterface{logicalInterface{name: "testname", config: configMethodStatic{hwaddress: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5})}}, 1, ""},
|
||||
},
|
||||
{
|
||||
name: "testname",
|
||||
netdev: "[NetDev]\nKind=vlan\nName=testname\nMACAddress=00:01:02:03:04:05\n\n[VLAN]\nId=1\n",
|
||||
network: "[Match]\nName=testname\n\n[Network]\nDHCP=true\n",
|
||||
kind: "vlan",
|
||||
iface: &vlanInterface{logicalInterface{name: "testname", config: configMethodDHCP{hwaddress: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5})}}, 1, ""},
|
||||
},
|
||||
{
|
||||
name: "testname",
|
||||
netdev: "[NetDev]\nKind=vlan\nName=testname\n\n[VLAN]\nId=0\n",
|
||||
network: "[Match]\nName=testname\n\n[Network]\nDNS=8.8.8.8\n\n[Address]\nAddress=192.168.1.100/24\n\n[Route]\nDestination=0.0.0.0/0\nGateway=1.2.3.4\n",
|
||||
kind: "vlan",
|
||||
iface: &vlanInterface{logicalInterface: logicalInterface{
|
||||
name: "testname",
|
||||
config: configMethodStatic{
|
||||
addresses: []net.IPNet{{IP: []byte{192, 168, 1, 100}, Mask: []byte{255, 255, 255, 0}}},
|
||||
nameservers: []net.IP{[]byte{8, 8, 8, 8}},
|
||||
routes: []route{route{destination: net.IPNet{IP: []byte{0, 0, 0, 0}, Mask: []byte{0, 0, 0, 0}}, gateway: []byte{1, 2, 3, 4}}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
} {
|
||||
if tt.i.Netdev() != tt.l {
|
||||
t.Fatalf("bad netdev config (%q): got %q, want %q", tt.i, tt.i.Netdev(), tt.l)
|
||||
if name := tt.iface.Name(); name != tt.name {
|
||||
t.Fatalf("bad name (%q): want %q, got %q", tt.iface, tt.name, name)
|
||||
}
|
||||
if netdev := tt.iface.Netdev(); netdev != tt.netdev {
|
||||
t.Fatalf("bad netdev (%q): want %q, got %q", tt.iface, tt.netdev, netdev)
|
||||
}
|
||||
if link := tt.iface.Link(); link != tt.link {
|
||||
t.Fatalf("bad link (%q): want %q, got %q", tt.iface, tt.link, link)
|
||||
}
|
||||
if network := tt.iface.Network(); network != tt.network {
|
||||
t.Fatalf("bad network (%q): want %q, got %q", tt.iface, tt.network, network)
|
||||
}
|
||||
if kind := tt.iface.Type(); kind != tt.kind {
|
||||
t.Fatalf("bad type (%q): want %q, got %q", tt.iface, tt.kind, kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVLANInterfaceLink(t *testing.T) {
|
||||
v := vlanInterface{}
|
||||
if v.Link() != "" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestVLANInterfaceNetwork(t *testing.T) {
|
||||
v := vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testname",
|
||||
config: configMethodStatic{
|
||||
address: net.IPNet{
|
||||
IP: []byte{192, 168, 1, 100},
|
||||
Mask: []byte{255, 255, 255, 0},
|
||||
},
|
||||
nameservers: []net.IP{
|
||||
[]byte{8, 8, 8, 8},
|
||||
},
|
||||
routes: []route{
|
||||
route{
|
||||
destination: net.IPNet{
|
||||
IP: []byte{0, 0, 0, 0},
|
||||
Mask: []byte{0, 0, 0, 0},
|
||||
},
|
||||
gateway: []byte{1, 2, 3, 4},
|
||||
},
|
||||
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",
|
||||
},
|
||||
0,
|
||||
"",
|
||||
}
|
||||
network := `[Match]
|
||||
Name=testname
|
||||
|
||||
[Network]
|
||||
DNS=8.8.8.8
|
||||
|
||||
[Address]
|
||||
Address=192.168.1.100/24
|
||||
|
||||
[Route]
|
||||
Destination=0.0.0.0/0
|
||||
Gateway=1.2.3.4
|
||||
`
|
||||
if v.Network() != network {
|
||||
t.Log(v.Network())
|
||||
t.FailNow()
|
||||
} {
|
||||
if p := tt.i.ModprobeParams(); p != tt.p {
|
||||
t.Fatalf("bad params (%q): got %s, want %s", tt.i, p, tt.p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +160,7 @@ func TestBuildInterfacesBlindBond(t *testing.T) {
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{
|
||||
"slaves": []string{"eth0"},
|
||||
"bond-slaves": []string{"eth0"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -252,16 +170,17 @@ func TestBuildInterfacesBlindBond(t *testing.T) {
|
||||
name: "bond0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 1,
|
||||
configDepth: 0,
|
||||
},
|
||||
[]string{"eth0"},
|
||||
map[string]string{},
|
||||
}
|
||||
eth0 := &physicalInterface{
|
||||
logicalInterface{
|
||||
name: "eth0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{bond0},
|
||||
configDepth: 0,
|
||||
configDepth: 1,
|
||||
},
|
||||
}
|
||||
expect := []InterfaceGenerator{bond0, eth0}
|
||||
@@ -289,7 +208,7 @@ func TestBuildInterfacesBlindVLAN(t *testing.T) {
|
||||
name: "vlan0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 1,
|
||||
configDepth: 0,
|
||||
},
|
||||
0,
|
||||
"eth0",
|
||||
@@ -299,7 +218,7 @@ func TestBuildInterfacesBlindVLAN(t *testing.T) {
|
||||
name: "eth0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{vlan0},
|
||||
configDepth: 0,
|
||||
configDepth: 1,
|
||||
},
|
||||
}
|
||||
expect := []InterfaceGenerator{eth0, vlan0}
|
||||
@@ -323,7 +242,9 @@ func TestBuildInterfaces(t *testing.T) {
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{
|
||||
"slaves": []string{"eth0"},
|
||||
"bond-slaves": []string{"eth0"},
|
||||
"bond-mode": []string{"4"},
|
||||
"bond-miimon": []string{"100"},
|
||||
},
|
||||
},
|
||||
&stanzaInterface{
|
||||
@@ -332,7 +253,7 @@ func TestBuildInterfaces(t *testing.T) {
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{
|
||||
"slaves": []string{"bond0"},
|
||||
"bond-slaves": []string{"bond0"},
|
||||
},
|
||||
},
|
||||
&stanzaInterface{
|
||||
@@ -362,7 +283,7 @@ func TestBuildInterfaces(t *testing.T) {
|
||||
name: "vlan1",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 2,
|
||||
configDepth: 0,
|
||||
},
|
||||
1,
|
||||
"bond0",
|
||||
@@ -372,7 +293,7 @@ func TestBuildInterfaces(t *testing.T) {
|
||||
name: "vlan0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 1,
|
||||
configDepth: 0,
|
||||
},
|
||||
0,
|
||||
"eth0",
|
||||
@@ -382,9 +303,10 @@ func TestBuildInterfaces(t *testing.T) {
|
||||
name: "bond1",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 2,
|
||||
configDepth: 0,
|
||||
},
|
||||
[]string{"bond0"},
|
||||
map[string]string{},
|
||||
}
|
||||
bond0 := &bondInterface{
|
||||
logicalInterface{
|
||||
@@ -394,16 +316,20 @@ func TestBuildInterfaces(t *testing.T) {
|
||||
configDepth: 1,
|
||||
},
|
||||
[]string{"eth0"},
|
||||
map[string]string{
|
||||
"mode": "4",
|
||||
"miimon": "100",
|
||||
},
|
||||
}
|
||||
eth0 := &physicalInterface{
|
||||
logicalInterface{
|
||||
name: "eth0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{bond0, vlan0},
|
||||
configDepth: 0,
|
||||
configDepth: 2,
|
||||
},
|
||||
}
|
||||
expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1}
|
||||
expect := []InterfaceGenerator{bond0, bond1, eth0, vlan0, vlan1}
|
||||
if !reflect.DeepEqual(interfaces, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
@@ -418,6 +344,8 @@ func TestFilename(t *testing.T) {
|
||||
{logicalInterface{name: "iface", configDepth: 9}, "09-iface"},
|
||||
{logicalInterface{name: "iface", configDepth: 10}, "0a-iface"},
|
||||
{logicalInterface{name: "iface", configDepth: 53}, "35-iface"},
|
||||
{logicalInterface{hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), configDepth: 1}, "01-01:23:45:67:89:ab"},
|
||||
{logicalInterface{name: "iface", hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), configDepth: 1}, "01-iface"},
|
||||
} {
|
||||
if tt.i.Filename() != tt.f {
|
||||
t.Fatalf("bad filename (%q): got %q, want %q", tt.i, tt.i.Filename(), tt.f)
|
||||
|
@@ -37,7 +37,7 @@ type route struct {
|
||||
type configMethod interface{}
|
||||
|
||||
type configMethodStatic struct {
|
||||
address net.IPNet
|
||||
addresses []net.IPNet
|
||||
nameservers []net.IP
|
||||
routes []route
|
||||
hwaddress net.HardwareAddr
|
||||
@@ -193,20 +193,21 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa
|
||||
switch confMethod {
|
||||
case "static":
|
||||
config := configMethodStatic{
|
||||
addresses: make([]net.IPNet, 1),
|
||||
routes: make([]route, 0),
|
||||
nameservers: make([]net.IP, 0),
|
||||
}
|
||||
if addresses, ok := optionMap["address"]; ok {
|
||||
if len(addresses) == 1 {
|
||||
config.address.IP = net.ParseIP(addresses[0])
|
||||
config.addresses[0].IP = net.ParseIP(addresses[0])
|
||||
}
|
||||
}
|
||||
if netmasks, ok := optionMap["netmask"]; ok {
|
||||
if len(netmasks) == 1 {
|
||||
config.address.Mask = net.IPMask(net.ParseIP(netmasks[0]).To4())
|
||||
config.addresses[0].Mask = net.IPMask(net.ParseIP(netmasks[0]).To4())
|
||||
}
|
||||
}
|
||||
if config.address.IP == nil || config.address.Mask == nil {
|
||||
if config.addresses[0].IP == nil || config.addresses[0].Mask == nil {
|
||||
return nil, fmt.Errorf("malformed static network config for %q", iface)
|
||||
}
|
||||
if gateways, ok := optionMap["gateway"]; ok {
|
||||
@@ -235,7 +236,11 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa
|
||||
for i, field := range fields[:len(fields)-1] {
|
||||
switch field {
|
||||
case "-net":
|
||||
route.destination.IP = net.ParseIP(fields[i+1])
|
||||
if _, dst, err := net.ParseCIDR(fields[i+1]); err == nil {
|
||||
route.destination = *dst
|
||||
} else {
|
||||
route.destination.IP = net.ParseIP(fields[i+1])
|
||||
}
|
||||
case "netmask":
|
||||
route.destination.Mask = net.IPMask(net.ParseIP(fields[i+1]).To4())
|
||||
case "gw":
|
||||
@@ -293,7 +298,6 @@ func parseHwaddress(options map[string][]string, iface string) (net.HardwareAddr
|
||||
}
|
||||
|
||||
func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
|
||||
options["slaves"] = options["bond-slaves"]
|
||||
return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
|
||||
}
|
||||
|
||||
|
@@ -129,7 +129,7 @@ func TestParseBondStanzaNoSlaves(t *testing.T) {
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if bond.options["slaves"] != nil {
|
||||
if bond.options["bond-slaves"] != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
@@ -152,9 +152,6 @@ func TestParseBondStanza(t *testing.T) {
|
||||
if bond.configMethod != conf {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(bond.options["slaves"], options["bond-slaves"]) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePhysicalStanza(t *testing.T) {
|
||||
@@ -197,9 +194,11 @@ func TestParseVLANStanzas(t *testing.T) {
|
||||
|
||||
func TestParseInterfaceStanzaStaticAddress(t *testing.T) {
|
||||
options := []string{"address 192.168.1.100", "netmask 255.255.255.0"}
|
||||
expect := net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 100),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
expect := []net.IPNet{
|
||||
{
|
||||
IP: net.IPv4(192, 168, 1, 100),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
},
|
||||
}
|
||||
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
|
||||
@@ -210,7 +209,7 @@ func TestParseInterfaceStanzaStaticAddress(t *testing.T) {
|
||||
if !ok {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(static.address, expect) {
|
||||
if !reflect.DeepEqual(static.addresses, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
@@ -286,31 +285,57 @@ func TestBadParseInterfaceStanzasStaticPostUp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaStaticPostUp(t *testing.T) {
|
||||
options := []string{
|
||||
"address 192.168.1.100",
|
||||
"netmask 255.255.255.0",
|
||||
"post-up route add gw 192.168.1.1 -net 192.168.1.0 netmask 255.255.255.0",
|
||||
}
|
||||
expect := []route{
|
||||
for _, tt := range []struct {
|
||||
options []string
|
||||
expect []route
|
||||
}{
|
||||
{
|
||||
destination: net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 0),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
options: []string{
|
||||
"address 192.168.1.100",
|
||||
"netmask 255.255.255.0",
|
||||
"post-up route add gw 192.168.1.1 -net 192.168.1.0 netmask 255.255.255.0",
|
||||
},
|
||||
expect: []route{
|
||||
{
|
||||
destination: net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 0),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
},
|
||||
gateway: net.IPv4(192, 168, 1, 1),
|
||||
},
|
||||
},
|
||||
gateway: net.IPv4(192, 168, 1, 1),
|
||||
},
|
||||
}
|
||||
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
static, ok := iface.configMethod.(configMethodStatic)
|
||||
if !ok {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(static.routes, expect) {
|
||||
t.FailNow()
|
||||
{
|
||||
options: []string{
|
||||
"address 192.168.1.100",
|
||||
"netmask 255.255.255.0",
|
||||
"post-up route add gw 192.168.1.1 -net 192.168.1.0/24 || true",
|
||||
},
|
||||
expect: []route{
|
||||
{
|
||||
destination: func() net.IPNet {
|
||||
if _, net, err := net.ParseCIDR("192.168.1.0/24"); err == nil {
|
||||
return *net
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}(),
|
||||
gateway: net.IPv4(192, 168, 1, 1),
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, tt.options)
|
||||
if err != nil {
|
||||
t.Fatalf("bad error (%+v): want nil, got %s\n", tt, err)
|
||||
}
|
||||
static, ok := iface.configMethod.(configMethodStatic)
|
||||
if !ok {
|
||||
t.Fatalf("bad config method (%+v): want configMethodStatic, got %T\n", tt, iface.configMethod)
|
||||
}
|
||||
if !reflect.DeepEqual(static.routes, tt.expect) {
|
||||
t.Fatalf("bad routes (%+v): want %#v, got %#v\n", tt, tt.expect, static.routes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -57,6 +57,11 @@ type HttpClient struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type Getter interface {
|
||||
Get(string) ([]byte, error)
|
||||
GetRetry(string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewHttpClient() *HttpClient {
|
||||
hc := &HttpClient{
|
||||
MaxBackoff: time.Second * 5,
|
||||
|
26
system/env.go
Normal file
26
system/env.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// dropinContents generates the contents for a drop-in unit given the config.
|
||||
// The argument must be a struct from the 'config' package.
|
||||
func dropinContents(e interface{}) string {
|
||||
et := reflect.TypeOf(e)
|
||||
ev := reflect.ValueOf(e)
|
||||
|
||||
var out string
|
||||
for i := 0; i < et.NumField(); i++ {
|
||||
if val := ev.Field(i).String(); val != "" {
|
||||
key := et.Field(i).Tag.Get("env")
|
||||
out += fmt.Sprintf("Environment=\"%s=%s\"\n", key, val)
|
||||
}
|
||||
}
|
||||
|
||||
if out == "" {
|
||||
return ""
|
||||
}
|
||||
return "[Service]\n" + out
|
||||
}
|
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
|
||||
}
|
428
system/env_file_test.go
Normal file
428
system/env_file_test.go
Normal file
@@ -0,0 +1,428 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
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{config.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{config.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{config.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{config.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{config.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{config.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{config.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{config.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{config.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{config.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)
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package initialize
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -6,15 +6,17 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
const DefaultIpv4Address = "127.0.0.1"
|
||||
|
||||
type EtcHosts string
|
||||
type EtcHosts struct {
|
||||
Config config.EtcHosts
|
||||
}
|
||||
|
||||
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
|
||||
if eh != "localhost" {
|
||||
if eh.Config != "localhost" {
|
||||
return "", errors.New("Invalid option to manage_etc_hosts")
|
||||
}
|
||||
|
||||
@@ -28,8 +30,8 @@ func (eh EtcHosts) generateEtcHosts() (out string, err error) {
|
||||
|
||||
}
|
||||
|
||||
func (eh EtcHosts) File(root string) (*system.File, error) {
|
||||
if eh == "" {
|
||||
func (eh EtcHosts) File() (*File, error) {
|
||||
if eh.Config == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -38,9 +40,9 @@ func (eh EtcHosts) File(root string) (*system.File, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &system.File{
|
||||
return &File{config.File{
|
||||
Path: path.Join("etc", "hosts"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: etcHosts,
|
||||
}, nil
|
||||
}}, nil
|
||||
}
|
46
system/etc_hosts_test.go
Normal file
46
system/etc_hosts_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestEtcdHostsFile(t *testing.T) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
config config.EtcHosts
|
||||
file *File
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"invalid",
|
||||
nil,
|
||||
fmt.Errorf("Invalid option to manage_etc_hosts"),
|
||||
},
|
||||
{
|
||||
"localhost",
|
||||
&File{config.File{
|
||||
Content: fmt.Sprintf("127.0.0.1 %s\n", hostname),
|
||||
Path: "etc/hosts",
|
||||
RawFilePermissions: "0644",
|
||||
}},
|
||||
nil,
|
||||
},
|
||||
} {
|
||||
file, err := EtcHosts{tt.config}.File()
|
||||
if !reflect.DeepEqual(tt.err, err) {
|
||||
t.Errorf("bad error (%q): want %q, got %q", tt.config, tt.err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.file, file) {
|
||||
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.file, file)
|
||||
}
|
||||
}
|
||||
}
|
25
system/etcd.go
Normal file
25
system/etcd.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
// Etcd is a top-level structure which embeds its underlying configuration,
|
||||
// config.Etcd, and provides the system-specific Unit().
|
||||
type Etcd struct {
|
||||
config.Etcd
|
||||
}
|
||||
|
||||
// Units creates a Unit file drop-in for etcd, using any configured options.
|
||||
func (ee Etcd) Units() ([]Unit, error) {
|
||||
content := dropinContents(ee.Etcd)
|
||||
if content == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return []Unit{{config.Unit{
|
||||
Name: "etcd.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: content,
|
||||
}}}, nil
|
||||
}
|
60
system/etcd_test.go
Normal file
60
system/etcd_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestEtcdUnits(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
config config.Etcd
|
||||
units []Unit
|
||||
}{
|
||||
{
|
||||
config.Etcd{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
config.Etcd{
|
||||
Discovery: "http://disco.example.com/foobar",
|
||||
PeerBindAddr: "127.0.0.1:7002",
|
||||
},
|
||||
[]Unit{{config.Unit{
|
||||
Name: "etcd.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: `[Service]
|
||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
`,
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config.Etcd{
|
||||
Name: "node001",
|
||||
Discovery: "http://disco.example.com/foobar",
|
||||
PeerBindAddr: "127.0.0.1:7002",
|
||||
},
|
||||
[]Unit{{config.Unit{
|
||||
Name: "etcd.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: `[Service]
|
||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||
Environment="ETCD_NAME=node001"
|
||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
`,
|
||||
}}},
|
||||
},
|
||||
} {
|
||||
units, err := Etcd{tt.config}.Units()
|
||||
if err != nil {
|
||||
t.Errorf("bad error (%q): want %q, got %q", tt.config, nil, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.units, units) {
|
||||
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.units, units)
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,14 +8,14 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
// File is a top-level structure which embeds its underlying configuration,
|
||||
// config.File, and provides the system-specific Permissions().
|
||||
type File struct {
|
||||
Encoding string
|
||||
Content string
|
||||
Owner string
|
||||
Path string
|
||||
RawFilePermissions string `yaml:"permissions"`
|
||||
config.File
|
||||
}
|
||||
|
||||
func (f *File) Permissions() (os.FileMode, error) {
|
||||
|
@@ -5,6 +5,8 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestWriteFileUnencodedContent(t *testing.T) {
|
||||
@@ -17,11 +19,11 @@ func TestWriteFileUnencodedContent(t *testing.T) {
|
||||
fn := "foo"
|
||||
fullPath := path.Join(dir, fn)
|
||||
|
||||
wf := File{
|
||||
wf := File{config.File{
|
||||
Path: fn,
|
||||
Content: "bar",
|
||||
RawFilePermissions: "0644",
|
||||
}
|
||||
}}
|
||||
|
||||
path, err := WriteFile(&wf, dir)
|
||||
if err != nil {
|
||||
@@ -56,11 +58,11 @@ func TestWriteFileInvalidPermission(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
wf := File{
|
||||
wf := File{config.File{
|
||||
Path: path.Join(dir, "tmp", "foo"),
|
||||
Content: "bar",
|
||||
RawFilePermissions: "pants",
|
||||
}
|
||||
}}
|
||||
|
||||
if _, err := WriteFile(&wf, dir); err == nil {
|
||||
t.Fatalf("Expected error to be raised when writing file with invalid permission")
|
||||
@@ -77,10 +79,10 @@ func TestWriteFilePermissions(t *testing.T) {
|
||||
fn := "foo"
|
||||
fullPath := path.Join(dir, fn)
|
||||
|
||||
wf := File{
|
||||
wf := File{config.File{
|
||||
Path: fn,
|
||||
RawFilePermissions: "0755",
|
||||
}
|
||||
}}
|
||||
|
||||
path, err := WriteFile(&wf, dir)
|
||||
if err != nil {
|
||||
@@ -106,11 +108,11 @@ func TestWriteFileEncodedContent(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
wf := File{
|
||||
wf := File{config.File{
|
||||
Path: path.Join(dir, "tmp", "foo"),
|
||||
Content: "",
|
||||
Encoding: "base64",
|
||||
}
|
||||
}}
|
||||
|
||||
if _, err := WriteFile(&wf, dir); err == nil {
|
||||
t.Fatalf("Expected error to be raised when writing file with encoding")
|
||||
|
26
system/fleet.go
Normal file
26
system/fleet.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
// Fleet is a top-level structure which embeds its underlying configuration,
|
||||
// config.Fleet, and provides the system-specific Unit().
|
||||
type Fleet struct {
|
||||
config.Fleet
|
||||
}
|
||||
|
||||
// Units generates a Unit file drop-in for fleet, if any fleet options were
|
||||
// configured in cloud-config
|
||||
func (fe Fleet) Units() ([]Unit, error) {
|
||||
content := dropinContents(fe.Fleet)
|
||||
if content == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return []Unit{{config.Unit{
|
||||
Name: "fleet.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: content,
|
||||
}}}, nil
|
||||
}
|
41
system/fleet_test.go
Normal file
41
system/fleet_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestFleetUnits(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
config config.Fleet
|
||||
units []Unit
|
||||
}{
|
||||
{
|
||||
config.Fleet{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
config.Fleet{
|
||||
PublicIP: "12.34.56.78",
|
||||
},
|
||||
[]Unit{{config.Unit{
|
||||
Name: "fleet.service",
|
||||
Content: `[Service]
|
||||
Environment="FLEET_PUBLIC_IP=12.34.56.78"
|
||||
`,
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
}}},
|
||||
},
|
||||
} {
|
||||
units, err := Fleet{tt.config}.Units()
|
||||
if err != nil {
|
||||
t.Errorf("bad error (%q): want %q, got %q", tt.config, nil, err)
|
||||
}
|
||||
if !reflect.DeepEqual(units, tt.units) {
|
||||
t.Errorf("bad units (%q): want %q, got %q", tt.config, tt.units, units)
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,11 +2,13 @@ package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/network"
|
||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
|
||||
)
|
||||
@@ -17,6 +19,13 @@ const (
|
||||
|
||||
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
||||
defer func() {
|
||||
if e := restartNetworkd(); e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
// TODO(crawford): Get rid of this once networkd fixes the race
|
||||
// https://bugs.freedesktop.org/show_bug.cgi?id=76077
|
||||
time.Sleep(5 * time.Second)
|
||||
if e := restartNetworkd(); e != nil {
|
||||
err = e
|
||||
}
|
||||
@@ -26,19 +35,18 @@ func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = probe8012q(); err != nil {
|
||||
if err = maybeProbe8012q(interfaces); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
return maybeProbeBonding(interfaces)
|
||||
}
|
||||
|
||||
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
||||
sysInterfaceMap := make(map[string]*net.Interface)
|
||||
if systemInterfaces, err := net.Interfaces(); err == nil {
|
||||
for _, iface := range systemInterfaces {
|
||||
// Need a copy of the interface so we can take the address
|
||||
temp := iface
|
||||
sysInterfaceMap[temp.Name] = &temp
|
||||
iface := iface
|
||||
sysInterfaceMap[iface.Name] = &iface
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
@@ -46,6 +54,7 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
||||
|
||||
for _, iface := range interfaces {
|
||||
if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok {
|
||||
log.Printf("Taking down interface %q\n", systemInterface.Name)
|
||||
if err := netlink.NetworkLinkDown(systemInterface); err != nil {
|
||||
fmt.Printf("Error while downing interface %q (%s). Continuing...\n", systemInterface.Name, err)
|
||||
}
|
||||
@@ -55,26 +64,44 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func probe8012q() error {
|
||||
return exec.Command("modprobe", "8021q").Run()
|
||||
func maybeProbe8012q(interfaces []network.InterfaceGenerator) error {
|
||||
for _, iface := range interfaces {
|
||||
if iface.Type() == "vlan" {
|
||||
log.Printf("Probing LKM %q (%q)\n", "8021q", "8021q")
|
||||
return exec.Command("modprobe", "8021q").Run()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeProbeBonding(interfaces []network.InterfaceGenerator) error {
|
||||
for _, iface := range interfaces {
|
||||
if iface.Type() == "bond" {
|
||||
args := append([]string{"bonding"}, strings.Split(iface.ModprobeParams(), " ")...)
|
||||
log.Printf("Probing LKM %q (%q)\n", "bonding", args)
|
||||
return exec.Command("modprobe", args...).Run()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartNetworkd() error {
|
||||
_, err := RunUnitCommand("restart", "systemd-networkd.service")
|
||||
log.Printf("Restarting networkd.service\n")
|
||||
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service")
|
||||
return err
|
||||
}
|
||||
|
||||
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
|
||||
for _, iface := range interfaces {
|
||||
filename := path.Join(runtimeNetworkPath, fmt.Sprintf("%s.netdev", iface.Filename()))
|
||||
filename := fmt.Sprintf("%s.netdev", iface.Filename())
|
||||
if err := writeConfig(filename, iface.Netdev()); err != nil {
|
||||
return err
|
||||
}
|
||||
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.link", iface.Filename()))
|
||||
filename = fmt.Sprintf("%s.link", iface.Filename())
|
||||
if err := writeConfig(filename, iface.Link()); err != nil {
|
||||
return err
|
||||
}
|
||||
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.network", iface.Filename()))
|
||||
filename = fmt.Sprintf("%s.network", iface.Filename())
|
||||
if err := writeConfig(filename, iface.Network()); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -82,10 +109,11 @@ func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeConfig(filename string, config string) error {
|
||||
if config == "" {
|
||||
func writeConfig(filename string, content string) error {
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(filename, []byte(config), 0444)
|
||||
log.Printf("Writing networkd unit %q\n", filename)
|
||||
_, err := WriteFile(&File{config.File{Content: content, Path: filename}}, runtimeNetworkPath)
|
||||
return err
|
||||
}
|
||||
|
32
system/oem.go
Normal file
32
system/oem.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
// OEM is a top-level structure which embeds its underlying configuration,
|
||||
// config.OEM, and provides the system-specific File().
|
||||
type OEM struct {
|
||||
config.OEM
|
||||
}
|
||||
|
||||
func (oem OEM) File() (*File, error) {
|
||||
if oem.ID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
content := fmt.Sprintf("ID=%s\n", oem.ID)
|
||||
content += fmt.Sprintf("VERSION_ID=%s\n", oem.VersionID)
|
||||
content += fmt.Sprintf("NAME=%q\n", oem.Name)
|
||||
content += fmt.Sprintf("HOME_URL=%q\n", oem.HomeURL)
|
||||
content += fmt.Sprintf("BUG_REPORT_URL=%q\n", oem.BugReportURL)
|
||||
|
||||
return &File{config.File{
|
||||
Path: path.Join("etc", "oem-release"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: content,
|
||||
}}, nil
|
||||
}
|
47
system/oem_test.go
Normal file
47
system/oem_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestOEMFile(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
config config.OEM
|
||||
file *File
|
||||
}{
|
||||
{
|
||||
config.OEM{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
config.OEM{
|
||||
ID: "rackspace",
|
||||
Name: "Rackspace Cloud Servers",
|
||||
VersionID: "168.0.0",
|
||||
HomeURL: "https://www.rackspace.com/cloud/servers/",
|
||||
BugReportURL: "https://github.com/coreos/coreos-overlay",
|
||||
},
|
||||
&File{config.File{
|
||||
Path: "etc/oem-release",
|
||||
RawFilePermissions: "0644",
|
||||
Content: `ID=rackspace
|
||||
VERSION_ID=168.0.0
|
||||
NAME="Rackspace Cloud Servers"
|
||||
HOME_URL="https://www.rackspace.com/cloud/servers/"
|
||||
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
|
||||
`,
|
||||
}},
|
||||
},
|
||||
} {
|
||||
file, err := OEM{tt.config}.File()
|
||||
if err != nil {
|
||||
t.Errorf("bad error (%q): want %q, got %q", tt.config, nil, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.file, file) {
|
||||
t.Errorf("bad file (%q): want %#v, got %#v", tt.config, tt.file, file)
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,66 +10,25 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
|
||||
)
|
||||
|
||||
func NewUnitManager(root string) UnitManager {
|
||||
return &systemd{root}
|
||||
}
|
||||
|
||||
type systemd struct {
|
||||
root string
|
||||
}
|
||||
|
||||
// fakeMachineID is placed on non-usr CoreOS images and should
|
||||
// never be used as a true MachineID
|
||||
const fakeMachineID = "42000000000000000000000000000042"
|
||||
|
||||
// Name for drop-in service configuration files created by cloudconfig
|
||||
const cloudConfigDropIn = "20-cloudinit.conf"
|
||||
|
||||
type Unit struct {
|
||||
Name string
|
||||
Mask bool
|
||||
Enable bool
|
||||
Runtime bool
|
||||
Content string
|
||||
Command string
|
||||
|
||||
// For drop-in units, a cloudinit.conf is generated.
|
||||
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
|
||||
// until the correct behaviour for multiple drop-in units is determined.
|
||||
DropIn bool `yaml:"-"`
|
||||
}
|
||||
|
||||
func (u *Unit) Type() string {
|
||||
ext := filepath.Ext(u.Name)
|
||||
return strings.TrimLeft(ext, ".")
|
||||
}
|
||||
|
||||
func (u *Unit) Group() (group string) {
|
||||
t := u.Type()
|
||||
if t == "network" || t == "netdev" || t == "link" {
|
||||
group = "network"
|
||||
} else {
|
||||
group = "system"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Script []byte
|
||||
|
||||
// Destination builds the appropriate absolute file path for
|
||||
// the Unit. The root argument indicates the effective base
|
||||
// directory of the system (similar to a chroot).
|
||||
func (u *Unit) Destination(root string) string {
|
||||
dir := "etc"
|
||||
if u.Runtime {
|
||||
dir = "run"
|
||||
}
|
||||
|
||||
if u.DropIn {
|
||||
return path.Join(root, dir, "systemd", u.Group(), fmt.Sprintf("%s.d", u.Name), cloudConfigDropIn)
|
||||
} else {
|
||||
return path.Join(root, dir, "systemd", u.Group(), u.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// PlaceUnit writes a unit file at the provided destination, creating
|
||||
// parent directories as necessary.
|
||||
func PlaceUnit(u *Unit, dst string) error {
|
||||
func (s *systemd) PlaceUnit(u *Unit, dst string) error {
|
||||
dir := filepath.Dir(dst)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
|
||||
@@ -77,11 +36,11 @@ func PlaceUnit(u *Unit, dst string) error {
|
||||
}
|
||||
}
|
||||
|
||||
file := File{
|
||||
file := File{config.File{
|
||||
Path: filepath.Base(dst),
|
||||
Content: u.Content,
|
||||
RawFilePermissions: "0644",
|
||||
}
|
||||
}}
|
||||
|
||||
_, err := WriteFile(&file, dir)
|
||||
if err != nil {
|
||||
@@ -91,7 +50,7 @@ func PlaceUnit(u *Unit, dst string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnableUnitFile(unit string, runtime bool) error {
|
||||
func (s *systemd) EnableUnitFile(unit string, runtime bool) error {
|
||||
conn, err := dbus.New()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -102,7 +61,7 @@ func EnableUnitFile(unit string, runtime bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func RunUnitCommand(command, unit string) (string, error) {
|
||||
func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
|
||||
conn, err := dbus.New()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -131,7 +90,7 @@ func RunUnitCommand(command, unit string) (string, error) {
|
||||
return fn(unit, "replace")
|
||||
}
|
||||
|
||||
func DaemonReload() error {
|
||||
func (s *systemd) DaemonReload() error {
|
||||
conn, err := dbus.New()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -140,6 +99,57 @@ func DaemonReload() error {
|
||||
return conn.Reload()
|
||||
}
|
||||
|
||||
// MaskUnit masks the given Unit by symlinking its unit file to
|
||||
// /dev/null, analogous to `systemctl mask`.
|
||||
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
||||
// file at the location*, to ensure that the mask will succeed.
|
||||
func (s *systemd) MaskUnit(unit *Unit) error {
|
||||
masked := unit.Destination(s.root)
|
||||
if _, err := os.Stat(masked); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := os.Remove(masked); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink("/dev/null", masked)
|
||||
}
|
||||
|
||||
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
|
||||
// associated with the given Unit is empty or appears to be a symlink to
|
||||
// /dev/null, it is removed.
|
||||
func (s *systemd) UnmaskUnit(unit *Unit) error {
|
||||
masked := unit.Destination(s.root)
|
||||
ne, err := nullOrEmpty(masked)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ne {
|
||||
log.Printf("%s is not null or empty, refusing to unmask", masked)
|
||||
return nil
|
||||
}
|
||||
return os.Remove(masked)
|
||||
}
|
||||
|
||||
// nullOrEmpty checks whether a given path appears to be an empty regular file
|
||||
// or a symlink to /dev/null
|
||||
func nullOrEmpty(path string) (bool, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
m := fi.Mode()
|
||||
if m.IsRegular() && fi.Size() <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
if m&os.ModeCharDevice > 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func ExecuteScript(scriptPath string) (string, error) {
|
||||
props := []dbus.Property{
|
||||
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
|
||||
@@ -178,54 +188,3 @@ func MachineID(root string) string {
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// MaskUnit masks the given Unit by symlinking its unit file to
|
||||
// /dev/null, analogous to `systemctl mask`.
|
||||
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
||||
// file at the location*, to ensure that the mask will succeed.
|
||||
func MaskUnit(unit *Unit, root string) error {
|
||||
masked := unit.Destination(root)
|
||||
if _, err := os.Stat(masked); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := os.Remove(masked); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink("/dev/null", masked)
|
||||
}
|
||||
|
||||
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
|
||||
// associated with the given Unit is empty or appears to be a symlink to
|
||||
// /dev/null, it is removed.
|
||||
func UnmaskUnit(unit *Unit, root string) error {
|
||||
masked := unit.Destination(root)
|
||||
ne, err := nullOrEmpty(masked)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ne {
|
||||
log.Printf("%s is not null or empty, refusing to unmask", masked)
|
||||
return nil
|
||||
}
|
||||
return os.Remove(masked)
|
||||
}
|
||||
|
||||
// nullOrEmpty checks whether a given path appears to be an empty regular file
|
||||
// or a symlink to /dev/null
|
||||
func nullOrEmpty(path string) (bool, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
m := fi.Mode()
|
||||
if m.IsRegular() && fi.Size() <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
if m&os.ModeCharDevice > 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
@@ -5,10 +5,12 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestPlaceNetworkUnit(t *testing.T) {
|
||||
u := Unit{
|
||||
u := Unit{config.Unit{
|
||||
Name: "50-eth0.network",
|
||||
Runtime: true,
|
||||
Content: `[Match]
|
||||
@@ -17,7 +19,7 @@ Name=eth47
|
||||
[Network]
|
||||
Address=10.209.171.177/19
|
||||
`,
|
||||
}
|
||||
}}
|
||||
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
@@ -25,13 +27,15 @@ Address=10.209.171.177/19
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := &systemd{dir}
|
||||
|
||||
dst := u.Destination(dir)
|
||||
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
||||
if dst != expectDst {
|
||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
|
||||
if err := PlaceUnit(&u, dst); err != nil {
|
||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("PlaceUnit failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -64,10 +68,10 @@ func TestUnitDestination(t *testing.T) {
|
||||
dir := "/some/dir"
|
||||
name := "foobar.service"
|
||||
|
||||
u := Unit{
|
||||
u := Unit{config.Unit{
|
||||
Name: name,
|
||||
DropIn: false,
|
||||
}
|
||||
}}
|
||||
|
||||
dst := u.Destination(dir)
|
||||
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
|
||||
@@ -85,14 +89,14 @@ func TestUnitDestination(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaceMountUnit(t *testing.T) {
|
||||
u := Unit{
|
||||
u := Unit{config.Unit{
|
||||
Name: "media-state.mount",
|
||||
Runtime: false,
|
||||
Content: `[Mount]
|
||||
What=/dev/sdb1
|
||||
Where=/media/state
|
||||
`,
|
||||
}
|
||||
}}
|
||||
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
@@ -100,13 +104,15 @@ Where=/media/state
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := &systemd{dir}
|
||||
|
||||
dst := u.Destination(dir)
|
||||
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
||||
if dst != expectDst {
|
||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
|
||||
if err := PlaceUnit(&u, dst); err != nil {
|
||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("PlaceUnit failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -155,9 +161,11 @@ func TestMaskUnit(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := &systemd{dir}
|
||||
|
||||
// Ensure mask works with units that do not currently exist
|
||||
uf := &Unit{Name: "foo.service"}
|
||||
if err := MaskUnit(uf, dir); err != nil {
|
||||
uf := &Unit{config.Unit{Name: "foo.service"}}
|
||||
if err := sd.MaskUnit(uf); err != nil {
|
||||
t.Fatalf("Unable to mask new unit: %v", err)
|
||||
}
|
||||
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
|
||||
@@ -170,12 +178,12 @@ func TestMaskUnit(t *testing.T) {
|
||||
}
|
||||
|
||||
// Ensure mask works with unit files that already exist
|
||||
ub := &Unit{Name: "bar.service"}
|
||||
ub := &Unit{config.Unit{Name: "bar.service"}}
|
||||
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
|
||||
if _, err := os.Create(barPath); err != nil {
|
||||
t.Fatalf("Error creating new unit file: %v", err)
|
||||
}
|
||||
if err := MaskUnit(ub, dir); err != nil {
|
||||
if err := sd.MaskUnit(ub); err != nil {
|
||||
t.Fatalf("Unable to mask existing unit: %v", err)
|
||||
}
|
||||
barTgt, err := os.Readlink(barPath)
|
||||
@@ -194,12 +202,14 @@ func TestUnmaskUnit(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
nilUnit := &Unit{Name: "null.service"}
|
||||
if err := UnmaskUnit(nilUnit, dir); err != nil {
|
||||
sd := &systemd{dir}
|
||||
|
||||
nilUnit := &Unit{config.Unit{Name: "null.service"}}
|
||||
if err := sd.UnmaskUnit(nilUnit); err != nil {
|
||||
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
|
||||
}
|
||||
|
||||
uf := &Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}
|
||||
uf := &Unit{config.Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}}
|
||||
dst := uf.Destination(dir)
|
||||
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
|
||||
t.Fatalf("Unable to create unit directory: %v", err)
|
||||
@@ -211,7 +221,7 @@ func TestUnmaskUnit(t *testing.T) {
|
||||
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
|
||||
t.Fatalf("Unable to write unit file: %v", err)
|
||||
}
|
||||
if err := UnmaskUnit(uf, dir); err != nil {
|
||||
if err := sd.UnmaskUnit(uf); err != nil {
|
||||
t.Errorf("unmask of non-empty unit returned unexpected error: %v", err)
|
||||
}
|
||||
got, _ := ioutil.ReadFile(dst)
|
||||
@@ -219,12 +229,12 @@ func TestUnmaskUnit(t *testing.T) {
|
||||
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
|
||||
}
|
||||
|
||||
ub := &Unit{Name: "bar.service"}
|
||||
ub := &Unit{config.Unit{Name: "bar.service"}}
|
||||
dst = ub.Destination(dir)
|
||||
if err := os.Symlink("/dev/null", dst); err != nil {
|
||||
t.Fatalf("Unable to create masked unit: %v", err)
|
||||
}
|
||||
if err := UnmaskUnit(ub, dir); err != nil {
|
||||
if err := sd.UnmaskUnit(ub); err != nil {
|
||||
t.Errorf("unmask of unit returned unexpected error: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(dst); !os.IsNotExist(err) {
|
||||
|
44
system/unit.go
Normal file
44
system/unit.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Unit is a top-level structure which embeds its underlying configuration,
|
||||
// config.Unit, and provides the system-specific Destination().
|
||||
type Unit struct {
|
||||
config.Unit
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
137
system/update.go
Normal file
137
system/update.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
const (
|
||||
locksmithUnit = "locksmithd.service"
|
||||
updateEngineUnit = "update-engine.service"
|
||||
)
|
||||
|
||||
// Update is a top-level structure which contains its underlying configuration,
|
||||
// config.Update, a function for reading the configuration (the default
|
||||
// implementation reading from the filesystem), and provides the system-specific
|
||||
// File() and Unit().
|
||||
type Update struct {
|
||||
Config config.Update
|
||||
ReadConfig func() (io.Reader, error)
|
||||
}
|
||||
|
||||
func DefaultReadConfig() (io.Reader, error) {
|
||||
etcUpdate := path.Join("/etc", "coreos", "update.conf")
|
||||
usrUpdate := path.Join("/usr", "share", "coreos", "update.conf")
|
||||
|
||||
f, err := os.Open(etcUpdate)
|
||||
if os.IsNotExist(err) {
|
||||
f, err = os.Open(usrUpdate)
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
// File generates an `/etc/coreos/update.conf` file (if any update
|
||||
// configuration options are set in cloud-config) by either rewriting the
|
||||
// existing file on disk, or starting from `/usr/share/coreos/update.conf`
|
||||
func (uc Update) File() (*File, error) {
|
||||
if config.IsZero(uc.Config) {
|
||||
return nil, nil
|
||||
}
|
||||
if err := config.AssertValid(uc.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate the list of possible substitutions to be performed based on the options that are configured
|
||||
subs := map[string]string{}
|
||||
uct := reflect.TypeOf(uc.Config)
|
||||
ucv := reflect.ValueOf(uc.Config)
|
||||
for i := 0; i < uct.NumField(); i++ {
|
||||
val := ucv.Field(i).String()
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
env := uct.Field(i).Tag.Get("env")
|
||||
subs[env] = fmt.Sprintf("%s=%s", env, val)
|
||||
}
|
||||
|
||||
conf, err := uc.ReadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scanner := bufio.NewScanner(conf)
|
||||
|
||||
var out string
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
for env, value := range subs {
|
||||
if strings.HasPrefix(line, env) {
|
||||
line = value
|
||||
delete(subs, env)
|
||||
break
|
||||
}
|
||||
}
|
||||
out += line
|
||||
out += "\n"
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range sortedKeys(subs) {
|
||||
out += subs[key]
|
||||
out += "\n"
|
||||
}
|
||||
|
||||
return &File{config.File{
|
||||
Path: path.Join("etc", "coreos", "update.conf"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: out,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// Units generates units for the cloud-init initializer to act on:
|
||||
// - a locksmith Unit, if "reboot-strategy" was set in cloud-config
|
||||
// - an update_engine Unit, if "group" or "server" was set in cloud-config
|
||||
func (uc Update) Units() ([]Unit, error) {
|
||||
var units []Unit
|
||||
if uc.Config.RebootStrategy != "" {
|
||||
ls := &Unit{config.Unit{
|
||||
Name: locksmithUnit,
|
||||
Command: "restart",
|
||||
Mask: false,
|
||||
Runtime: true,
|
||||
}}
|
||||
|
||||
if uc.Config.RebootStrategy == "off" {
|
||||
ls.Command = "stop"
|
||||
ls.Mask = true
|
||||
}
|
||||
units = append(units, *ls)
|
||||
}
|
||||
|
||||
if uc.Config.Group != "" || uc.Config.Server != "" {
|
||||
ue := Unit{config.Unit{
|
||||
Name: updateEngineUnit,
|
||||
Command: "restart",
|
||||
}}
|
||||
units = append(units, ue)
|
||||
}
|
||||
|
||||
return units, nil
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]string) (keys []string) {
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return
|
||||
}
|
151
system/update_test.go
Normal file
151
system/update_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func testReadConfig(config string) func() (io.Reader, error) {
|
||||
return func() (io.Reader, error) {
|
||||
return strings.NewReader(config), nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUnits(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
config config.Update
|
||||
units []Unit
|
||||
err error
|
||||
}{
|
||||
{
|
||||
config: config.Update{},
|
||||
},
|
||||
{
|
||||
config: config.Update{Group: "master", Server: "http://foo.com"},
|
||||
units: []Unit{{config.Unit{
|
||||
Name: "update-engine.service",
|
||||
Command: "restart",
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "best-effort"},
|
||||
units: []Unit{{config.Unit{
|
||||
Name: "locksmithd.service",
|
||||
Command: "restart",
|
||||
Runtime: true,
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "etcd-lock"},
|
||||
units: []Unit{{config.Unit{
|
||||
Name: "locksmithd.service",
|
||||
Command: "restart",
|
||||
Runtime: true,
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "reboot"},
|
||||
units: []Unit{{config.Unit{
|
||||
Name: "locksmithd.service",
|
||||
Command: "restart",
|
||||
Runtime: true,
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "off"},
|
||||
units: []Unit{{config.Unit{
|
||||
Name: "locksmithd.service",
|
||||
Command: "stop",
|
||||
Runtime: true,
|
||||
Mask: true,
|
||||
}}},
|
||||
},
|
||||
} {
|
||||
units, err := Update{tt.config, testReadConfig("")}.Units()
|
||||
if !reflect.DeepEqual(tt.err, err) {
|
||||
t.Errorf("bad error (%q): want %q, got %q", tt.config, tt.err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.units, units) {
|
||||
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.units, units)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFile(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
config config.Update
|
||||
orig string
|
||||
file *File
|
||||
err error
|
||||
}{
|
||||
{
|
||||
config: config.Update{},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "wizzlewazzle"},
|
||||
err: errors.New("invalid value \"wizzlewazzle\" for option \"RebootStrategy\" (valid options: \"best-effort,etcd-lock,reboot,off\")"),
|
||||
},
|
||||
{
|
||||
config: config.Update{Group: "master", Server: "http://foo.com"},
|
||||
file: &File{config.File{
|
||||
Content: "GROUP=master\nSERVER=http://foo.com\n",
|
||||
Path: "etc/coreos/update.conf",
|
||||
RawFilePermissions: "0644",
|
||||
}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "best-effort"},
|
||||
file: &File{config.File{
|
||||
Content: "REBOOT_STRATEGY=best-effort\n",
|
||||
Path: "etc/coreos/update.conf",
|
||||
RawFilePermissions: "0644",
|
||||
}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "etcd-lock"},
|
||||
file: &File{config.File{
|
||||
Content: "REBOOT_STRATEGY=etcd-lock\n",
|
||||
Path: "etc/coreos/update.conf",
|
||||
RawFilePermissions: "0644",
|
||||
}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "reboot"},
|
||||
file: &File{config.File{
|
||||
Content: "REBOOT_STRATEGY=reboot\n",
|
||||
Path: "etc/coreos/update.conf",
|
||||
RawFilePermissions: "0644",
|
||||
}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "off"},
|
||||
file: &File{config.File{
|
||||
Content: "REBOOT_STRATEGY=off\n",
|
||||
Path: "etc/coreos/update.conf",
|
||||
RawFilePermissions: "0644",
|
||||
}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "etcd-lock"},
|
||||
orig: "SERVER=https://example.com\nGROUP=thegroupc\nREBOOT_STRATEGY=awesome",
|
||||
file: &File{config.File{
|
||||
Content: "SERVER=https://example.com\nGROUP=thegroupc\nREBOOT_STRATEGY=etcd-lock\n",
|
||||
Path: "etc/coreos/update.conf",
|
||||
RawFilePermissions: "0644",
|
||||
}},
|
||||
},
|
||||
} {
|
||||
file, err := Update{tt.config, testReadConfig(tt.orig)}.File()
|
||||
if !reflect.DeepEqual(tt.err, err) {
|
||||
t.Errorf("bad error (%q): want %q, got %q", tt.config, tt.err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.file, file) {
|
||||
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.file, file)
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,30 +6,16 @@ import (
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Name string `yaml:"name"`
|
||||
PasswordHash string `yaml:"passwd"`
|
||||
SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"`
|
||||
SSHImportGithubUser string `yaml:"coreos-ssh-import-github"`
|
||||
SSHImportURL string `yaml:"coreos-ssh-import-url"`
|
||||
GECOS string `yaml:"gecos"`
|
||||
Homedir string `yaml:"homedir"`
|
||||
NoCreateHome bool `yaml:"no-create-home"`
|
||||
PrimaryGroup string `yaml:"primary-group"`
|
||||
Groups []string `yaml:"groups"`
|
||||
NoUserGroup bool `yaml:"no-user-group"`
|
||||
System bool `yaml:"system"`
|
||||
NoLogInit bool `yaml:"no-log-init"`
|
||||
}
|
||||
|
||||
func UserExists(u *User) bool {
|
||||
func UserExists(u *config.User) bool {
|
||||
_, err := user.Lookup(u.Name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func CreateUser(u *User) error {
|
||||
func CreateUser(u *config.User) error {
|
||||
args := []string{}
|
||||
|
||||
if u.PasswordHash != "" {
|
||||
|
25
test
25
test
@@ -13,12 +13,27 @@ COVER=${COVER:-"-cover"}
|
||||
|
||||
source ./build
|
||||
|
||||
declare -a TESTPKGS=(initialize system datasource pkg network)
|
||||
declare -a TESTPKGS=(
|
||||
config
|
||||
datasource
|
||||
datasource/configdrive
|
||||
datasource/file
|
||||
datasource/metadata
|
||||
datasource/metadata/cloudsigma
|
||||
datasource/metadata/digitalocean
|
||||
datasource/metadata/ec2
|
||||
datasource/proc_cmdline
|
||||
datasource/url
|
||||
initialize
|
||||
network
|
||||
pkg
|
||||
system
|
||||
)
|
||||
|
||||
if [ -z "$PKG" ]; then
|
||||
GOFMTPATH="$TESTPKGS coreos-cloudinit.go"
|
||||
GOFMTPATH="${TESTPKGS[*]} coreos-cloudinit.go"
|
||||
# prepend repo path to each package
|
||||
TESTPKGS="${TESTPKGS[@]/#/${REPO_PATH}/} ./"
|
||||
TESTPKGS="${TESTPKGS[*]/#/${REPO_PATH}/} ./"
|
||||
else
|
||||
GOFMTPATH="$TESTPKGS"
|
||||
# strip out slashes and dots from PKG=./foo/
|
||||
@@ -33,5 +48,9 @@ go test ${COVER} $@ ${TESTPKGS}
|
||||
|
||||
echo "Checking gofmt..."
|
||||
fmtRes=$(gofmt -l $GOFMTPATH)
|
||||
if [ -n "$fmtRes" ]; then
|
||||
echo "$fmtRes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Success"
|
||||
|
202
third_party/github.com/cloudsigma/cepgo/LICENSE
vendored
Normal file
202
third_party/github.com/cloudsigma/cepgo/LICENSE
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
43
third_party/github.com/cloudsigma/cepgo/README.md
vendored
Normal file
43
third_party/github.com/cloudsigma/cepgo/README.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
cepgo
|
||||
=====
|
||||
|
||||
Cepko implements easy-to-use communication with CloudSigma's VMs through a
|
||||
virtual serial port without bothering with formatting the messages properly nor
|
||||
parsing the output with the specific and sometimes confusing shell tools for
|
||||
that purpose.
|
||||
|
||||
Having the server definition accessible by the VM can be useful in various
|
||||
ways. For example it is possible to easily determine from within the VM, which
|
||||
network interfaces are connected to public and which to private network.
|
||||
Another use is to pass some data to initial VM setup scripts, like setting the
|
||||
hostname to the VM name or passing ssh public keys through server meta.
|
||||
|
||||
Example usage:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudsigma/cepgo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := cepgo.NewCepgo()
|
||||
result, err := c.Meta()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("%#v", result)
|
||||
}
|
||||
|
||||
Output:
|
||||
|
||||
map[string]interface {}{
|
||||
"optimize_for":"custom",
|
||||
"ssh_public_key":"ssh-rsa AAA...",
|
||||
"description":"[...]",
|
||||
}
|
||||
|
||||
For more information take a look at the Server Context section of CloudSigma
|
||||
API Docs: http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html
|
186
third_party/github.com/cloudsigma/cepgo/cepgo.go
vendored
Normal file
186
third_party/github.com/cloudsigma/cepgo/cepgo.go
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
// Cepko implements easy-to-use communication with CloudSigma's VMs through a
|
||||
// virtual serial port without bothering with formatting the messages properly
|
||||
// nor parsing the output with the specific and sometimes confusing shell tools
|
||||
// for that purpose.
|
||||
//
|
||||
// Having the server definition accessible by the VM can be useful in various
|
||||
// ways. For example it is possible to easily determine from within the VM,
|
||||
// which network interfaces are connected to public and which to private
|
||||
// network. Another use is to pass some data to initial VM setup scripts, like
|
||||
// setting the hostname to the VM name or passing ssh public keys through
|
||||
// server meta.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "fmt"
|
||||
//
|
||||
// "github.com/cloudsigma/cepgo"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// c := cepgo.NewCepgo()
|
||||
// result, err := c.Meta()
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// fmt.Printf("%#v", result)
|
||||
// }
|
||||
//
|
||||
// Output:
|
||||
//
|
||||
// map[string]string{
|
||||
// "optimize_for":"custom",
|
||||
// "ssh_public_key":"ssh-rsa AAA...",
|
||||
// "description":"[...]",
|
||||
// }
|
||||
//
|
||||
// For more information take a look at the Server Context section API Docs:
|
||||
// http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html
|
||||
package cepgo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/tarm/goserial"
|
||||
)
|
||||
|
||||
const (
|
||||
requestPattern = "<\n%s\n>"
|
||||
EOT = '\x04' // End Of Transmission
|
||||
)
|
||||
|
||||
var (
|
||||
SerialPort string = "/dev/ttyS1"
|
||||
Baud int = 115200
|
||||
)
|
||||
|
||||
// Sets the serial port. If the operating system is windows CloudSigma's server
|
||||
// context is at COM2 port, otherwise (linux, freebsd, darwin) the port is
|
||||
// being left to the default /dev/ttyS1.
|
||||
func init() {
|
||||
if runtime.GOOS == "windows" {
|
||||
SerialPort = "COM2"
|
||||
}
|
||||
}
|
||||
|
||||
// The default fetcher makes the connection to the serial port,
|
||||
// writes given query and reads until the EOT symbol.
|
||||
func fetchViaSerialPort(key string) ([]byte, error) {
|
||||
config := &serial.Config{Name: SerialPort, Baud: Baud}
|
||||
connection, err := serial.OpenPort(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(requestPattern, key)
|
||||
if _, err := connection.Write([]byte(query)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(connection)
|
||||
answer, err := reader.ReadBytes(EOT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return answer[0 : len(answer)-1], nil
|
||||
}
|
||||
|
||||
// Queries to the serial port can be executed only from instance of this type.
|
||||
// The result from each of them can be either interface{}, map[string]string or
|
||||
// a single in case of single value is returned. There is also a public metod
|
||||
// who directly calls the fetcher and returns raw []byte from the serial port.
|
||||
type Cepgo struct {
|
||||
fetcher func(string) ([]byte, error)
|
||||
}
|
||||
|
||||
// Creates a Cepgo instance with the default serial port fetcher.
|
||||
func NewCepgo() *Cepgo {
|
||||
cepgo := new(Cepgo)
|
||||
cepgo.fetcher = fetchViaSerialPort
|
||||
return cepgo
|
||||
}
|
||||
|
||||
// Creates a Cepgo instance with custom fetcher.
|
||||
func NewCepgoFetcher(fetcher func(string) ([]byte, error)) *Cepgo {
|
||||
cepgo := new(Cepgo)
|
||||
cepgo.fetcher = fetcher
|
||||
return cepgo
|
||||
}
|
||||
|
||||
// Fetches raw []byte from the serial port using directly the fetcher member.
|
||||
func (c *Cepgo) FetchRaw(key string) ([]byte, error) {
|
||||
return c.fetcher(key)
|
||||
}
|
||||
|
||||
// Fetches a single key and tries to unmarshal the result to json and returns
|
||||
// it. If the unmarshalling fails it's safe to assume the result it's just a
|
||||
// string and returns it.
|
||||
func (c *Cepgo) Key(key string) (interface{}, error) {
|
||||
var result interface{}
|
||||
|
||||
fetched, err := c.FetchRaw(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(fetched, &result)
|
||||
if err != nil {
|
||||
return string(fetched), nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Fetches all the server context. Equivalent of c.Key("")
|
||||
func (c *Cepgo) All() (interface{}, error) {
|
||||
return c.Key("")
|
||||
}
|
||||
|
||||
// Fetches only the object meta field and makes sure to return a proper
|
||||
// map[string]string
|
||||
func (c *Cepgo) Meta() (map[string]string, error) {
|
||||
rawMeta, err := c.Key("/meta/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return typeAssertToMapOfStrings(rawMeta)
|
||||
}
|
||||
|
||||
// Fetches only the global context and makes sure to return a proper
|
||||
// map[string]string
|
||||
func (c *Cepgo) GlobalContext() (map[string]string, error) {
|
||||
rawContext, err := c.Key("/global_context/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return typeAssertToMapOfStrings(rawContext)
|
||||
}
|
||||
|
||||
// Just a little helper function that uses type assertions in order to convert
|
||||
// a interface{} to map[string]string if this is possible.
|
||||
func typeAssertToMapOfStrings(raw interface{}) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
dictionary, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("Received bytes are formatted badly")
|
||||
}
|
||||
|
||||
for key, rawValue := range dictionary {
|
||||
if value, ok := rawValue.(string); ok {
|
||||
result[key] = value
|
||||
} else {
|
||||
return nil, errors.New("Server context metadata is formatted badly")
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
122
third_party/github.com/cloudsigma/cepgo/cepgo_test.go
vendored
Normal file
122
third_party/github.com/cloudsigma/cepgo/cepgo_test.go
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
package cepgo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func fetchMock(key string) ([]byte, error) {
|
||||
context := []byte(`{
|
||||
"context": true,
|
||||
"cpu": 4000,
|
||||
"cpu_model": null,
|
||||
"cpus_instead_of_cores": false,
|
||||
"enable_numa": false,
|
||||
"global_context": {
|
||||
"some_global_key": "some_global_val"
|
||||
},
|
||||
"grantees": [],
|
||||
"hv_relaxed": false,
|
||||
"hv_tsc": false,
|
||||
"jobs": [],
|
||||
"mem": 4294967296,
|
||||
"meta": {
|
||||
"base64_fields": "cloudinit-user-data",
|
||||
"cloudinit-user-data": "I2Nsb3VkLWNvbmZpZwoKaG9zdG5hbWU6IGNvcmVvczE=",
|
||||
"ssh_public_key": "ssh-rsa AAAAB2NzaC1yc2E.../hQ5D5 john@doe"
|
||||
},
|
||||
"name": "coreos",
|
||||
"nics": [
|
||||
{
|
||||
"runtime": {
|
||||
"interface_type": "public",
|
||||
"ip_v4": {
|
||||
"uuid": "31.171.251.74"
|
||||
},
|
||||
"ip_v6": null
|
||||
},
|
||||
"vlan": null
|
||||
}
|
||||
],
|
||||
"smp": 2,
|
||||
"status": "running",
|
||||
"uuid": "20a0059b-041e-4d0c-bcc6-9b2852de48b3"
|
||||
}`)
|
||||
|
||||
if key == "" {
|
||||
return context, nil
|
||||
}
|
||||
|
||||
var marshalledContext map[string]interface{}
|
||||
|
||||
err := json.Unmarshal(context, &marshalledContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if key[0] == '/' {
|
||||
key = key[1:]
|
||||
}
|
||||
if key[len(key)-1] == '/' {
|
||||
key = key[:len(key)-1]
|
||||
}
|
||||
|
||||
return json.Marshal(marshalledContext[key])
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
cepgo := NewCepgoFetcher(fetchMock)
|
||||
|
||||
result, err := cepgo.All()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
for _, key := range []string{"meta", "name", "uuid", "global_context"} {
|
||||
if _, ok := result.(map[string]interface{})[key]; !ok {
|
||||
t.Errorf("%s not in all keys", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKey(t *testing.T) {
|
||||
cepgo := NewCepgoFetcher(fetchMock)
|
||||
|
||||
result, err := cepgo.Key("uuid")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if _, ok := result.(string); !ok {
|
||||
t.Errorf("%#v\n", result)
|
||||
|
||||
t.Error("Fetching the uuid did not return a string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeta(t *testing.T) {
|
||||
cepgo := NewCepgoFetcher(fetchMock)
|
||||
|
||||
meta, err := cepgo.Meta()
|
||||
if err != nil {
|
||||
t.Errorf("%#v\n", meta)
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if _, ok := meta["ssh_public_key"]; !ok {
|
||||
t.Error("ssh_public_key is not in the meta")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalContext(t *testing.T) {
|
||||
cepgo := NewCepgoFetcher(fetchMock)
|
||||
|
||||
result, err := cepgo.GlobalContext()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if _, ok := result["some_global_key"]; !ok {
|
||||
t.Error("some_global_key is not in the global context")
|
||||
}
|
||||
}
|
27
third_party/github.com/tarm/goserial/LICENSE
vendored
Normal file
27
third_party/github.com/tarm/goserial/LICENSE
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
63
third_party/github.com/tarm/goserial/README.md
vendored
Normal file
63
third_party/github.com/tarm/goserial/README.md
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
GoSerial
|
||||
========
|
||||
A simple go package to allow you to read and write from the
|
||||
serial port as a stream of bytes.
|
||||
|
||||
Details
|
||||
-------
|
||||
It aims to have the same API on all platforms, including windows. As
|
||||
an added bonus, the windows package does not use cgo, so you can cross
|
||||
compile for windows from another platform. Unfortunately goinstall
|
||||
does not currently let you cross compile so you will have to do it
|
||||
manually:
|
||||
|
||||
GOOS=windows make clean install
|
||||
|
||||
Currently there is very little in the way of configurability. You can
|
||||
set the baud rate. Then you can Read(), Write(), or Close() the
|
||||
connection. Read() will block until at least one byte is returned.
|
||||
Write is the same. There is currently no exposed way to set the
|
||||
timeouts, though patches are welcome.
|
||||
|
||||
Currently all ports are opened with 8 data bits, 1 stop bit, no
|
||||
parity, no hardware flow control, and no software flow control. This
|
||||
works fine for many real devices and many faux serial devices
|
||||
including usb-to-serial converters and bluetooth serial ports.
|
||||
|
||||
You may Read() and Write() simulantiously on the same connection (from
|
||||
different goroutines).
|
||||
|
||||
Usage
|
||||
-----
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/tarm/goserial"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := &serial.Config{Name: "COM45", Baud: 115200}
|
||||
s, err := serial.OpenPort(c)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
n, err := s.Write([]byte("test"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 128)
|
||||
n, err = s.Read(buf)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Print("%q", buf[:n])
|
||||
}
|
||||
```
|
||||
|
||||
Possible Future Work
|
||||
--------------------
|
||||
- better tests (loopback etc)
|
39
third_party/github.com/tarm/goserial/basic_test.go
vendored
Normal file
39
third_party/github.com/tarm/goserial/basic_test.go
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
package serial
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConnection(t *testing.T) {
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
c0 := &Config{Name: "COM5", Baud: 115200}
|
||||
|
||||
/*
|
||||
c1 := new(Config)
|
||||
c1.Name = "COM5"
|
||||
c1.Baud = 115200
|
||||
*/
|
||||
|
||||
s, err := OpenPort(c0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = s.Write([]byte("test"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 128)
|
||||
_, err = s.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// BUG(tarmigan): Add loopback test
|
||||
func TestLoopback(t *testing.T) {
|
||||
|
||||
}
|
99
third_party/github.com/tarm/goserial/serial.go
vendored
Normal file
99
third_party/github.com/tarm/goserial/serial.go
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Goserial is a simple go package to allow you to read and write from
|
||||
the serial port as a stream of bytes.
|
||||
|
||||
It aims to have the same API on all platforms, including windows. As
|
||||
an added bonus, the windows package does not use cgo, so you can cross
|
||||
compile for windows from another platform. Unfortunately goinstall
|
||||
does not currently let you cross compile so you will have to do it
|
||||
manually:
|
||||
|
||||
GOOS=windows make clean install
|
||||
|
||||
Currently there is very little in the way of configurability. You can
|
||||
set the baud rate. Then you can Read(), Write(), or Close() the
|
||||
connection. Read() will block until at least one byte is returned.
|
||||
Write is the same. There is currently no exposed way to set the
|
||||
timeouts, though patches are welcome.
|
||||
|
||||
Currently all ports are opened with 8 data bits, 1 stop bit, no
|
||||
parity, no hardware flow control, and no software flow control. This
|
||||
works fine for many real devices and many faux serial devices
|
||||
including usb-to-serial converters and bluetooth serial ports.
|
||||
|
||||
You may Read() and Write() simulantiously on the same connection (from
|
||||
different goroutines).
|
||||
|
||||
Example usage:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/tarm/goserial"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := &serial.Config{Name: "COM5", Baud: 115200}
|
||||
s, err := serial.OpenPort(c)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
n, err := s.Write([]byte("test"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 128)
|
||||
n, err = s.Read(buf)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Print("%q", buf[:n])
|
||||
}
|
||||
*/
|
||||
package serial
|
||||
|
||||
import "io"
|
||||
|
||||
// Config contains the information needed to open a serial port.
|
||||
//
|
||||
// Currently few options are implemented, but more may be added in the
|
||||
// future (patches welcome), so it is recommended that you create a
|
||||
// new config addressing the fields by name rather than by order.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// c0 := &serial.Config{Name: "COM45", Baud: 115200}
|
||||
// or
|
||||
// c1 := new(serial.Config)
|
||||
// c1.Name = "/dev/tty.usbserial"
|
||||
// c1.Baud = 115200
|
||||
//
|
||||
type Config struct {
|
||||
Name string
|
||||
Baud int
|
||||
|
||||
// Size int // 0 get translated to 8
|
||||
// Parity SomeNewTypeToGetCorrectDefaultOf_None
|
||||
// StopBits SomeNewTypeToGetCorrectDefaultOf_1
|
||||
|
||||
// RTSFlowControl bool
|
||||
// DTRFlowControl bool
|
||||
// XONFlowControl bool
|
||||
|
||||
// CRLFTranslate bool
|
||||
// TimeoutStuff int
|
||||
}
|
||||
|
||||
// OpenPort opens a serial port with the specified configuration
|
||||
func OpenPort(c *Config) (io.ReadWriteCloser, error) {
|
||||
return openPort(c.Name, c.Baud)
|
||||
}
|
||||
|
||||
// func Flush()
|
||||
|
||||
// func SendBreak()
|
||||
|
||||
// func RegisterBreakHandler(func())
|
90
third_party/github.com/tarm/goserial/serial_linux.go
vendored
Normal file
90
third_party/github.com/tarm/goserial/serial_linux.go
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
// +build linux,!cgo
|
||||
|
||||
package serial
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func openPort(name string, baud int) (rwc io.ReadWriteCloser, err error) {
|
||||
|
||||
var bauds = map[int]uint32{
|
||||
50: syscall.B50,
|
||||
75: syscall.B75,
|
||||
110: syscall.B110,
|
||||
134: syscall.B134,
|
||||
150: syscall.B150,
|
||||
200: syscall.B200,
|
||||
300: syscall.B300,
|
||||
600: syscall.B600,
|
||||
1200: syscall.B1200,
|
||||
1800: syscall.B1800,
|
||||
2400: syscall.B2400,
|
||||
4800: syscall.B4800,
|
||||
9600: syscall.B9600,
|
||||
19200: syscall.B19200,
|
||||
38400: syscall.B38400,
|
||||
57600: syscall.B57600,
|
||||
115200: syscall.B115200,
|
||||
230400: syscall.B230400,
|
||||
460800: syscall.B460800,
|
||||
500000: syscall.B500000,
|
||||
576000: syscall.B576000,
|
||||
921600: syscall.B921600,
|
||||
1000000: syscall.B1000000,
|
||||
1152000: syscall.B1152000,
|
||||
1500000: syscall.B1500000,
|
||||
2000000: syscall.B2000000,
|
||||
2500000: syscall.B2500000,
|
||||
3000000: syscall.B3000000,
|
||||
3500000: syscall.B3500000,
|
||||
4000000: syscall.B4000000,
|
||||
}
|
||||
|
||||
rate := bauds[baud]
|
||||
|
||||
if rate == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(name, syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil && f != nil {
|
||||
f.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
fd := f.Fd()
|
||||
t := syscall.Termios{
|
||||
Iflag: syscall.IGNPAR,
|
||||
Cflag: syscall.CS8 | syscall.CREAD | syscall.CLOCAL | rate,
|
||||
Cc: [32]uint8{syscall.VMIN: 1},
|
||||
Ispeed: rate,
|
||||
Ospeed: rate,
|
||||
}
|
||||
|
||||
if _, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_IOCTL,
|
||||
uintptr(fd),
|
||||
uintptr(syscall.TCSETS),
|
||||
uintptr(unsafe.Pointer(&t)),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
); errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
if err = syscall.SetNonblock(int(fd), false); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
107
third_party/github.com/tarm/goserial/serial_posix.go
vendored
Normal file
107
third_party/github.com/tarm/goserial/serial_posix.go
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
// +build !windows,cgo
|
||||
|
||||
package serial
|
||||
|
||||
// #include <termios.h>
|
||||
// #include <unistd.h>
|
||||
import "C"
|
||||
|
||||
// TODO: Maybe change to using syscall package + ioctl instead of cgo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
//"unsafe"
|
||||
)
|
||||
|
||||
func openPort(name string, baud int) (rwc io.ReadWriteCloser, err error) {
|
||||
f, err := os.OpenFile(name, syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fd := C.int(f.Fd())
|
||||
if C.isatty(fd) != 1 {
|
||||
f.Close()
|
||||
return nil, errors.New("File is not a tty")
|
||||
}
|
||||
|
||||
var st C.struct_termios
|
||||
_, err = C.tcgetattr(fd, &st)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
var speed C.speed_t
|
||||
switch baud {
|
||||
case 115200:
|
||||
speed = C.B115200
|
||||
case 57600:
|
||||
speed = C.B57600
|
||||
case 38400:
|
||||
speed = C.B38400
|
||||
case 19200:
|
||||
speed = C.B19200
|
||||
case 9600:
|
||||
speed = C.B9600
|
||||
case 4800:
|
||||
speed = C.B4800
|
||||
case 2400:
|
||||
speed = C.B2400
|
||||
default:
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("Unknown baud rate %v", baud)
|
||||
}
|
||||
|
||||
_, err = C.cfsetispeed(&st, speed)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
_, err = C.cfsetospeed(&st, speed)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Select local mode
|
||||
st.c_cflag |= (C.CLOCAL | C.CREAD)
|
||||
|
||||
// Select raw mode
|
||||
st.c_lflag &= ^C.tcflag_t(C.ICANON | C.ECHO | C.ECHOE | C.ISIG)
|
||||
st.c_oflag &= ^C.tcflag_t(C.OPOST)
|
||||
|
||||
_, err = C.tcsetattr(fd, C.TCSANOW, &st)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//fmt.Println("Tweaking", name)
|
||||
r1, _, e := syscall.Syscall(syscall.SYS_FCNTL,
|
||||
uintptr(f.Fd()),
|
||||
uintptr(syscall.F_SETFL),
|
||||
uintptr(0))
|
||||
if e != 0 || r1 != 0 {
|
||||
s := fmt.Sprint("Clearing NONBLOCK syscall error:", e, r1)
|
||||
f.Close()
|
||||
return nil, errors.New(s)
|
||||
}
|
||||
|
||||
/*
|
||||
r1, _, e = syscall.Syscall(syscall.SYS_IOCTL,
|
||||
uintptr(f.Fd()),
|
||||
uintptr(0x80045402), // IOSSIOSPEED
|
||||
uintptr(unsafe.Pointer(&baud)));
|
||||
if e != 0 || r1 != 0 {
|
||||
s := fmt.Sprint("Baudrate syscall error:", e, r1)
|
||||
f.Close()
|
||||
return nil, os.NewError(s)
|
||||
}
|
||||
*/
|
||||
|
||||
return f, nil
|
||||
}
|
263
third_party/github.com/tarm/goserial/serial_windows.go
vendored
Normal file
263
third_party/github.com/tarm/goserial/serial_windows.go
vendored
Normal file
@@ -0,0 +1,263 @@
|
||||
// +build windows
|
||||
|
||||
package serial
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type serialPort struct {
|
||||
f *os.File
|
||||
fd syscall.Handle
|
||||
rl sync.Mutex
|
||||
wl sync.Mutex
|
||||
ro *syscall.Overlapped
|
||||
wo *syscall.Overlapped
|
||||
}
|
||||
|
||||
type structDCB struct {
|
||||
DCBlength, BaudRate uint32
|
||||
flags [4]byte
|
||||
wReserved, XonLim, XoffLim uint16
|
||||
ByteSize, Parity, StopBits byte
|
||||
XonChar, XoffChar, ErrorChar, EofChar, EvtChar byte
|
||||
wReserved1 uint16
|
||||
}
|
||||
|
||||
type structTimeouts struct {
|
||||
ReadIntervalTimeout uint32
|
||||
ReadTotalTimeoutMultiplier uint32
|
||||
ReadTotalTimeoutConstant uint32
|
||||
WriteTotalTimeoutMultiplier uint32
|
||||
WriteTotalTimeoutConstant uint32
|
||||
}
|
||||
|
||||
func openPort(name string, baud int) (rwc io.ReadWriteCloser, err error) {
|
||||
if len(name) > 0 && name[0] != '\\' {
|
||||
name = "\\\\.\\" + name
|
||||
}
|
||||
|
||||
h, err := syscall.CreateFile(syscall.StringToUTF16Ptr(name),
|
||||
syscall.GENERIC_READ|syscall.GENERIC_WRITE,
|
||||
0,
|
||||
nil,
|
||||
syscall.OPEN_EXISTING,
|
||||
syscall.FILE_ATTRIBUTE_NORMAL|syscall.FILE_FLAG_OVERLAPPED,
|
||||
0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := os.NewFile(uintptr(h), name)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
f.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err = setCommState(h, baud); err != nil {
|
||||
return
|
||||
}
|
||||
if err = setupComm(h, 64, 64); err != nil {
|
||||
return
|
||||
}
|
||||
if err = setCommTimeouts(h); err != nil {
|
||||
return
|
||||
}
|
||||
if err = setCommMask(h); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ro, err := newOverlapped()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
wo, err := newOverlapped()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
port := new(serialPort)
|
||||
port.f = f
|
||||
port.fd = h
|
||||
port.ro = ro
|
||||
port.wo = wo
|
||||
|
||||
return port, nil
|
||||
}
|
||||
|
||||
func (p *serialPort) Close() error {
|
||||
return p.f.Close()
|
||||
}
|
||||
|
||||
func (p *serialPort) Write(buf []byte) (int, error) {
|
||||
p.wl.Lock()
|
||||
defer p.wl.Unlock()
|
||||
|
||||
if err := resetEvent(p.wo.HEvent); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var n uint32
|
||||
err := syscall.WriteFile(p.fd, buf, &n, p.wo)
|
||||
if err != nil && err != syscall.ERROR_IO_PENDING {
|
||||
return int(n), err
|
||||
}
|
||||
return getOverlappedResult(p.fd, p.wo)
|
||||
}
|
||||
|
||||
func (p *serialPort) Read(buf []byte) (int, error) {
|
||||
if p == nil || p.f == nil {
|
||||
return 0, fmt.Errorf("Invalid port on read %v %v", p, p.f)
|
||||
}
|
||||
|
||||
p.rl.Lock()
|
||||
defer p.rl.Unlock()
|
||||
|
||||
if err := resetEvent(p.ro.HEvent); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var done uint32
|
||||
err := syscall.ReadFile(p.fd, buf, &done, p.ro)
|
||||
if err != nil && err != syscall.ERROR_IO_PENDING {
|
||||
return int(done), err
|
||||
}
|
||||
return getOverlappedResult(p.fd, p.ro)
|
||||
}
|
||||
|
||||
var (
|
||||
nSetCommState,
|
||||
nSetCommTimeouts,
|
||||
nSetCommMask,
|
||||
nSetupComm,
|
||||
nGetOverlappedResult,
|
||||
nCreateEvent,
|
||||
nResetEvent uintptr
|
||||
)
|
||||
|
||||
func init() {
|
||||
k32, err := syscall.LoadLibrary("kernel32.dll")
|
||||
if err != nil {
|
||||
panic("LoadLibrary " + err.Error())
|
||||
}
|
||||
defer syscall.FreeLibrary(k32)
|
||||
|
||||
nSetCommState = getProcAddr(k32, "SetCommState")
|
||||
nSetCommTimeouts = getProcAddr(k32, "SetCommTimeouts")
|
||||
nSetCommMask = getProcAddr(k32, "SetCommMask")
|
||||
nSetupComm = getProcAddr(k32, "SetupComm")
|
||||
nGetOverlappedResult = getProcAddr(k32, "GetOverlappedResult")
|
||||
nCreateEvent = getProcAddr(k32, "CreateEventW")
|
||||
nResetEvent = getProcAddr(k32, "ResetEvent")
|
||||
}
|
||||
|
||||
func getProcAddr(lib syscall.Handle, name string) uintptr {
|
||||
addr, err := syscall.GetProcAddress(lib, name)
|
||||
if err != nil {
|
||||
panic(name + " " + err.Error())
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func setCommState(h syscall.Handle, baud int) error {
|
||||
var params structDCB
|
||||
params.DCBlength = uint32(unsafe.Sizeof(params))
|
||||
|
||||
params.flags[0] = 0x01 // fBinary
|
||||
params.flags[0] |= 0x10 // Assert DSR
|
||||
|
||||
params.BaudRate = uint32(baud)
|
||||
params.ByteSize = 8
|
||||
|
||||
r, _, err := syscall.Syscall(nSetCommState, 2, uintptr(h), uintptr(unsafe.Pointer(¶ms)), 0)
|
||||
if r == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCommTimeouts(h syscall.Handle) error {
|
||||
var timeouts structTimeouts
|
||||
const MAXDWORD = 1<<32 - 1
|
||||
timeouts.ReadIntervalTimeout = MAXDWORD
|
||||
timeouts.ReadTotalTimeoutMultiplier = MAXDWORD
|
||||
timeouts.ReadTotalTimeoutConstant = MAXDWORD - 1
|
||||
|
||||
/* From http://msdn.microsoft.com/en-us/library/aa363190(v=VS.85).aspx
|
||||
|
||||
For blocking I/O see below:
|
||||
|
||||
Remarks:
|
||||
|
||||
If an application sets ReadIntervalTimeout and
|
||||
ReadTotalTimeoutMultiplier to MAXDWORD and sets
|
||||
ReadTotalTimeoutConstant to a value greater than zero and
|
||||
less than MAXDWORD, one of the following occurs when the
|
||||
ReadFile function is called:
|
||||
|
||||
If there are any bytes in the input buffer, ReadFile returns
|
||||
immediately with the bytes in the buffer.
|
||||
|
||||
If there are no bytes in the input buffer, ReadFile waits
|
||||
until a byte arrives and then returns immediately.
|
||||
|
||||
If no bytes arrive within the time specified by
|
||||
ReadTotalTimeoutConstant, ReadFile times out.
|
||||
*/
|
||||
|
||||
r, _, err := syscall.Syscall(nSetCommTimeouts, 2, uintptr(h), uintptr(unsafe.Pointer(&timeouts)), 0)
|
||||
if r == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupComm(h syscall.Handle, in, out int) error {
|
||||
r, _, err := syscall.Syscall(nSetupComm, 3, uintptr(h), uintptr(in), uintptr(out))
|
||||
if r == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCommMask(h syscall.Handle) error {
|
||||
const EV_RXCHAR = 0x0001
|
||||
r, _, err := syscall.Syscall(nSetCommMask, 2, uintptr(h), EV_RXCHAR, 0)
|
||||
if r == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resetEvent(h syscall.Handle) error {
|
||||
r, _, err := syscall.Syscall(nResetEvent, 1, uintptr(h), 0, 0)
|
||||
if r == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newOverlapped() (*syscall.Overlapped, error) {
|
||||
var overlapped syscall.Overlapped
|
||||
r, _, err := syscall.Syscall6(nCreateEvent, 4, 0, 1, 0, 0, 0, 0)
|
||||
if r == 0 {
|
||||
return nil, err
|
||||
}
|
||||
overlapped.HEvent = syscall.Handle(r)
|
||||
return &overlapped, nil
|
||||
}
|
||||
|
||||
func getOverlappedResult(h syscall.Handle, overlapped *syscall.Overlapped) (int, error) {
|
||||
var n int
|
||||
r, _, err := syscall.Syscall6(nGetOverlappedResult, 4,
|
||||
uintptr(h),
|
||||
uintptr(unsafe.Pointer(overlapped)),
|
||||
uintptr(unsafe.Pointer(&n)), 1, 0, 0)
|
||||
if r == 0 {
|
||||
return n, err
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
@@ -1,3 +1,15 @@
|
||||
The following files were ported to Go from C files of libyaml, and thus
|
||||
are still covered by their original copyright and license:
|
||||
|
||||
apic.go
|
||||
emitterc.go
|
||||
parserc.go
|
||||
readerc.go
|
||||
scannerc.go
|
||||
writerc.go
|
||||
yamlh.go
|
||||
yamlprivateh.go
|
||||
|
||||
Copyright (c) 2006 Kirill Simonov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user