Compare commits

..

1 Commits

Author SHA1 Message Date
Brian Waldon
a398ce82f7 chore(release): Bump version to v0.6.1 2014-05-14 10:52:22 -07:00
203 changed files with 2154 additions and 11124 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
*.swp
bin/
coverage/
gopath/
pkg/

View File

@@ -1,11 +0,0 @@
language: go
go:
- 1.3
- 1.2
install:
- go get code.google.com/p/go.tools/cmd/cover
- go get code.google.com/p/go.tools/cmd/vet
script:
- ./test

View File

@@ -39,25 +39,22 @@ Thanks for your contributions!
### Format of the Commit Message
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.
We follow a rough convention for commit messages borrowed from AngularJS. This
is an example of a commit:
```
environment: write new keys in consistent order
feat(scripts/test-cluster): add a cluster test command
Go 1.3 randomizes the ordering of keys when iterating over a map.
Sort the keys to make this ordering consistent.
Fixes #38
this uses tmux to setup a test cluster that you can easily kill and
start for debugging.
```
The format can be described more formally as follows:
```
<subsystem>: <what changed>
<type>(<scope>): <subject>
<BLANK LINE>
<why this change was made>
<body>
<BLANK LINE>
<footer>
```
@@ -66,3 +63,25 @@ 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#).

View File

@@ -13,7 +13,7 @@ If no **id** field is provided, coreos-cloudinit will ignore this section.
For example, the following cloud-config document...
```yaml
```
#cloud-config
coreos:
oem:
@@ -26,7 +26,7 @@ coreos:
...would be rendered to the following `/etc/oem-release`:
```yaml
```
ID=rackspace
NAME="Rackspace Cloud Servers"
VERSION_ID=168.0.0

View File

@@ -1,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. Your cloud-config is processed during each boot.
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime.
## 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 `#cloud-config`, followed by an associative array which has zero or more of the following keys:
A cloud-config file should contain 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.
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...
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...
```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,55 +68,19 @@ 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:
fleet:
public-ip: $public_ipv4
metadata: region=us-west
```
...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"
```
For more information on fleet configuration, see the [fleet documentation][fleet-config].
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/deployment-and-configuration.md#configuration
#### update
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field.
The `reboot-strategy` parameter also affects the behaviour of [locksmith](https://github.com/coreos/locksmith).
- **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed.
- _reboot_: Reboot immediately after an update is applied.
- _etcd-lock_: Reboot after first taking a distributed lock in etcd, this guarantees that only one host will reboot concurrently and that the cluster will remain available during the update.
- _best-effort_ - If etcd is running, "etcd-lock", otherwise simply "reboot".
- _off_ - Disable rebooting after updates are applied (not recommended).
- **server**: is the omaha endpoint URL which will be queried for updates.
- **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable")
*Note: cloudinit will only manipulate the locksmith unit file in the systemd runtime directory (`/run/systemd/system/locksmithd.service`). If any manual modifications are made to an overriding unit configuration file (e.g. `/etc/systemd/system/locksmithd.service`), cloudinit will no longer be able to control the locksmith service unit.*
##### Example
```yaml
```
#cloud-config
coreos:
update:
@@ -125,16 +89,13 @@ coreos:
#### units
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:
The `coreos.units.*` parameters define a list of arbitrary systemd units to start. 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`. 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.
- **runtime**: Boolean indicating whether or not to persist the unit across reboots. This is analagous to the `--runtime` argument to `systemd 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.
- **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already.
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands.
- **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.
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. Default value is restart.
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
@@ -142,7 +103,7 @@ Each item is an object with the following fields:
Write a unit to disk, automatically starting it.
```yaml
```
#cloud-config
coreos:
@@ -159,12 +120,15 @@ coreos:
Restart=always
ExecStart=/usr/bin/docker start -a redis_server
ExecStop=/usr/bin/docker stop -t 2 redis_server
[Install]
WantedBy=local.target
```
Start the built-in `etcd` and `fleet` services:
Start the builtin `etcd` and `fleet` services:
```yaml
#cloud-config
```
# cloud-config
coreos:
units:
@@ -181,7 +145,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:
@@ -193,7 +157,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
@@ -207,7 +171,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
@@ -226,7 +190,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:
@@ -265,7 +229,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:
@@ -278,17 +242,17 @@ users:
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
```yaml
```
#cloud-config
users:
- name: elroy
coreos-ssh-import-url: https://github-enterprise.example.com/api/v3/users/elroy/keys?access_token=<TOKEN>
coreos-ssh-import-url: https://token:<OAUTH-TOKEN>@github-enterprise.example.com/users/elroy/keys
```
You can also specify any URL whose response matches the JSON format for public keys:
```yaml
```
#cloud-config
users:
@@ -298,8 +262,7 @@ users:
### write_files
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:
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:
- **path**: Absolute location on disk where contents should be written
- **content**: Data to write at the provided `path`
@@ -309,19 +272,14 @@ Each item in the list may have the following keys:
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/resolv.conf
- path: /etc/fleet/fleet.conf
permissions: 0644
owner: root
content: |
nameserver 8.8.8.8
- path: /etc/motd
permissions: 0644
owner: root
content: |
Good news, everyone!
verbosity=1
metadata="region=us-west,type=ssd"
```
### manage_etc_hosts
@@ -331,7 +289,7 @@ Currently, the only supported value is "localhost" which will cause your system'
to resolve to "127.0.0.1". This is helpful when the host does not have DNS
infrastructure in place to resolve its own hostname, for example, when using Vagrant.
```yaml
```
#cloud-config
manage_etc_hosts: localhost

View File

@@ -14,21 +14,17 @@ 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:
```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
```
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:
```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...]
```
qemu-system-x86_64 \
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
[usual qemu options here...]

View File

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

34
Godeps/Godeps.json generated
View File

@@ -1,34 +0,0 @@
{
"ImportPath": "github.com/coreos/coreos-cloudinit",
"GoVersion": "go1.3.1",
"Packages": [
"./..."
],
"Deps": [
{
"ImportPath": "github.com/cloudsigma/cepgo",
"Rev": "1bfc4895bf5c4d3b599f3f6ee142299488c8739b"
},
{
"ImportPath": "github.com/coreos/go-systemd/dbus",
"Rev": "4fbc5060a317b142e6c7bfbedb65596d5f0ab99b"
},
{
"ImportPath": "github.com/dotcloud/docker/pkg/netlink",
"Comment": "v0.11.1-359-g55d41c3e21e1",
"Rev": "55d41c3e21e1593b944c06196ffb2ac57ab7f653"
},
{
"ImportPath": "github.com/guelfey/go.dbus",
"Rev": "f6a3a2366cc39b8479cadc499d3c735fb10fbdda"
},
{
"ImportPath": "github.com/tarm/goserial",
"Rev": "cdabc8d44e8e84f58f18074ae44337e1f2f375b9"
},
{
"ImportPath": "gopkg.in/yaml.v1",
"Rev": "feb4ca79644e8e7e39c06095246ee54b1282c118"
}
]
}

5
Godeps/Readme generated
View File

@@ -1,5 +0,0 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.

2
Godeps/_workspace/.gitignore generated vendored
View File

@@ -1,2 +0,0 @@
/pkg
/bin

View File

@@ -1,23 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test

View File

@@ -1,202 +0,0 @@
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.

View File

@@ -1,43 +0,0 @@
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

View File

@@ -1,186 +0,0 @@
// 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/Godeps/_workspace/src/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
}

View File

@@ -1,122 +0,0 @@
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")
}
}

View File

@@ -1,2 +0,0 @@
Michael Crosby <michael@crosbymichael.com> (@crosbymichael)
Guillaume J. Charmes <guillaume@docker.com> (@creack)

View File

@@ -1,23 +0,0 @@
// Packet netlink provide access to low level Netlink sockets and messages.
//
// Actual implementations are in:
// netlink_linux.go
// netlink_darwin.go
package netlink
import (
"errors"
"net"
)
var (
ErrWrongSockType = errors.New("Wrong socket type")
ErrShortResponse = errors.New("Got short response from netlink")
)
// A Route is a subnet associated with the interface to reach it.
type Route struct {
*net.IPNet
Iface *net.Interface
Default bool
}

View File

@@ -1,891 +0,0 @@
// +build amd64
package netlink
import (
"encoding/binary"
"fmt"
"math/rand"
"net"
"syscall"
"unsafe"
)
const (
IFNAMSIZ = 16
DEFAULT_CHANGE = 0xFFFFFFFF
IFLA_INFO_KIND = 1
IFLA_INFO_DATA = 2
VETH_INFO_PEER = 1
IFLA_NET_NS_FD = 28
SIOC_BRADDBR = 0x89a0
SIOC_BRADDIF = 0x89a2
)
var nextSeqNr int
type ifreqHwaddr struct {
IfrnName [16]byte
IfruHwaddr syscall.RawSockaddr
}
type ifreqIndex struct {
IfrnName [16]byte
IfruIndex int32
}
func nativeEndian() binary.ByteOrder {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
return binary.BigEndian
}
return binary.LittleEndian
}
func getSeq() int {
nextSeqNr = nextSeqNr + 1
return nextSeqNr
}
func getIpFamily(ip net.IP) int {
if len(ip) <= net.IPv4len {
return syscall.AF_INET
}
if ip.To4() != nil {
return syscall.AF_INET
}
return syscall.AF_INET6
}
type NetlinkRequestData interface {
Len() int
ToWireFormat() []byte
}
type IfInfomsg struct {
syscall.IfInfomsg
}
func newIfInfomsg(family int) *IfInfomsg {
return &IfInfomsg{
IfInfomsg: syscall.IfInfomsg{
Family: uint8(family),
},
}
}
func newIfInfomsgChild(parent *RtAttr, family int) *IfInfomsg {
msg := newIfInfomsg(family)
parent.children = append(parent.children, msg)
return msg
}
func (msg *IfInfomsg) ToWireFormat() []byte {
native := nativeEndian()
length := syscall.SizeofIfInfomsg
b := make([]byte, length)
b[0] = msg.Family
b[1] = 0
native.PutUint16(b[2:4], msg.Type)
native.PutUint32(b[4:8], uint32(msg.Index))
native.PutUint32(b[8:12], msg.Flags)
native.PutUint32(b[12:16], msg.Change)
return b
}
func (msg *IfInfomsg) Len() int {
return syscall.SizeofIfInfomsg
}
type IfAddrmsg struct {
syscall.IfAddrmsg
}
func newIfAddrmsg(family int) *IfAddrmsg {
return &IfAddrmsg{
IfAddrmsg: syscall.IfAddrmsg{
Family: uint8(family),
},
}
}
func (msg *IfAddrmsg) ToWireFormat() []byte {
native := nativeEndian()
length := syscall.SizeofIfAddrmsg
b := make([]byte, length)
b[0] = msg.Family
b[1] = msg.Prefixlen
b[2] = msg.Flags
b[3] = msg.Scope
native.PutUint32(b[4:8], msg.Index)
return b
}
func (msg *IfAddrmsg) Len() int {
return syscall.SizeofIfAddrmsg
}
type RtMsg struct {
syscall.RtMsg
}
func newRtMsg(family int) *RtMsg {
return &RtMsg{
RtMsg: syscall.RtMsg{
Family: uint8(family),
Table: syscall.RT_TABLE_MAIN,
Scope: syscall.RT_SCOPE_UNIVERSE,
Protocol: syscall.RTPROT_BOOT,
Type: syscall.RTN_UNICAST,
},
}
}
func (msg *RtMsg) ToWireFormat() []byte {
native := nativeEndian()
length := syscall.SizeofRtMsg
b := make([]byte, length)
b[0] = msg.Family
b[1] = msg.Dst_len
b[2] = msg.Src_len
b[3] = msg.Tos
b[4] = msg.Table
b[5] = msg.Protocol
b[6] = msg.Scope
b[7] = msg.Type
native.PutUint32(b[8:12], msg.Flags)
return b
}
func (msg *RtMsg) Len() int {
return syscall.SizeofRtMsg
}
func rtaAlignOf(attrlen int) int {
return (attrlen + syscall.RTA_ALIGNTO - 1) & ^(syscall.RTA_ALIGNTO - 1)
}
type RtAttr struct {
syscall.RtAttr
Data []byte
children []NetlinkRequestData
}
func newRtAttr(attrType int, data []byte) *RtAttr {
return &RtAttr{
RtAttr: syscall.RtAttr{
Type: uint16(attrType),
},
children: []NetlinkRequestData{},
Data: data,
}
}
func newRtAttrChild(parent *RtAttr, attrType int, data []byte) *RtAttr {
attr := newRtAttr(attrType, data)
parent.children = append(parent.children, attr)
return attr
}
func (a *RtAttr) Len() int {
l := 0
for _, child := range a.children {
l += child.Len() + syscall.SizeofRtAttr
}
if l == 0 {
l++
}
return rtaAlignOf(l + len(a.Data))
}
func (a *RtAttr) ToWireFormat() []byte {
native := nativeEndian()
length := a.Len()
buf := make([]byte, rtaAlignOf(length+syscall.SizeofRtAttr))
if a.Data != nil {
copy(buf[4:], a.Data)
} else {
next := 4
for _, child := range a.children {
childBuf := child.ToWireFormat()
copy(buf[next:], childBuf)
next += rtaAlignOf(len(childBuf))
}
}
if l := uint16(rtaAlignOf(length)); l != 0 {
native.PutUint16(buf[0:2], l+1)
}
native.PutUint16(buf[2:4], a.Type)
return buf
}
type NetlinkRequest struct {
syscall.NlMsghdr
Data []NetlinkRequestData
}
func (rr *NetlinkRequest) ToWireFormat() []byte {
native := nativeEndian()
length := rr.Len
dataBytes := make([][]byte, len(rr.Data))
for i, data := range rr.Data {
dataBytes[i] = data.ToWireFormat()
length += uint32(len(dataBytes[i]))
}
b := make([]byte, length)
native.PutUint32(b[0:4], length)
native.PutUint16(b[4:6], rr.Type)
native.PutUint16(b[6:8], rr.Flags)
native.PutUint32(b[8:12], rr.Seq)
native.PutUint32(b[12:16], rr.Pid)
next := 16
for _, data := range dataBytes {
copy(b[next:], data)
next += len(data)
}
return b
}
func (rr *NetlinkRequest) AddData(data NetlinkRequestData) {
if data != nil {
rr.Data = append(rr.Data, data)
}
}
func newNetlinkRequest(proto, flags int) *NetlinkRequest {
return &NetlinkRequest{
NlMsghdr: syscall.NlMsghdr{
Len: uint32(syscall.NLMSG_HDRLEN),
Type: uint16(proto),
Flags: syscall.NLM_F_REQUEST | uint16(flags),
Seq: uint32(getSeq()),
},
}
}
type NetlinkSocket struct {
fd int
lsa syscall.SockaddrNetlink
}
func getNetlinkSocket() (*NetlinkSocket, error) {
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_ROUTE)
if err != nil {
return nil, err
}
s := &NetlinkSocket{
fd: fd,
}
s.lsa.Family = syscall.AF_NETLINK
if err := syscall.Bind(fd, &s.lsa); err != nil {
syscall.Close(fd)
return nil, err
}
return s, nil
}
func (s *NetlinkSocket) Close() {
syscall.Close(s.fd)
}
func (s *NetlinkSocket) Send(request *NetlinkRequest) error {
if err := syscall.Sendto(s.fd, request.ToWireFormat(), 0, &s.lsa); err != nil {
return err
}
return nil
}
func (s *NetlinkSocket) Receive() ([]syscall.NetlinkMessage, error) {
rb := make([]byte, syscall.Getpagesize())
nr, _, err := syscall.Recvfrom(s.fd, rb, 0)
if err != nil {
return nil, err
}
if nr < syscall.NLMSG_HDRLEN {
return nil, ErrShortResponse
}
rb = rb[:nr]
return syscall.ParseNetlinkMessage(rb)
}
func (s *NetlinkSocket) GetPid() (uint32, error) {
lsa, err := syscall.Getsockname(s.fd)
if err != nil {
return 0, err
}
switch v := lsa.(type) {
case *syscall.SockaddrNetlink:
return v.Pid, nil
}
return 0, ErrWrongSockType
}
func (s *NetlinkSocket) HandleAck(seq uint32) error {
native := nativeEndian()
pid, err := s.GetPid()
if err != nil {
return err
}
done:
for {
msgs, err := s.Receive()
if err != nil {
return err
}
for _, m := range msgs {
if m.Header.Seq != seq {
return fmt.Errorf("Wrong Seq nr %d, expected %d", m.Header.Seq, seq)
}
if m.Header.Pid != pid {
return fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid)
}
if m.Header.Type == syscall.NLMSG_DONE {
break done
}
if m.Header.Type == syscall.NLMSG_ERROR {
error := int32(native.Uint32(m.Data[0:4]))
if error == 0 {
break done
}
return syscall.Errno(-error)
}
}
}
return nil
}
// Add a new default gateway. Identical to:
// ip route add default via $ip
func AddDefaultGw(ip net.IP) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
family := getIpFamily(ip)
wb := newNetlinkRequest(syscall.RTM_NEWROUTE, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
msg := newRtMsg(family)
wb.AddData(msg)
var ipData []byte
if family == syscall.AF_INET {
ipData = ip.To4()
} else {
ipData = ip.To16()
}
gateway := newRtAttr(syscall.RTA_GATEWAY, ipData)
wb.AddData(gateway)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// Bring up a particular network interface
func NetworkLinkUp(iface *net.Interface) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Change = syscall.IFF_UP
msg.Flags = syscall.IFF_UP
msg.Index = int32(iface.Index)
wb.AddData(msg)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func NetworkLinkDown(iface *net.Interface) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Change = syscall.IFF_UP
msg.Flags = 0 & ^syscall.IFF_UP
msg.Index = int32(iface.Index)
wb.AddData(msg)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func NetworkSetMTU(iface *net.Interface, mtu int) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Type = syscall.RTM_SETLINK
msg.Flags = syscall.NLM_F_REQUEST
msg.Index = int32(iface.Index)
msg.Change = DEFAULT_CHANGE
wb.AddData(msg)
var (
b = make([]byte, 4)
native = nativeEndian()
)
native.PutUint32(b, uint32(mtu))
data := newRtAttr(syscall.IFLA_MTU, b)
wb.AddData(data)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// same as ip link set $name master $master
func NetworkSetMaster(iface, master *net.Interface) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Type = syscall.RTM_SETLINK
msg.Flags = syscall.NLM_F_REQUEST
msg.Index = int32(iface.Index)
msg.Change = DEFAULT_CHANGE
wb.AddData(msg)
var (
b = make([]byte, 4)
native = nativeEndian()
)
native.PutUint32(b, uint32(master.Index))
data := newRtAttr(syscall.IFLA_MASTER, b)
wb.AddData(data)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func NetworkSetNsPid(iface *net.Interface, nspid int) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Type = syscall.RTM_SETLINK
msg.Flags = syscall.NLM_F_REQUEST
msg.Index = int32(iface.Index)
msg.Change = DEFAULT_CHANGE
wb.AddData(msg)
var (
b = make([]byte, 4)
native = nativeEndian()
)
native.PutUint32(b, uint32(nspid))
data := newRtAttr(syscall.IFLA_NET_NS_PID, b)
wb.AddData(data)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func NetworkSetNsFd(iface *net.Interface, fd int) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Type = syscall.RTM_SETLINK
msg.Flags = syscall.NLM_F_REQUEST
msg.Index = int32(iface.Index)
msg.Change = DEFAULT_CHANGE
wb.AddData(msg)
var (
b = make([]byte, 4)
native = nativeEndian()
)
native.PutUint32(b, uint32(fd))
data := newRtAttr(IFLA_NET_NS_FD, b)
wb.AddData(data)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// Add an Ip address to an interface. This is identical to:
// ip addr add $ip/$ipNet dev $iface
func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
family := getIpFamily(ip)
wb := newNetlinkRequest(syscall.RTM_NEWADDR, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
msg := newIfAddrmsg(family)
msg.Index = uint32(iface.Index)
prefixLen, _ := ipNet.Mask.Size()
msg.Prefixlen = uint8(prefixLen)
wb.AddData(msg)
var ipData []byte
if family == syscall.AF_INET {
ipData = ip.To4()
} else {
ipData = ip.To16()
}
localData := newRtAttr(syscall.IFA_LOCAL, ipData)
wb.AddData(localData)
addrData := newRtAttr(syscall.IFA_ADDRESS, ipData)
wb.AddData(addrData)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func zeroTerminated(s string) []byte {
return []byte(s + "\000")
}
func nonZeroTerminated(s string) []byte {
return []byte(s)
}
// Add a new network link of a specified type. This is identical to
// running: ip add link $name type $linkType
func NetworkLinkAdd(name string, linkType string) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
wb.AddData(msg)
if name != "" {
nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name))
wb.AddData(nameData)
}
kindData := newRtAttr(IFLA_INFO_KIND, nonZeroTerminated(linkType))
infoData := newRtAttr(syscall.IFLA_LINKINFO, kindData.ToWireFormat())
wb.AddData(infoData)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// Returns an array of IPNet for all the currently routed subnets on ipv4
// This is similar to the first column of "ip route" output
func NetworkGetRoutes() ([]Route, error) {
native := nativeEndian()
s, err := getNetlinkSocket()
if err != nil {
return nil, err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_GETROUTE, syscall.NLM_F_DUMP)
msg := newIfInfomsg(syscall.AF_UNSPEC)
wb.AddData(msg)
if err := s.Send(wb); err != nil {
return nil, err
}
pid, err := s.GetPid()
if err != nil {
return nil, err
}
res := make([]Route, 0)
done:
for {
msgs, err := s.Receive()
if err != nil {
return nil, err
}
for _, m := range msgs {
if m.Header.Seq != wb.Seq {
return nil, fmt.Errorf("Wrong Seq nr %d, expected 1", m.Header.Seq)
}
if m.Header.Pid != pid {
return nil, fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid)
}
if m.Header.Type == syscall.NLMSG_DONE {
break done
}
if m.Header.Type == syscall.NLMSG_ERROR {
error := int32(native.Uint32(m.Data[0:4]))
if error == 0 {
break done
}
return nil, syscall.Errno(-error)
}
if m.Header.Type != syscall.RTM_NEWROUTE {
continue
}
var r Route
msg := (*RtMsg)(unsafe.Pointer(&m.Data[0:syscall.SizeofRtMsg][0]))
if msg.Flags&syscall.RTM_F_CLONED != 0 {
// Ignore cloned routes
continue
}
if msg.Table != syscall.RT_TABLE_MAIN {
// Ignore non-main tables
continue
}
if msg.Family != syscall.AF_INET {
// Ignore non-ipv4 routes
continue
}
if msg.Dst_len == 0 {
// Default routes
r.Default = true
}
attrs, err := syscall.ParseNetlinkRouteAttr(&m)
if err != nil {
return nil, err
}
for _, attr := range attrs {
switch attr.Attr.Type {
case syscall.RTA_DST:
ip := attr.Value
r.IPNet = &net.IPNet{
IP: ip,
Mask: net.CIDRMask(int(msg.Dst_len), 8*len(ip)),
}
case syscall.RTA_OIF:
index := int(native.Uint32(attr.Value[0:4]))
r.Iface, _ = net.InterfaceByIndex(index)
}
}
if r.Default || r.IPNet != nil {
res = append(res, r)
}
}
}
return res, nil
}
func getIfSocket() (fd int, err error) {
for _, socket := range []int{
syscall.AF_INET,
syscall.AF_PACKET,
syscall.AF_INET6,
} {
if fd, err = syscall.Socket(socket, syscall.SOCK_DGRAM, 0); err == nil {
break
}
}
if err == nil {
return fd, nil
}
return -1, err
}
func NetworkChangeName(iface *net.Interface, newName string) error {
fd, err := getIfSocket()
if err != nil {
return err
}
defer syscall.Close(fd)
data := [IFNAMSIZ * 2]byte{}
// the "-1"s here are very important for ensuring we get proper null
// termination of our new C strings
copy(data[:IFNAMSIZ-1], iface.Name)
copy(data[IFNAMSIZ:IFNAMSIZ*2-1], newName)
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.SIOCSIFNAME, uintptr(unsafe.Pointer(&data[0]))); errno != 0 {
return errno
}
return nil
}
func NetworkCreateVethPair(name1, name2 string) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
wb.AddData(msg)
nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name1))
wb.AddData(nameData)
nest1 := newRtAttr(syscall.IFLA_LINKINFO, nil)
newRtAttrChild(nest1, IFLA_INFO_KIND, zeroTerminated("veth"))
nest2 := newRtAttrChild(nest1, IFLA_INFO_DATA, nil)
nest3 := newRtAttrChild(nest2, VETH_INFO_PEER, nil)
newIfInfomsgChild(nest3, syscall.AF_UNSPEC)
newRtAttrChild(nest3, syscall.IFLA_IFNAME, zeroTerminated(name2))
wb.AddData(nest1)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// Create the actual bridge device. This is more backward-compatible than
// netlink.NetworkLinkAdd and works on RHEL 6.
func CreateBridge(name string, setMacAddr bool) error {
s, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
if err != nil {
// ipv6 issue, creating with ipv4
s, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
if err != nil {
return err
}
}
defer syscall.Close(s)
nameBytePtr, err := syscall.BytePtrFromString(name)
if err != nil {
return err
}
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), SIOC_BRADDBR, uintptr(unsafe.Pointer(nameBytePtr))); err != 0 {
return err
}
if setMacAddr {
return setBridgeMacAddress(s, name)
}
return nil
}
// Add a slave to abridge device. This is more backward-compatible than
// netlink.NetworkSetMaster and works on RHEL 6.
func AddToBridge(iface, master *net.Interface) error {
s, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
if err != nil {
// ipv6 issue, creating with ipv4
s, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
if err != nil {
return err
}
}
defer syscall.Close(s)
ifr := ifreqIndex{}
copy(ifr.IfrnName[:], master.Name)
ifr.IfruIndex = int32(iface.Index)
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), SIOC_BRADDIF, uintptr(unsafe.Pointer(&ifr))); err != 0 {
return err
}
return nil
}
func setBridgeMacAddress(s int, name string) error {
ifr := ifreqHwaddr{}
ifr.IfruHwaddr.Family = syscall.ARPHRD_ETHER
copy(ifr.IfrnName[:], name)
for i := 0; i < 6; i++ {
ifr.IfruHwaddr.Data[i] = int8(rand.Intn(255))
}
ifr.IfruHwaddr.Data[0] &^= 0x1 // clear multicast bit
ifr.IfruHwaddr.Data[0] |= 0x2 // set local assignment bit (IEEE802)
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), syscall.SIOCSIFHWADDR, uintptr(unsafe.Pointer(&ifr))); err != 0 {
return err
}
return nil
}

View File

@@ -1,69 +0,0 @@
// +build !linux !amd64
package netlink
import (
"errors"
"net"
)
var (
ErrNotImplemented = errors.New("not implemented")
)
func NetworkGetRoutes() ([]Route, error) {
return nil, ErrNotImplemented
}
func NetworkLinkAdd(name string, linkType string) error {
return ErrNotImplemented
}
func NetworkLinkUp(iface *net.Interface) error {
return ErrNotImplemented
}
func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
return ErrNotImplemented
}
func AddDefaultGw(ip net.IP) error {
return ErrNotImplemented
}
func NetworkSetMTU(iface *net.Interface, mtu int) error {
return ErrNotImplemented
}
func NetworkCreateVethPair(name1, name2 string) error {
return ErrNotImplemented
}
func NetworkChangeName(iface *net.Interface, newName string) error {
return ErrNotImplemented
}
func NetworkSetNsFd(iface *net.Interface, fd int) error {
return ErrNotImplemented
}
func NetworkSetNsPid(iface *net.Interface, nspid int) error {
return ErrNotImplemented
}
func NetworkSetMaster(iface, master *net.Interface) error {
return ErrNotImplemented
}
func NetworkLinkDown(iface *net.Interface) error {
return ErrNotImplemented
}
func CreateBridge(name string, setMacAddr bool) error {
return ErrNotImplemented
}
func AddToBridge(iface, master *net.Interface) error {
return ErrNotImplemented
}

View File

@@ -1,27 +0,0 @@
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.

View File

@@ -1,63 +0,0 @@
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)

View File

@@ -1,61 +0,0 @@
package serial
import (
"testing"
"time"
)
func TestConnection(t *testing.T) {
c0 := &Config{Name: "/dev/ttyUSB0", Baud: 115200}
c1 := &Config{Name: "/dev/ttyUSB1", Baud: 115200}
s1, err := OpenPort(c0)
if err != nil {
t.Fatal(err)
}
s2, err := OpenPort(c1)
if err != nil {
t.Fatal(err)
}
ch := make(chan int, 1)
go func() {
buf := make([]byte, 128)
var readCount int
for {
n, err := s2.Read(buf)
if err != nil {
t.Fatal(err)
}
readCount++
t.Logf("Read %v %v bytes: % 02x %s", readCount, n, buf[:n], buf[:n])
select {
case <-ch:
ch <- readCount
close(ch)
default:
}
}
}()
if _, err = s1.Write([]byte("hello")); err != nil {
t.Fatal(err)
}
if _, err = s1.Write([]byte(" ")); err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
if _, err = s1.Write([]byte("world")); err != nil {
t.Fatal(err)
}
time.Sleep(time.Second / 10)
ch <- 0
s1.Write([]byte(" ")) // We could be blocked in the read without this
c := <-ch
exp := 5
if c >= exp {
t.Fatalf("Expected less than %v read, got %v", exp, c)
}
}

View File

@@ -1,99 +0,0 @@
/*
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())

View File

@@ -1,90 +0,0 @@
// +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
}

View File

@@ -1,107 +0,0 @@
// +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
}

View File

@@ -1,263 +0,0 @@
// +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(&params)), 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
}

View File

@@ -1,128 +0,0 @@
# YAML support for the Go language
Introduction
------------
The yaml package enables Go programs to comfortably encode and decode YAML
values. It was developed within [Canonical](https://www.canonical.com) as
part of the [juju](https://juju.ubuntu.com) project, and is based on a
pure Go port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML)
C library to parse and generate YAML data quickly and reliably.
Compatibility
-------------
The yaml package is almost compatible with YAML 1.1, including support for
anchors, tags, etc. There are still a few missing bits, such as document
merging, base-60 floats (huh?), and multi-document unmarshalling. These
features are not hard to add, and will be introduced as necessary.
Installation and usage
----------------------
The import path for the package is *gopkg.in/yaml.v1*.
To install it, run:
go get gopkg.in/yaml.v1
API documentation
-----------------
If opened in a browser, the import path itself leads to the API documentation:
* [https://gopkg.in/yaml.v1](https://gopkg.in/yaml.v1)
API stability
-------------
The package API for yaml v1 will remain stable as described in [gopkg.in](https://gopkg.in).
License
-------
The yaml package is licensed under the LGPL with an exception that allows it to be linked statically. Please see the LICENSE file for details.
Example
-------
```Go
package main
import (
"fmt"
"log"
"gopkg.in/yaml.v1"
)
var data = `
a: Easy!
b:
c: 2
d: [3, 4]
`
type T struct {
A string
B struct{C int; D []int ",flow"}
}
func main() {
t := T{}
err := yaml.Unmarshal([]byte(data), &t)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("--- t:\n%v\n\n", t)
d, err := yaml.Marshal(&t)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("--- t dump:\n%s\n\n", string(d))
m := make(map[interface{}]interface{})
err = yaml.Unmarshal([]byte(data), &m)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("--- m:\n%v\n\n", m)
d, err = yaml.Marshal(&m)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("--- m dump:\n%s\n\n", string(d))
}
```
This example will generate the following output:
```
--- t:
{Easy! {2 [3 4]}}
--- t dump:
a: Easy!
b:
c: 2
d: [3, 4]
--- m:
map[a:Easy! b:map[c:2 d:[3 4]]]
--- m dump:
a: Easy!
b:
c: 2
d:
- 3
- 4
```

View File

@@ -1,3 +0,0 @@
Alex Crawford <alex.crawford@coreos.com> (@crawford)
Jonathan Boulle <jonathan.boulle@coreos.com> (@jonboulle)
Brian Waldon <brian.waldon@coreos.com> (@bcwaldon)

View File

@@ -1,4 +1,4 @@
# coreos-cloudinit [![Build Status](https://travis-ci.org/coreos/coreos-cloudinit.png?branch=master)](https://travis-ci.org/coreos/coreos-cloudinit)
# coreos-cloudinit
coreos-cloudinit enables a user to customize CoreOS machines by providing either a cloud-config document or an executable script through user-data.

7
build
View File

@@ -3,12 +3,7 @@
ORG_PATH="github.com/coreos"
REPO_PATH="${ORG_PATH}/coreos-cloudinit"
if [ ! -h gopath/src/${REPO_PATH} ]; then
mkdir -p gopath/src/${ORG_PATH}
ln -s ../../../.. gopath/src/${REPO_PATH} || exit 255
fi
export GOBIN=${PWD}/bin
export GOPATH=${PWD}/gopath
export GOPATH=${PWD}
go build -o bin/coreos-cloudinit ${REPO_PATH}

View File

@@ -1,377 +1,109 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"flag"
"fmt"
"log"
"os"
"sync"
"time"
"github.com/coreos/coreos-cloudinit/datasource"
"github.com/coreos/coreos-cloudinit/datasource/configdrive"
"github.com/coreos/coreos-cloudinit/datasource/file"
"github.com/coreos/coreos-cloudinit/datasource/metadata/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/datasource/waagent"
"github.com/coreos/coreos-cloudinit/initialize"
"github.com/coreos/coreos-cloudinit/pkg"
"github.com/coreos/coreos-cloudinit/system"
)
const (
version = "0.10.7"
datasourceInterval = 100 * time.Millisecond
datasourceMaxInterval = 30 * time.Second
datasourceTimeout = 5 * time.Minute
)
var (
flags = struct {
printVersion bool
ignoreFailure bool
sources struct {
file string
configDrive string
waagent 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(&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.StringVar(&flags.sources.waagent, "from-waagent", "", "Read data from provided waagent 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",
},
"azure": oemConfig{
"from-waagent": "/var/lib/waagent",
},
}
)
const version = "0.6.1"
func main() {
failure := false
var printVersion bool
flag.BoolVar(&printVersion, "version", false, "Print the version and exit")
var ignoreFailure bool
flag.BoolVar(&ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")
var file string
flag.StringVar(&file, "from-file", "", "Read user-data from provided file")
var url string
flag.StringVar(&url, "from-url", "", "Download user-data from provided url")
var useProcCmdline bool
flag.BoolVar(&useProcCmdline, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=<url>', using the cloud-config served by an HTTP GET to <url>", datasource.ProcCmdlineLocation, datasource.ProcCmdlineCloudConfigFlag))
var workspace string
flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
var sshKeyName string
flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
flag.Parse()
if c, ok := oemConfigs[flags.oem]; ok {
for k, v := range c {
flag.Set(k, v)
}
} 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 flags.printVersion == true {
if printVersion == true {
fmt.Printf("coreos-cloudinit version %s\n", version)
os.Exit(0)
}
switch flags.convertNetconf {
case "":
case "debian":
case "digitalocean":
default:
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-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")
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\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)
os.Exit(1)
}
// Extract IPv4 addresses from metadata if possible
var subs map[string]string
if len(metadataBytes) > 0 {
subs, err = initialize.ExtractIPsFromMetadata(metadataBytes)
if err != nil {
fmt.Printf("Failed extracting IPs from meta-data: %v\n", err)
os.Exit(1)
}
}
// Apply environment to user-data
env := initialize.NewEnvironment("/", ds.ConfigRoot(), flags.workspace, flags.convertNetconf, flags.sshKeyName, subs)
userdata := env.Apply(string(userdataBytes))
var ccm, ccu *initialize.CloudConfig
var script *system.Script
if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil {
fmt.Printf("Failed to parse meta-data: %v\n", err)
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\nContinuing...\n", err)
failure = true
var ds datasource.Datasource
if file != "" {
ds = datasource.NewLocalFile(file)
} else if url != "" {
ds = datasource.NewMetadataService(url)
} else if useProcCmdline {
ds = datasource.NewProcCmdline()
} else {
switch t := ud.(type) {
case *initialize.CloudConfig:
ccu = t
case system.Script:
script = &t
}
}
var cc *initialize.CloudConfig
if ccm != nil && ccu != nil {
fmt.Println("Merging cloud-config from meta-data and user-data")
merged := mergeCloudConfig(*ccm, *ccu)
cc = &merged
} else if ccm != nil && ccu == nil {
fmt.Println("Processing cloud-config from meta-data")
cc = ccm
} else if ccm == nil && ccu != nil {
fmt.Println("Processing cloud-config from user-data")
cc = ccu
} else {
fmt.Println("No cloud-config data to handle.")
}
if cc != nil {
if err = initialize.Apply(*cc, env); err != nil {
fmt.Printf("Failed to apply cloud-config: %v\n", err)
os.Exit(1)
}
}
if script != nil {
if err = runScript(*script, env); err != nil {
fmt.Printf("Failed to run script: %v\n", err)
os.Exit(1)
}
}
if failure && !flags.ignoreFailure {
fmt.Println("Provide one of --from-file, --from-url or --from-proc-cmdline")
os.Exit(1)
}
}
// mergeCloudConfig merges certain options from mdcc (a CloudConfig derived from
// meta-data) onto udcc (a CloudConfig derived from user-data), if they are
// not already set on udcc (i.e. user-data always takes precedence)
// NB: This needs to be kept in sync with ParseMetadata so that it tracks all
// elements of a CloudConfig which that function can populate.
func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudConfig) {
if mdcc.Hostname != "" {
if udcc.Hostname != "" {
fmt.Printf("Warning: user-data hostname (%s) overrides metadata hostname (%s)\n", udcc.Hostname, mdcc.Hostname)
} else {
udcc.Hostname = mdcc.Hostname
}
}
for _, key := range mdcc.SSHAuthorizedKeys {
udcc.SSHAuthorizedKeys = append(udcc.SSHAuthorizedKeys, key)
}
if mdcc.NetworkConfigPath != "" {
if udcc.NetworkConfigPath != "" {
fmt.Printf("Warning: user-data NetworkConfigPath %s overrides metadata NetworkConfigPath %s\n", udcc.NetworkConfigPath, mdcc.NetworkConfigPath)
} else {
udcc.NetworkConfigPath = mdcc.NetworkConfigPath
}
}
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
}
// getDatasources creates a slice of possible Datasources for cloudinit based
// on the different source command-line flags.
func getDatasources() []datasource.Datasource {
dss := make([]datasource.Datasource, 0, 5)
if flags.sources.file != "" {
dss = append(dss, file.NewDatasource(flags.sources.file))
}
if flags.sources.url != "" {
dss = append(dss, url.NewDatasource(flags.sources.url))
}
if flags.sources.configDrive != "" {
dss = append(dss, configdrive.NewDatasource(flags.sources.configDrive))
}
if flags.sources.metadataService {
dss = append(dss, ec2.NewDatasource(ec2.DefaultAddress))
}
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.waagent != "" {
dss = append(dss, waagent.NewDatasource(flags.sources.waagent))
}
if flags.sources.procCmdLine {
dss = append(dss, proc_cmdline.NewDatasource())
}
return dss
}
// selectDatasource attempts to choose a valid Datasource to use based on its
// current availability. The first Datasource to report to be available is
// returned. Datasources will be retried if possible if they are not
// immediately available. If all Datasources are permanently unavailable or
// datasourceTimeout is reached before one becomes available, nil is returned.
func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
ds := make(chan datasource.Datasource)
stop := make(chan struct{})
var wg sync.WaitGroup
for _, s := range sources {
wg.Add(1)
go func(s datasource.Datasource) {
defer wg.Done()
duration := datasourceInterval
for {
fmt.Printf("Checking availability of %q\n", s.Type())
if s.IsAvailable() {
ds <- s
return
} else if !s.AvailabilityChanges() {
return
}
select {
case <-stop:
return
case <-time.After(duration):
duration = pkg.ExpBackoff(duration, datasourceMaxInterval)
}
}
}(s)
}
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
var s datasource.Datasource
select {
case s = <-ds:
case <-done:
case <-time.After(datasourceTimeout):
}
close(stop)
return s
}
// TODO(jonboulle): this should probably be refactored and moved into a different module
func runScript(script system.Script, env *initialize.Environment) error {
err := initialize.PrepWorkspace(env.Workspace())
log.Printf("Fetching user-data from datasource of type %q", ds.Type())
userdataBytes, err := ds.Fetch()
if err != nil {
fmt.Printf("Failed preparing workspace: %v\n", err)
return err
log.Printf("Failed fetching user-data from datasource: %v", err)
if ignoreFailure {
os.Exit(0)
} else {
os.Exit(1)
}
}
path, err := initialize.PersistScriptInWorkspace(script, env.Workspace())
if err == nil {
var name string
name, err = system.ExecuteScript(path)
initialize.PersistUnitNameInWorkspace(name, env.Workspace())
if len(userdataBytes) == 0 {
log.Printf("No user data to handle, exiting.")
os.Exit(0)
}
env := initialize.NewEnvironment("/", workspace)
userdata := string(userdataBytes)
userdata = env.Apply(userdata)
parsed, err := initialize.ParseUserData(userdata)
if err != nil {
log.Printf("Failed parsing user-data: %v", err)
if ignoreFailure {
os.Exit(0)
} else {
os.Exit(1)
}
}
err = initialize.PrepWorkspace(env.Workspace())
if err != nil {
log.Fatalf("Failed preparing workspace: %v", err)
}
switch t := parsed.(type) {
case initialize.CloudConfig:
err = initialize.Apply(t, env)
case system.Script:
var path string
path, err = initialize.PersistScriptInWorkspace(t, env.Workspace())
if err == nil {
var name string
name, err = system.ExecuteScript(path)
initialize.PersistUnitNameInWorkspace(name, workspace)
}
}
if err != nil {
log.Fatalf("Failed resolving user-data: %v", err)
}
return err
}

View File

@@ -1,136 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"reflect"
"testing"
"github.com/coreos/coreos-cloudinit/initialize"
)
func TestMergeCloudConfig(t *testing.T) {
simplecc := initialize.CloudConfig{
SSHAuthorizedKeys: []string{"abc", "def"},
Hostname: "foobar",
NetworkConfigPath: "/path/somewhere",
NetworkConfig: `{}`,
}
for i, tt := range []struct {
udcc initialize.CloudConfig
mdcc initialize.CloudConfig
want initialize.CloudConfig
}{
{
// If mdcc is empty, udcc should be returned unchanged
simplecc,
initialize.CloudConfig{},
simplecc,
},
{
// If udcc is empty, mdcc should be returned unchanged(overridden)
initialize.CloudConfig{},
simplecc,
simplecc,
},
{
// user-data should override completely in the case of conflicts
simplecc,
initialize.CloudConfig{
Hostname: "meta-hostname",
NetworkConfigPath: "/path/meta",
NetworkConfig: `{"hostname":"test"}`,
},
simplecc,
},
{
// Mixed merge should succeed
initialize.CloudConfig{
SSHAuthorizedKeys: []string{"abc", "def"},
Hostname: "user-hostname",
NetworkConfigPath: "/path/somewhere",
NetworkConfig: `{"hostname":"test"}`,
},
initialize.CloudConfig{
SSHAuthorizedKeys: []string{"woof", "qux"},
Hostname: "meta-hostname",
},
initialize.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{
Hostname: "supercool",
},
initialize.CloudConfig{
SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"},
NetworkConfigPath: "/dev/fun",
NetworkConfig: `{"hostname":"test"}`,
},
initialize.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{
Hostname: "mememe",
ManageEtcHosts: initialize.EtcHosts("lolz"),
},
initialize.CloudConfig{
Hostname: "youyouyou",
NetworkConfigPath: "meta-meta-yo",
NetworkConfig: `{"hostname":"test"}`,
},
initialize.CloudConfig{
Hostname: "mememe",
ManageEtcHosts: initialize.EtcHosts("lolz"),
NetworkConfigPath: "meta-meta-yo",
NetworkConfig: `{"hostname":"test"}`,
},
},
{
// Non-mergeable (unexpected) settings in meta-data are ignored
initialize.CloudConfig{
Hostname: "mememe",
},
initialize.CloudConfig{
ManageEtcHosts: initialize.EtcHosts("lolz"),
NetworkConfigPath: "meta-meta-yo",
NetworkConfig: `{"hostname":"test"}`,
},
initialize.CloudConfig{
Hostname: "mememe",
NetworkConfigPath: "meta-meta-yo",
NetworkConfig: `{"hostname":"test"}`,
},
},
} {
got := mergeCloudConfig(tt.mdcc, tt.udcc)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("case #%d: mergeCloudConfig mutated CloudConfig unexpectedly:\ngot:\n%s\nwant:\n%s", i, got, tt.want)
}
}
}

View File

@@ -1,86 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
}

View File

@@ -1,141 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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 %v, 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 %v, got %q", tt, nil, err)
}
if string(filename) != tt.filename {
t.Fatalf("bad path for %q: want %q, got %q", tt, tt.filename, filename)
}
}
}
func TestConfigRoot(t *testing.T) {
for _, tt := range []struct {
root string
configRoot string
}{
{
"/",
"/openstack",
},
{
"/media/configdrive",
"/media/configdrive/openstack",
},
} {
cd := configDrive{tt.root, nil}
if configRoot := cd.ConfigRoot(); configRoot != tt.configRoot {
t.Fatalf("bad config root for %q: want %q, got %q", tt, tt.configRoot, configRoot)
}
}
}
func TestNewDatasource(t *testing.T) {
for _, tt := range []struct {
root string
expectRoot string
}{
{
root: "",
expectRoot: "",
},
{
root: "/media/configdrive",
expectRoot: "/media/configdrive",
},
} {
service := NewDatasource(tt.root)
if service.root != tt.expectRoot {
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
}
}
}

View File

@@ -1,27 +1,31 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package datasource
import (
"io/ioutil"
"net/http"
)
type Datasource interface {
IsAvailable() bool
AvailabilityChanges() bool
ConfigRoot() string
FetchMetadata() ([]byte, error)
FetchUserdata() ([]byte, error)
FetchNetworkConfig(string) ([]byte, error)
Type() string
Fetch() ([]byte, error)
Type() string
}
func fetchURL(url string) ([]byte, error) {
client := http.Client{}
resp, err := client.Get(url)
if err != nil {
return []byte{}, err
}
defer resp.Body.Close()
if resp.StatusCode / 100 != 2 {
return []byte{}, nil
}
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return respBytes, nil
}

21
datasource/file.go Normal file
View File

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

View File

@@ -1,59 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package file
import (
"io/ioutil"
"os"
)
type localFile struct {
path string
}
func NewDatasource(path string) *localFile {
return &localFile{path}
}
func (f *localFile) IsAvailable() bool {
_, err := os.Stat(f.path)
return !os.IsNotExist(err)
}
func (f *localFile) AvailabilityChanges() bool {
return true
}
func (f *localFile) ConfigRoot() string {
return ""
}
func (f *localFile) FetchMetadata() ([]byte, error) {
return []byte{}, nil
}
func (f *localFile) FetchUserdata() ([]byte, error) {
return ioutil.ReadFile(f.path)
}
func (f *localFile) FetchNetworkConfig(filename string) ([]byte, error) {
return nil, nil
}
func (f *localFile) Type() string {
return "local-file"
}

View File

@@ -1,161 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cloudsigma
import (
"encoding/base64"
"encoding/json"
"os"
"strings"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/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
}

View File

@@ -1,168 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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)
}
}
}

View File

@@ -1,123 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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"
}

View File

@@ -1,115 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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{Err: fmt.Errorf("test error")},
expectErr: pkg.ErrTimeout{Err: fmt.Errorf("test error")},
},
} {
service := &metadataService{
MetadataService: metadata.MetadataService{
Root: tt.root,
Client: &test.HttpClient{Resources: tt.resources, Err: 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 ""
}

View File

@@ -1,123 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
}
}

View File

@@ -1,201 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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{Resources: s.resources, Err: 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{Resources: s.resources, Err: 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{Err: fmt.Errorf("test error")},
expectErr: pkg.ErrTimeout{Err: fmt.Errorf("test error")},
},
} {
service := &metadataService{metadata.MetadataService{
Root: tt.root,
Client: &test.HttpClient{Resources: tt.resources, Err: 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 ""
}

View File

@@ -1,77 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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)
}

View File

@@ -1,187 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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 %t, got %t", 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{Resources: tt.resources, Err: nil},
ApiVersion: tt.apiVersion,
}
if a := service.IsAvailable(); a != tt.expect {
t.Fatalf("bad isAvailable (%q): want %t, got %t", 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{Err: fmt.Errorf("test not found error")},
userdata: []byte{},
},
{
root: "/",
clientErr: pkg.ErrTimeout{Err: fmt.Errorf("test timeout error")},
expectErr: pkg.ErrTimeout{Err: fmt.Errorf("test timeout error")},
},
} {
service := &MetadataService{
Root: tt.root,
Client: &test.HttpClient{Resources: tt.resources, Err: 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 ""
}

View File

@@ -1,43 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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)
}

View File

@@ -0,0 +1,17 @@
package datasource
type metadataService struct {
url string
}
func NewMetadataService(url string) *metadataService {
return &metadataService{url}
}
func (ms *metadataService) Fetch() ([]byte, error) {
return fetchURL(ms.url)
}
func (ms *metadataService) Type() string {
return "metadata-service"
}

View File

@@ -0,0 +1,66 @@
package datasource
import (
"errors"
"io/ioutil"
"log"
"strings"
)
const (
ProcCmdlineLocation = "/proc/cmdline"
ProcCmdlineCloudConfigFlag = "cloud-config-url"
)
type procCmdline struct{}
func NewProcCmdline() *procCmdline {
return &procCmdline{}
}
func (self *procCmdline) Fetch() ([]byte, error) {
cmdline, err := ioutil.ReadFile(ProcCmdlineLocation)
if err != nil {
return nil, err
}
url, err := findCloudConfigURL(string(cmdline))
if err != nil {
return nil, err
}
cfg, err := fetchURL(url)
if err != nil {
return nil, err
}
return cfg, nil
}
func (self *procCmdline) Type() string {
return "proc-cmdline"
}
func findCloudConfigURL(input string) (url string, err error) {
err = errors.New("cloud-config-url not found")
for _, token := range strings.Split(input, " ") {
parts := strings.SplitN(token, "=", 2)
key := parts[0]
key = strings.Replace(key, "_", "-", -1)
if key != "cloud-config-url" {
continue
}
if len(parts) != 2 {
log.Printf("Found cloud-config-url in /proc/cmdline with no value, ignoring.")
continue
}
url = parts[1]
err = nil
}
return
}

View File

@@ -1,115 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proc_cmdline
import (
"errors"
"io/ioutil"
"log"
"strings"
"github.com/coreos/coreos-cloudinit/pkg"
)
const (
ProcCmdlineLocation = "/proc/cmdline"
ProcCmdlineCloudConfigFlag = "cloud-config-url"
)
type procCmdline struct {
Location string
}
func NewDatasource() *procCmdline {
return &procCmdline{Location: ProcCmdlineLocation}
}
func (c *procCmdline) IsAvailable() bool {
contents, err := ioutil.ReadFile(c.Location)
if err != nil {
return false
}
cmdline := strings.TrimSpace(string(contents))
_, err = findCloudConfigURL(cmdline)
return (err == nil)
}
func (c *procCmdline) AvailabilityChanges() bool {
return false
}
func (c *procCmdline) ConfigRoot() string {
return ""
}
func (c *procCmdline) FetchMetadata() ([]byte, error) {
return []byte{}, nil
}
func (c *procCmdline) FetchUserdata() ([]byte, error) {
contents, err := ioutil.ReadFile(c.Location)
if err != nil {
return nil, err
}
cmdline := strings.TrimSpace(string(contents))
url, err := findCloudConfigURL(cmdline)
if err != nil {
return nil, err
}
client := pkg.NewHttpClient()
cfg, err := client.GetRetry(url)
if err != nil {
return nil, err
}
return cfg, nil
}
func (c *procCmdline) FetchNetworkConfig(filename string) ([]byte, error) {
return nil, nil
}
func (c *procCmdline) Type() string {
return "proc-cmdline"
}
func findCloudConfigURL(input string) (url string, err error) {
err = errors.New("cloud-config-url not found")
for _, token := range strings.Split(input, " ") {
parts := strings.SplitN(token, "=", 2)
key := parts[0]
key = strings.Replace(key, "_", "-", -1)
if key != "cloud-config-url" {
continue
}
if len(parts) != 2 {
log.Printf("Found cloud-config-url in /proc/cmdline with no value, ignoring.")
continue
}
url = parts[1]
err = nil
}
return
}

View File

@@ -1,104 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proc_cmdline
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func TestParseCmdlineCloudConfigFound(t *testing.T) {
tests := []struct {
input string
expect string
}{
{
"cloud-config-url=example.com",
"example.com",
},
{
"cloud_config_url=example.com",
"example.com",
},
{
"cloud-config-url cloud-config-url=example.com",
"example.com",
},
{
"cloud-config-url= cloud-config-url=example.com",
"example.com",
},
{
"cloud-config-url=one.example.com cloud-config-url=two.example.com",
"two.example.com",
},
{
"foo=bar cloud-config-url=example.com ping=pong",
"example.com",
},
}
for i, tt := range tests {
output, err := findCloudConfigURL(tt.input)
if output != tt.expect {
t.Errorf("Test case %d failed: %s != %s", i, output, tt.expect)
}
if err != nil {
t.Errorf("Test case %d produced error: %v", i, err)
}
}
}
func TestProcCmdlineAndFetchConfig(t *testing.T) {
var (
ProcCmdlineTmpl = "foo=bar cloud-config-url=%s/config\n"
CloudConfigContent = "#cloud-config\n"
)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && r.RequestURI == "/config" {
fmt.Fprint(w, CloudConfigContent)
}
}))
defer ts.Close()
file, err := ioutil.TempFile(os.TempDir(), "test_proc_cmdline")
defer os.Remove(file.Name())
if err != nil {
t.Errorf("Test produced error: %v", err)
}
_, err = file.Write([]byte(fmt.Sprintf(ProcCmdlineTmpl, ts.URL)))
if err != nil {
t.Errorf("Test produced error: %v", err)
}
p := NewDatasource()
p.Location = file.Name()
cfg, err := p.FetchUserdata()
if err != nil {
t.Errorf("Test produced error: %v", err)
}
if string(cfg) != CloudConfigContent {
t.Errorf("Test failed, response body: %s != %s", cfg, CloudConfigContent)
}
}

View File

@@ -0,0 +1,47 @@
package datasource
import (
"testing"
)
func TestParseCmdlineCloudConfigFound(t *testing.T) {
tests := []struct {
input string
expect string
}{
{
"cloud-config-url=example.com",
"example.com",
},
{
"cloud_config_url=example.com",
"example.com",
},
{
"cloud-config-url cloud-config-url=example.com",
"example.com",
},
{
"cloud-config-url= cloud-config-url=example.com",
"example.com",
},
{
"cloud-config-url=one.example.com cloud-config-url=two.example.com",
"two.example.com",
},
{
"foo=bar cloud-config-url=example.com ping=pong",
"example.com",
},
}
for i, tt := range tests {
output, err := findCloudConfigURL(tt.input)
if output != tt.expect {
t.Errorf("Test case %d failed: %s != %s", i, output, tt.expect)
}
if err != nil {
t.Errorf("Test case %d produced error: %v", i, err)
}
}
}

View File

@@ -1,60 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package url
import (
"github.com/coreos/coreos-cloudinit/pkg"
)
type remoteFile struct {
url string
}
func NewDatasource(url string) *remoteFile {
return &remoteFile{url}
}
func (f *remoteFile) IsAvailable() bool {
client := pkg.NewHttpClient()
_, err := client.Get(f.url)
return (err == nil)
}
func (f *remoteFile) AvailabilityChanges() bool {
return true
}
func (f *remoteFile) ConfigRoot() string {
return ""
}
func (f *remoteFile) FetchMetadata() ([]byte, error) {
return []byte{}, nil
}
func (f *remoteFile) FetchUserdata() ([]byte, error) {
client := pkg.NewHttpClient()
return client.GetRetry(f.url)
}
func (f *remoteFile) FetchNetworkConfig(filename string) ([]byte, error) {
return nil, nil
}
func (f *remoteFile) Type() string {
return "url"
}

View File

@@ -1,124 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package waagent
import (
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"net"
"os"
"path"
)
type waagent struct {
root string
readFile func(filename string) ([]byte, error)
}
func NewDatasource(root string) *waagent {
return &waagent{root, ioutil.ReadFile}
}
func (a *waagent) IsAvailable() bool {
_, err := os.Stat(path.Join(a.root, "provisioned"))
return !os.IsNotExist(err)
}
func (a *waagent) AvailabilityChanges() bool {
return true
}
func (a *waagent) ConfigRoot() string {
return a.root
}
func (a *waagent) FetchMetadata() ([]byte, error) {
metadataBytes, err := a.tryReadFile(path.Join(a.root, "SharedConfig.xml"))
if err != nil {
return nil, err
}
if len(metadataBytes) == 0 {
return metadataBytes, nil
}
type Instance struct {
Id string `xml:"id,attr"`
Address string `xml:"address,attr"`
InputEndpoints struct {
Endpoints []struct {
LoadBalancedPublicAddress string `xml:"loadBalancedPublicAddress,attr"`
} `xml:"Endpoint"`
}
}
type SharedConfig struct {
Incarnation struct {
Instance string `xml:"instance,attr"`
}
Instances struct {
Instances []Instance `xml:"Instance"`
}
}
var metadata SharedConfig
if err := xml.Unmarshal(metadataBytes, &metadata); err != nil {
return nil, err
}
var instance Instance
for _, i := range metadata.Instances.Instances {
if i.Id == metadata.Incarnation.Instance {
instance = i
break
}
}
attrs := map[string]string{
"local-ipv4": instance.Address,
}
for _, e := range instance.InputEndpoints.Endpoints {
host, _, err := net.SplitHostPort(e.LoadBalancedPublicAddress)
if err == nil {
attrs["public-ipv4"] = host
break
}
}
return json.Marshal(attrs)
}
func (a *waagent) FetchUserdata() ([]byte, error) {
return a.tryReadFile(path.Join(a.root, "CustomData"))
}
func (a *waagent) FetchNetworkConfig(filename string) ([]byte, error) {
return nil, nil
}
func (a *waagent) Type() string {
return "waagent"
}
func (a *waagent) tryReadFile(filename string) ([]byte, error) {
fmt.Printf("Attempting to read from %q\n", filename)
data, err := a.readFile(filename)
if os.IsNotExist(err) {
err = nil
}
return data, err
}

View File

@@ -1,184 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package waagent
import (
"encoding/json"
"os"
"reflect"
"testing"
)
type mockFilesystem map[string][]byte
func (m mockFilesystem) readFile(filename string) ([]byte, error) {
if contents := m[filename]; contents != nil {
return contents, nil
}
return nil, os.ErrNotExist
}
func TestFetchMetadata(t *testing.T) {
for _, tt := range []struct {
root string
files mockFilesystem
metadata map[string]string
}{
{
"/",
mockFilesystem{},
nil,
},
{
"/",
mockFilesystem{"/SharedConfig.xml": []byte("")},
nil,
},
{
"/var/lib/waagent",
mockFilesystem{"/var/lib/waagent/SharedConfig.xml": []byte("")},
nil,
},
{
"/var/lib/waagent",
mockFilesystem{"/var/lib/waagent/SharedConfig.xml": []byte(`<?xml version="1.0" encoding="utf-8"?>
<SharedConfig version="1.0.0.0" goalStateIncarnation="1">
<Deployment name="c8f9e4c9c18948e1bebf57c5685da756" guid="{1d10394f-c741-4a1a-a6bb-278f213c5a5e}" incarnation="0" isNonCancellableTopologyChangeEnabled="false">
<Service name="core-test-1" guid="{00000000-0000-0000-0000-000000000000}" />
<ServiceInstance name="c8f9e4c9c18948e1bebf57c5685da756.0" guid="{1e202e9a-8ffe-4915-b6ef-4118c9628fda}" />
</Deployment>
<Incarnation number="1" instance="core-test-1" guid="{8767eb4b-b445-4783-b1f5-6c0beaf41ea0}" />
<Role guid="{53ecc81e-257f-fbc9-a53a-8cf1a0a122b4}" name="core-test-1" settleTimeSeconds="0" />
<LoadBalancerSettings timeoutSeconds="0" waitLoadBalancerProbeCount="8">
<Probes>
<Probe name="D41D8CD98F00B204E9800998ECF8427E" />
<Probe name="C9DEC1518E1158748FA4B6081A8266DD" />
</Probes>
</LoadBalancerSettings>
<OutputEndpoints>
<Endpoint name="core-test-1:openInternalEndpoint" type="SFS">
<Target instance="core-test-1" endpoint="openInternalEndpoint" />
</Endpoint>
</OutputEndpoints>
<Instances>
<Instance id="core-test-1" address="100.73.202.64">
<FaultDomains randomId="0" updateId="0" updateCount="0" />
<InputEndpoints>
<Endpoint name="openInternalEndpoint" address="100.73.202.64" protocol="any" isPublic="false" enableDirectServerReturn="false" isDirectAddress="false" disableStealthMode="false">
<LocalPorts>
<LocalPortSelfManaged />
</LocalPorts>
</Endpoint>
<Endpoint name="ssh" address="100.73.202.64:22" protocol="tcp" hostName="core-test-1ContractContract" isPublic="true" loadBalancedPublicAddress="191.239.39.77:22" enableDirectServerReturn="false" isDirectAddress="false" disableStealthMode="false">
<LocalPorts>
<LocalPortRange from="22" to="22" />
</LocalPorts>
</Endpoint>
</InputEndpoints>
</Instance>
</Instances>
</SharedConfig>`)},
map[string]string{
"local-ipv4": "100.73.202.64",
"public-ipv4": "191.239.39.77",
},
},
} {
a := waagent{tt.root, tt.files.readFile}
metadataBytes, err := a.FetchMetadata()
if err != nil {
t.Fatalf("bad error for %q: want %v, got %q", tt, nil, err)
}
var metadata map[string]string
if len(metadataBytes) > 0 {
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
panic(err)
}
}
if !reflect.DeepEqual(tt.metadata, metadata) {
t.Fatalf("bad metadata for %q: want %q, got %q", tt, tt.metadata, metadata)
}
}
}
func TestFetchUserdata(t *testing.T) {
for _, tt := range []struct {
root string
files mockFilesystem
}{
{
"/",
mockFilesystem{},
},
{
"/",
mockFilesystem{"/CustomData": []byte{}},
},
{
"/var/lib/waagent/",
mockFilesystem{"/var/lib/waagent/CustomData": []byte{}},
},
} {
a := waagent{tt.root, tt.files.readFile}
_, err := a.FetchUserdata()
if err != nil {
t.Fatalf("bad error for %q: want %v, got %q", tt, nil, err)
}
}
}
func TestConfigRoot(t *testing.T) {
for _, tt := range []struct {
root string
configRoot string
}{
{
"/",
"/",
},
{
"/var/lib/waagent",
"/var/lib/waagent",
},
} {
a := waagent{tt.root, nil}
if configRoot := a.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: "/var/lib/waagent",
expectRoot: "/var/lib/waagent",
},
} {
service := NewDatasource(tt.root)
if service.root != tt.expectRoot {
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
}
}
}

View File

@@ -1,164 +1,37 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (
"errors"
"fmt"
"log"
"path"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1"
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
"github.com/coreos/coreos-cloudinit/network"
"github.com/coreos/coreos-cloudinit/system"
)
// CloudConfigFile represents a CoreOS specific configuration option that can generate
// an associated system.File to be written to disk
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)
}
// 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
Update map[string]string
Units []system.Unit
OEM OEMRelease
}
WriteFiles []system.File `yaml:"write_files"`
Hostname string
Users []system.User
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
NetworkConfigPath string
NetworkConfig string
WriteFiles []system.File `yaml:"write_files"`
Hostname string
Users []system.User
ManageEtcHosts string `yaml:"manage_etc_hosts"`
}
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(&system.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(&system.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)
}
}
}
}
}
}
}
// 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
err := goyaml.Unmarshal([]byte(contents), &cfg)
return &cfg, err
}
func (cc CloudConfig) String() string {
bytes, err := yaml.Marshal(cc)
bytes, err := goyaml.Marshal(cc)
if err != nil {
return ""
}
@@ -169,9 +42,6 @@ func (cc CloudConfig) String() string {
return stringified
}
// 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 {
if cfg.Hostname != "" {
if err := system.SetHostname(cfg.Hostname); err != nil {
@@ -180,45 +50,54 @@ func Apply(cfg CloudConfig, env *Environment) error {
log.Printf("Set hostname to %s", cfg.Hostname)
}
for _, user := range cfg.Users {
if user.Name == "" {
log.Printf("User object has no 'name' field, skipping")
continue
if cfg.Coreos.OEM.ID != "" {
if err := WriteOEMRelease(&cfg.Coreos.OEM, env.Root()); err != nil {
return err
}
log.Printf("Wrote /etc/oem-release to filesystem")
}
if system.UserExists(&user) {
log.Printf("User '%s' exists, ignoring creation-time fields", user.Name)
if user.PasswordHash != "" {
log.Printf("Setting '%s' user's password", user.Name)
if err := system.SetUserPassword(user.Name, user.PasswordHash); err != nil {
log.Printf("Failed setting '%s' user's password: %v", user.Name, err)
if len(cfg.Users) > 0 {
for _, user := range cfg.Users {
if user.Name == "" {
log.Printf("User object has no 'name' field, skipping")
continue
}
if system.UserExists(&user) {
log.Printf("User '%s' exists, ignoring creation-time fields", user.Name)
if user.PasswordHash != "" {
log.Printf("Setting '%s' user's password", user.Name)
if err := system.SetUserPassword(user.Name, user.PasswordHash); err != nil {
log.Printf("Failed setting '%s' user's password: %v", user.Name, err)
return err
}
}
} else {
log.Printf("Creating user '%s'", user.Name)
if err := system.CreateUser(&user); err != nil {
log.Printf("Failed creating user '%s': %v", user.Name, err)
return err
}
}
} else {
log.Printf("Creating user '%s'", user.Name)
if err := system.CreateUser(&user); err != nil {
log.Printf("Failed creating user '%s': %v", user.Name, err)
return err
}
}
if len(user.SSHAuthorizedKeys) > 0 {
log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name)
if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil {
return err
if len(user.SSHAuthorizedKeys) > 0 {
log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name)
if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil {
return err
}
}
}
if user.SSHImportGithubUser != "" {
log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name)
if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil {
return err
if user.SSHImportGithubUser != "" {
log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name)
if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil {
return err
}
}
}
if user.SSHImportURL != "" {
log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL)
if err := SSHImportKeysFromURL(user.Name, user.SSHImportURL); err != nil {
return err
if user.SSHImportURL != "" {
log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL)
if err := SSHImportKeysFromURL(user.Name, user.SSHImportURL); err != nil {
return err
}
}
}
}
@@ -232,142 +111,86 @@ 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())
if err != nil {
return err
}
if f != nil {
cfg.WriteFiles = append(cfg.WriteFiles, *f)
if len(cfg.WriteFiles) > 0 {
for _, file := range cfg.WriteFiles {
file.Path = path.Join(env.Root(), file.Path)
if err := system.WriteFile(&file); err != nil {
return err
}
log.Printf("Wrote file %s to filesystem", file.Path)
}
}
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
u, err := ccu.Units(env.Root())
if err != nil {
return err
if len(cfg.Coreos.Etcd) > 0 {
if err := WriteEtcdEnvironment(cfg.Coreos.Etcd, env.Root()); err != nil {
log.Fatalf("Failed to write etcd config to filesystem: %v", err)
}
cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
log.Printf("Wrote etcd config file to filesystem")
}
wroteEnvironment := false
for _, file := range cfg.WriteFiles {
fullPath, err := system.WriteFile(&file, env.Root())
if err != nil {
return err
if s, ok := cfg.Coreos.Update["reboot-strategy"]; ok {
if err := WriteLocksmithConfig(s, env.Root()); err != nil {
log.Fatalf("Failed to write locksmith config to filesystem: %v", err)
}
if path.Clean(file.Path) == "/etc/environment" {
wroteEnvironment = true
}
log.Printf("Wrote file %s to filesystem", fullPath)
log.Printf("Wrote locksmith config file to filesystem")
}
if !wroteEnvironment {
ef := env.DefaultEnvironmentFile()
if ef != nil {
err := system.WriteEnvFile(ef, env.Root())
if len(cfg.Coreos.Units) > 0 {
commands := make(map[string]string, 0)
for _, unit := range cfg.Coreos.Units {
dst := system.UnitDestination(&unit, env.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 {
return err
}
log.Printf("Placed unit %s at %s", unit.Name, dst)
}
if unit.Enable {
if unit.Group() != "network" {
log.Printf("Enabling unit file %s", dst)
if err := system.EnableUnitFile(dst, unit.Runtime); err != nil {
return err
}
log.Printf("Enabled unit %s", unit.Name)
} else {
log.Printf("Skipping enable for network-like unit %s", unit.Name)
}
}
if unit.Group() == "network" {
commands["systemd-networkd.service"] = "restart"
} else {
if unit.Command != "" {
commands[unit.Name] = unit.Command
}
}
}
if err := system.DaemonReload(); err != nil {
log.Fatalf("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)
if err != nil {
return err
}
log.Printf("Updated /etc/environment")
log.Printf("Result of '%s %s': %s", command, unit, res)
}
}
if env.NetconfType() != "" {
var interfaces []network.InterfaceGenerator
var err error
switch env.NetconfType() {
case "debian":
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())
if cfg.ManageEtcHosts != "" {
if err := WriteEtcHosts(cfg.ManageEtcHosts, env.Root()); err != nil {
log.Fatalf("Failed to write /etc/hosts to filesystem: %v", err)
}
if err != nil {
return err
}
log.Printf("Wrote /etc/hosts file to filesystem")
if err := system.WriteNetworkdConfigs(interfaces); err != nil {
return err
}
if err := system.RestartNetwork(interfaces); err != nil {
return err
}
}
um := system.NewUnitManager(env.Root())
return processUnits(cfg.Coreos.Units, env.Root(), um)
}
// processUnits takes a set of Units and applies them to the given root using
// the given UnitManager. This can involve things like writing unit files to
// disk, masking/unmasking units, or invoking systemd
// commands against units. It returns any error encountered.
func processUnits(units []system.Unit, root string, um system.UnitManager) error {
type action struct {
unit string
command string
}
actions := make([]action, 0, len(units))
reload := false
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 := um.PlaceUnit(&unit, dst); err != nil {
return err
}
log.Printf("Placed unit %s at %s", unit.Name, dst)
reload = true
}
if unit.Mask {
log.Printf("Masking unit file %s", unit.Name)
if err := um.MaskUnit(&unit); err != nil {
return err
}
} else if unit.Runtime {
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
if err := um.UnmaskUnit(&unit); err != nil {
return err
}
}
if unit.Enable {
if unit.Group() != "network" {
log.Printf("Enabling unit file %s", unit.Name)
if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
return err
}
log.Printf("Enabled unit %s", unit.Name)
} else {
log.Printf("Skipping enable for network-like unit %s", unit.Name)
}
}
if unit.Group() == "network" {
actions = append(actions, action{"systemd-networkd.service", "restart"})
} else if unit.Command != "" {
actions = append(actions, action{unit.Name, unit.Command})
}
}
if reload {
if err := um.DaemonReload(); err != nil {
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
}
}
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", action.command, action.unit, res)
}
return nil

View File

@@ -1,121 +1,10 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (
"fmt"
"strings"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestCloudConfigInvalidKeys(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("panic while instantiating CloudConfig with nil keys: %v", r)
}
}()
for _, tt := range []struct {
contents string
}{
{"coreos:"},
{"ssh_authorized_keys:"},
{"ssh_authorized_keys:\n -"},
{"ssh_authorized_keys:\n - 0:"},
{"write_files:"},
{"write_files:\n -"},
{"write_files:\n - 0:"},
{"users:"},
{"users:\n -"},
{"users:\n - 0:"},
} {
_, err := NewCloudConfig(tt.contents)
if err != nil {
t.Fatalf("error instantiating CloudConfig with invalid keys: %v", err)
}
}
}
func TestCloudConfigUnknownKeys(t *testing.T) {
contents := `
coreos:
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")
}
}
// Assert that the parsing of a cloud config file "generally works"
func TestCloudConfigEmpty(t *testing.T) {
cfg, err := NewCloudConfig("")
@@ -255,7 +144,7 @@ ssh_authorized_keys:
`
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error: %v", err)
t.Fatalf("Encountered unexpected error :%v", err)
}
keys := cfg.SSHAuthorizedKeys
@@ -273,26 +162,6 @@ func TestCloudConfigSerializationHeader(t *testing.T) {
}
}
// TestDropInIgnored asserts that users are unable to set DropIn=True on units
func TestDropInIgnored(t *testing.T) {
contents := `
coreos:
units:
- name: test
dropin: true
`
cfg, err := NewCloudConfig(contents)
if err != nil || len(cfg.Coreos.Units) != 1 {
t.Fatalf("Encountered unexpected error: %v", err)
}
if len(cfg.Coreos.Units) != 1 || cfg.Coreos.Units[0].Name != "test" {
t.Fatalf("Expected 1 unit, but got %d: %v", len(cfg.Coreos.Units), cfg.Coreos.Units)
}
if cfg.Coreos.Units[0].DropIn {
t.Errorf("dropin option on unit in cloud-config was not ignored!")
}
}
func TestCloudConfigUsers(t *testing.T) {
contents := `
users:
@@ -317,7 +186,7 @@ users:
}
if len(cfg.Users) != 1 {
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
}
user := cfg.Users[0]
@@ -378,109 +247,3 @@ users:
t.Errorf("Failed to parse no-log-init field")
}
}
type TestUnitManager struct {
placed []string
enabled []string
masked []string
unmasked []string
commands map[string]string
reload bool
}
func (tum *TestUnitManager) PlaceUnit(unit *system.Unit, dst string) error {
tum.placed = append(tum.placed, unit.Name)
return nil
}
func (tum *TestUnitManager) EnableUnitFile(unit string, runtime bool) error {
tum.enabled = append(tum.enabled, unit)
return nil
}
func (tum *TestUnitManager) RunUnitCommand(command, unit string) (string, error) {
tum.commands = make(map[string]string)
tum.commands[unit] = command
return "", nil
}
func (tum *TestUnitManager) DaemonReload() error {
tum.reload = true
return nil
}
func (tum *TestUnitManager) MaskUnit(unit *system.Unit) error {
tum.masked = append(tum.masked, unit.Name)
return nil
}
func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error {
tum.unmasked = append(tum.unmasked, unit.Name)
return nil
}
func TestProcessUnits(t *testing.T) {
tum := &TestUnitManager{}
units := []system.Unit{
system.Unit{
Name: "foo",
Mask: true,
},
}
if err := processUnits(units, "", tum); err != nil {
t.Fatalf("unexpected error calling processUnits: %v", err)
}
if len(tum.masked) != 1 || tum.masked[0] != "foo" {
t.Errorf("expected foo to be masked, but found %v", tum.masked)
}
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{
Name: "bar.network",
},
}
if err := processUnits(units, "", tum); err != nil {
t.Fatalf("unexpected error calling processUnits: %v", err)
}
if _, ok := tum.commands["systemd-networkd.service"]; !ok {
t.Errorf("expected systemd-networkd.service to be reloaded!")
}
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{
Name: "baz.service",
Content: "[Service]\nExecStart=/bin/true",
},
}
if err := processUnits(units, "", tum); err != nil {
t.Fatalf("unexpected error calling processUnits: %v", err)
}
if len(tum.placed) != 1 || tum.placed[0] != "baz.service" {
t.Fatalf("expected baz.service to be written, but got %v", tum.placed)
}
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{
Name: "locksmithd.service",
Runtime: true,
},
}
if err := processUnits(units, "", tum); err != nil {
t.Fatalf("unexpected error calling processUnits: %v", err)
}
if len(tum.unmasked) != 1 || tum.unmasked[0] != "locksmithd.service" {
t.Fatalf("expected locksmithd.service to be unmasked, but got %v", tum.unmasked)
}
tum = &TestUnitManager{}
units = []system.Unit{
system.Unit{
Name: "woof",
Enable: true,
},
}
if err := processUnits(units, "", tum); err != nil {
t.Fatalf("unexpected error calling processUnits: %v", err)
}
if len(tum.enabled) != 1 || tum.enabled[0] != "woof" {
t.Fatalf("expected woof to be enabled, but got %v", tum.enabled)
}
}

View File

@@ -1,135 +1,47 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (
"os"
"path"
"regexp"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
const DefaultSSHKeyName = "coreos-cloudinit"
type Environment struct {
root string
configRoot string
workspace string
netconfType string
sshKeyName string
substitutions map[string]string
}
// TODO(jonboulle): this is getting unwieldy, should be able to simplify the interface somehow
func NewEnvironment(root, configRoot, workspace, netconfType, sshKeyName string, substitutions map[string]string) *Environment {
if substitutions == nil {
substitutions = make(map[string]string)
}
// If certain values are not in the supplied substitution, fall back to retrieving them from the environment
for k, v := range map[string]string{
func NewEnvironment(root, workspace string) *Environment {
substitutions := 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
}
}
return &Environment{root, configRoot, workspace, netconfType, sshKeyName, substitutions}
return &Environment{root, workspace, DefaultSSHKeyName, substitutions}
}
func (e *Environment) Workspace() string {
return path.Join(e.root, e.workspace)
func (self *Environment) Workspace() string {
return path.Join(self.root, self.workspace)
}
func (e *Environment) Root() string {
return e.root
func (self *Environment) Root() string {
return self.root
}
func (e *Environment) ConfigRoot() string {
return e.configRoot
func (self *Environment) SSHKeyName() string {
return self.sshKeyName
}
func (e *Environment) NetconfType() string {
return e.netconfType
func (self *Environment) SetSSHKeyName(name string) {
self.sshKeyName = name
}
func (e *Environment) SSHKeyName() string {
return e.sshKeyName
}
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 {
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)
func (self *Environment) Apply(data string) string {
for key, val := range self.substitutions {
data = strings.Replace(data, key, val, -1)
}
return data
}
func (e *Environment) DefaultEnvironmentFile() *system.EnvFile {
ef := system.EnvFile{
File: &system.File{
Path: "/etc/environment",
},
Vars: map[string]string{},
}
if ip, ok := e.substitutions["$public_ipv4"]; ok && len(ip) > 0 {
ef.Vars["COREOS_PUBLIC_IPV4"] = ip
}
if ip, ok := e.substitutions["$private_ipv4"]; ok && len(ip) > 0 {
ef.Vars["COREOS_PRIVATE_IPV4"] = ip
}
if 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
}
}
// 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
}
return out
}

View File

@@ -1,148 +1,27 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
out string
}{
{
// Substituting both values directly should always take precedence
// over environment variables
map[string]string{
"$public_ipv4": "192.0.2.3",
"$private_ipv4": "192.0.2.203",
"$public_ipv6": "fe00:1234::",
"$private_ipv6": "fe00:5678::",
},
`[Service]
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 fe00:1234::"
ExecStop=/usr/bin/echo 192.0.2.203 fe00:5678::
ExecStop=/usr/bin/echo $unknown`,
},
{
// Substituting one value directly while falling back with the other
map[string]string{"$private_ipv4": "127.0.0.1"},
"$private_ipv4\n$public_ipv4",
"127.0.0.1\n1.2.3.4",
},
{
// Falling back to environment variables for both values
map[string]string{"foo": "bar"},
"$private_ipv4\n$public_ipv4",
"5.6.7.8\n1.2.3.4",
},
{
// No substitutions
nil,
"$private_ipv4\nfoobar",
"5.6.7.8\nfoobar",
},
{
// 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",
},
} {
os.Setenv("COREOS_PUBLIC_IPV4", "192.0.2.3")
os.Setenv("COREOS_PRIVATE_IPV4", "192.0.2.203")
env := NewEnvironment("./", "./")
input := `[Service]
ExecStart=/usr/bin/echo "$public_ipv4"
ExecStop=/usr/bin/echo $private_ipv4
ExecStop=/usr/bin/echo $unknown
`
expected := `[Service]
ExecStart=/usr/bin/echo "192.0.2.3"
ExecStop=/usr/bin/echo 192.0.2.203
ExecStop=/usr/bin/echo $unknown
`
env := NewEnvironment("./", "./", "./", "", "", tt.subs)
got := env.Apply(tt.input)
if got != tt.out {
t.Fatalf("Environment incorrectly applied.\ngot:\n%s\nwant:\n%s", got, tt.out)
}
}
}
func TestEnvironmentFile(t *testing.T) {
subs := map[string]string{
"$public_ipv4": "1.2.3.4",
"$private_ipv4": "5.6.7.8",
"$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)
output := env.Apply(input)
if output != expected {
t.Fatalf("Environment incorrectly applied.\nOutput:\n%s\nExpected:\n%s", output, expected)
}
}

View File

@@ -3,15 +3,26 @@ package initialize
import (
"errors"
"fmt"
"sort"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
type EtcdEnvironment map[string]string
func (ee EtcdEnvironment) String() (out string) {
norm := normalizeSvcEnv(ee)
func (ec EtcdEnvironment) normalized() map[string]string {
out := make(map[string]string, len(ec))
for key, val := range ec {
key = strings.ToUpper(key)
key = strings.Replace(key, "-", "_", -1)
out[key] = val
}
return out
}
func (ec EtcdEnvironment) String() (out string) {
norm := ec.normalized()
if val, ok := norm["DISCOVERY_URL"]; ok {
delete(norm, "DISCOVERY_URL")
@@ -20,44 +31,32 @@ func (ee EtcdEnvironment) String() (out string) {
}
}
var sorted sort.StringSlice
for k, _ := range norm {
sorted = append(sorted, k)
}
sorted.Sort()
out += "[Service]\n"
for _, key := range sorted {
val := norm[key]
for key, val := range norm {
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 len(ee) < 1 {
return nil, nil
}
if _, ok := ee["name"]; !ok {
// Write an EtcdEnvironment to the appropriate path on disk for etcd.service
func WriteEtcdEnvironment(env EtcdEnvironment, root string) error {
if _, ok := env["name"]; !ok {
if machineID := system.MachineID(root); machineID != "" {
ee["name"] = machineID
env["name"] = machineID
} else if hostname, err := system.Hostname(); err == nil {
ee["name"] = hostname
env["name"] = hostname
} else {
return nil, errors.New("Unable to determine default etcd name")
return errors.New("Unable to determine default etcd name")
}
}
etcd := system.Unit{
Name: "etcd.service",
Runtime: true,
DropIn: true,
Content: ee.String(),
file := system.File{
Path: path.Join(root, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf"),
RawFilePermissions: "0644",
Content: env.String(),
}
return []system.Unit{etcd}, nil
return system.WriteFile(&file)
}

View File

@@ -3,10 +3,9 @@ package initialize
import (
"io/ioutil"
"os"
"os/exec"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestEtcdEnvironment(t *testing.T) {
@@ -59,7 +58,7 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
}
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
ee := EtcdEnvironment{
ec := EtcdEnvironment{
"name": "node001",
"discovery": "http://disco.example.com/foobar",
"peer-bind-addr": "127.0.0.1:7002",
@@ -70,21 +69,8 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
}
defer os.RemoveAll(dir)
sd := system.NewUnitManager(dir)
uu, err := ee.Units(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
}
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 := sd.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
if err := WriteEtcdEnvironment(ec, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
@@ -104,8 +90,8 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
}
expect := `[Service]
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_NAME=node001"
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`
if string(contents) != expect {
@@ -113,46 +99,22 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
}
}
func TestEtcdEnvironmentEmptyNoOp(t *testing.T) {
ee := EtcdEnvironment{}
uu, err := ee.Units("")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(uu) > 0 {
t.Fatalf("Generated etcd units unexpectedly: %v", uu)
}
}
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
ee := EtcdEnvironment{"foo": "bar"}
ec := EtcdEnvironment{}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
sd := system.NewUnitManager(dir)
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
if err != nil {
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 := sd.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
if err := WriteEtcdEnvironment(ec, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
@@ -163,7 +125,6 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
}
expect := `[Service]
Environment="ETCD_FOO=bar"
Environment="ETCD_NAME=node007"
`
if string(contents) != expect {
@@ -171,14 +132,7 @@ Environment="ETCD_NAME=node007"
}
}
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")
}
func rmdir(path string) error {
cmd := exec.Command("rm", "-rf", path)
return cmd.Run()
}

View File

@@ -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
}

View File

@@ -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!")
}
}

View File

@@ -1,19 +1,3 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (

View File

@@ -17,7 +17,7 @@ users:
}
if len(cfg.Users) != 1 {
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
}
user := cfg.Users[0]

85
initialize/locksmith.go Normal file
View File

@@ -0,0 +1,85 @@
package initialize
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
const locksmithUnit = "locksmithd.service"
// addStrategy creates an `/etc/coreos/update.conf` file with the requested
// strategy via rewriting the file on disk or by starting from
// `/usr/share/coreos/update.conf`.
func addStrategy(strategy string, root string) error {
etcUpdate := path.Join(root, "etc", "coreos", "update.conf")
usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf")
// Ensure /etc/coreos/ exists before attempting to write a file in it
os.MkdirAll(path.Join(root, "etc", "coreos"), 0755)
tmp, err := ioutil.TempFile(path.Join(root, "etc", "coreos"), ".update.conf")
if err != nil {
return err
}
err = tmp.Chmod(0644)
if err != nil {
return err
}
conf, err := os.Open(etcUpdate)
if os.IsNotExist(err) {
conf, err = os.Open(usrUpdate)
if err != nil {
return err
}
}
scanner := bufio.NewScanner(conf)
sawStrat := false
stratLine := "REBOOT_STRATEGY="+strategy
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "REBOOT_STRATEGY=") {
line = stratLine
sawStrat = true
}
fmt.Fprintln(tmp, line)
if err := scanner.Err(); err != nil {
return err
}
}
if !sawStrat {
fmt.Fprintln(tmp, stratLine)
}
return os.Rename(tmp.Name(), etcUpdate)
}
// WriteLocksmithConfig updates the `update.conf` file with a REBOOT_STRATEGY for locksmith.
func WriteLocksmithConfig(strategy string, root string) error {
cmd := "restart"
if strategy == "off" {
err := system.MaskUnit(locksmithUnit, root)
if err != nil {
return err
}
cmd = "stop"
} else {
return addStrategy(strategy, root)
}
if err := system.DaemonReload(); err != nil {
return err
}
if _, err := system.RunUnitCommand(cmd, locksmithUnit); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,91 @@
package initialize
import (
"io/ioutil"
"os"
"path"
"testing"
)
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 TestLocksmithEnvironmentWrittenToDisk(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)
}
}
if err := WriteLocksmithConfig("etcd-lock", dir); err != nil {
t.Fatalf("Processing of LocksmithEnvironment failed: %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)
}
}
}
func TestLocksmithEnvironmentMasked(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)
if err := WriteLocksmithConfig("off", dir); err != nil {
t.Fatalf("Processing of LocksmithEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "etc", "systemd", "system", "locksmithd.service")
target, err := os.Readlink(fullPath)
if err != nil {
t.Fatalf("Unable to read link %v", err)
}
if target != "/dev/null" {
t.Fatalf("Locksmith not masked, unit target %v", target)
}
}

View File

@@ -1,19 +1,3 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (
@@ -27,10 +11,8 @@ import (
const DefaultIpv4Address = "127.0.0.1"
type EtcHosts string
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
if eh != "localhost" {
func generateEtcHosts(option string) (out string, err error) {
if option != "localhost" {
return "", errors.New("Invalid option to manage_etc_hosts")
}
@@ -44,19 +26,19 @@ func (eh EtcHosts) generateEtcHosts() (out string, err error) {
}
func (eh EtcHosts) File(root string) (*system.File, error) {
if eh == "" {
return nil, nil
}
// Write an /etc/hosts file
func WriteEtcHosts(option string, root string) error {
etcHosts, err := eh.generateEtcHosts()
etcHosts, err := generateEtcHosts(option)
if err != nil {
return nil, err
return err
}
return &system.File{
Path: path.Join("etc", "hosts"),
file := system.File{
Path: path.Join(root, "etc", "hosts"),
RawFilePermissions: "0644",
Content: etcHosts,
}, nil
}
return system.WriteFile(&file)
}

View File

@@ -6,8 +6,6 @@ import (
"os"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestCloudConfigManageEtcHosts(t *testing.T) {
@@ -27,9 +25,14 @@ manage_etc_hosts: localhost
}
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!")
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer rmdir(dir)
if err := WriteEtcHosts("invalid", dir); err == nil {
t.Fatalf("WriteEtcHosts succeeded with invalid value: %v", err)
}
}
@@ -38,20 +41,10 @@ func TestEtcHostsWrittenToDisk(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
defer rmdir(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)
if err := WriteEtcHosts("localhost", dir); err != nil {
t.Fatalf("WriteEtcHosts failed: %v", err)
}
fullPath := path.Join(dir, "etc", "hosts")

View File

@@ -1,88 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
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) {
if len(contents) == 0 {
return nil, nil
}
var metadata struct {
SSHAuthorizedKeyMap map[string]string `json:"public_keys"`
Hostname string `json:"hostname"`
NetworkConfig struct {
ContentPath string `json:"content_path"`
} `json:"network_config"`
}
if err := json.Unmarshal([]byte(contents), &metadata); err != nil {
return nil, err
}
var cfg CloudConfig
if len(metadata.SSHAuthorizedKeyMap) > 0 {
cfg.SSHAuthorizedKeys = make([]string, 0, len(metadata.SSHAuthorizedKeyMap))
for _, name := range sortedKeys(metadata.SSHAuthorizedKeyMap) {
cfg.SSHAuthorizedKeys = append(cfg.SSHAuthorizedKeys, metadata.SSHAuthorizedKeyMap[name])
}
}
cfg.Hostname = metadata.Hostname
cfg.NetworkConfigPath = metadata.NetworkConfig.ContentPath
return &cfg, nil
}
// ExtractIPsFromMetaData parses a JSON blob in the OpenStack metadata service
// format and returns a substitution map possibly containing private_ipv4,
// public_ipv4, private_ipv6, and public_ipv6 addresses.
func ExtractIPsFromMetadata(contents []byte) (map[string]string, error) {
var ips struct {
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.PrivateIPv4 != "" {
m["$private_ipv4"] = ips.PrivateIPv4
}
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
}

View File

@@ -1,85 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import "reflect"
import "testing"
func TestParseMetadata(t *testing.T) {
for i, tt := range []struct {
in string
want *CloudConfig
err bool
}{
{"", nil, false},
{`garbage, invalid json`, nil, true},
{`{"foo": "bar"}`, &CloudConfig{}, false},
{`{"network_config": {"content_path": "asdf"}}`, &CloudConfig{NetworkConfigPath: "asdf"}, false},
{`{"hostname": "turkleton"}`, &CloudConfig{Hostname: "turkleton"}, false},
{`{"public_keys": {"jack": "jill", "bob": "alice"}}`, &CloudConfig{SSHAuthorizedKeys: []string{"alice", "jill"}}, false},
{`{"unknown": "thing", "hostname": "my_host", "public_keys": {"do": "re", "mi": "fa"}, "network_config": {"content_path": "/root", "blah": "zzz"}}`, &CloudConfig{SSHAuthorizedKeys: []string{"re", "fa"}, Hostname: "my_host", NetworkConfigPath: "/root"}, false},
} {
got, err := ParseMetaData(tt.in)
if tt.err != (err != nil) {
t.Errorf("case #%d: bad error state: got %t, want %t (err=%v)", i, (err != nil), tt.err, err)
}
if got == nil {
if tt.want != nil {
t.Errorf("case #%d: unexpected nil output", i)
}
} else if tt.want == nil {
t.Errorf("case #%d: unexpected non-nil output", i)
} else {
if !reflect.DeepEqual(*got, *tt.want) {
t.Errorf("case #%d: bad output:\ngot\n%v\nwant\n%v", i, *got, *tt.want)
}
}
}
}
func TestExtractIPsFromMetadata(t *testing.T) {
for i, tt := range []struct {
in []byte
err bool
out map[string]string
}{
{
[]byte(`{"public-ipv4": "12.34.56.78", "local-ipv4": "1.2.3.4", "public-ipv6": "1234::", "local-ipv6": "5678::"}`),
false,
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"}`),
false,
map[string]string{"$private_ipv4": "127.0.0.1"},
},
{
[]byte(`garbage`),
true,
nil,
},
} {
got, err := ExtractIPsFromMetadata(tt.in)
if (err != nil) != tt.err {
t.Errorf("bad error state (got %t, want %t)", err != nil, tt.err)
}
if !reflect.DeepEqual(got, tt.out) {
t.Errorf("case %d: got %s, want %s", i, got, tt.out)
}
}
}

View File

@@ -16,7 +16,7 @@ type OEMRelease struct {
BugReportURL string `yaml:"bug-report-url"`
}
func (oem OEMRelease) String() string {
func (oem *OEMRelease) String() string {
fields := []string{
fmt.Sprintf("ID=%s", oem.ID),
fmt.Sprintf("VERSION_ID=%s", oem.VersionID),
@@ -28,14 +28,12 @@ func (oem OEMRelease) String() string {
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"),
func WriteOEMRelease(oem *OEMRelease, root string) error {
file := system.File{
Path: path.Join(root, "etc", "oem-release"),
RawFilePermissions: "0644",
Content: oem.String(),
}, nil
}
return system.WriteFile(&file)
}

View File

@@ -5,8 +5,6 @@ import (
"os"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestOEMReleaseWrittenToDisk(t *testing.T) {
@@ -23,16 +21,8 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
}
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)
if err := WriteOEMRelease(&oem, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "etc", "oem-release")

View File

@@ -1,26 +1,11 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/coreos/coreos-cloudinit/pkg"
"github.com/coreos/coreos-cloudinit/system"
)
@@ -40,19 +25,22 @@ func SSHImportKeysFromURL(system_user string, url string) error {
}
func fetchUserKeys(url string) ([]string, error) {
client := pkg.NewHttpClient()
data, err := client.GetRetry(url)
res, err := http.Get(url)
defer res.Body.Close()
if err != nil {
return nil, err
}
var userKeys []UserKey
err = json.Unmarshal(data, &userKeys)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
var data []UserKey
err = json.Unmarshal(body, &data)
if err != nil {
return nil, err
}
keys := make([]string, 0)
for _, key := range userKeys {
for _, key := range data {
keys = append(keys, key.Key)
}
return keys, err

View File

@@ -1,19 +1,3 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (
@@ -70,7 +54,7 @@ users:
}
if len(cfg.Users) != 1 {
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
}
user := cfg.Users[0]

View File

@@ -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
}

View File

@@ -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, s.name)
}
}
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)
}
}
}

View File

@@ -1,19 +1,3 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (
@@ -25,22 +9,24 @@ import (
)
func ParseUserData(contents string) (interface{}, error) {
if len(contents) == 0 {
return nil, nil
}
header := strings.SplitN(contents, "\n", 2)[0]
// Explicitly trim the header so we can handle user-data from
// non-unix operating systems. The rest of the file is parsed
// by yaml, which correctly handles CRLF.
// by goyaml, which correctly handles CRLF.
header = strings.TrimSpace(header)
if strings.HasPrefix(header, "#!") {
log.Printf("Parsing user-data as script")
return system.Script(contents), nil
} else if header == "#cloud-config" {
log.Printf("Parsing user-data as cloud-config")
return NewCloudConfig(contents)
cfg, err := NewCloudConfig(contents)
if err != nil {
log.Fatal(err.Error())
}
return *cfg, nil
} else {
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
}

View File

@@ -1,19 +1,3 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (
@@ -53,7 +37,7 @@ func TestParseConfigCRLF(t *testing.T) {
t.Fatalf("Failed parsing config: %v", err)
}
cfg := ud.(*CloudConfig)
cfg := ud.(CloudConfig)
if cfg.Hostname != "foo" {
t.Error("Failed parsing hostname from config")
@@ -63,12 +47,3 @@ func TestParseConfigCRLF(t *testing.T) {
t.Error("Parsed incorrect number of SSH keys")
}
}
func TestParseConfigEmpty(t *testing.T) {
i, e := ParseUserData(``)
if i != nil {
t.Error("ParseUserData of empty string returned non-nil unexpectedly")
} else if e != nil {
t.Error("ParseUserData of empty string returned error unexpectedly")
}
}

View File

@@ -1,25 +1,8 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package initialize
import (
"io/ioutil"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
@@ -45,23 +28,21 @@ func PersistScriptInWorkspace(script system.Script, workspace string) (string, e
}
tmp.Close()
relpath := strings.TrimPrefix(tmp.Name(), workspace)
file := system.File{
Path: relpath,
Path: tmp.Name(),
RawFilePermissions: "0744",
Content: string(script),
Content: string(script),
}
return system.WriteFile(&file, workspace)
err = system.WriteFile(&file)
return file.Path, err
}
func PersistUnitNameInWorkspace(name string, workspace string) error {
file := system.File{
Path: path.Join("scripts", "unit-name"),
Path: path.Join(workspace, "scripts", "unit-name"),
RawFilePermissions: "0644",
Content: name,
Content: name,
}
_, err := system.WriteFile(&file, workspace)
return err
return system.WriteFile(&file)
}

View File

@@ -1,65 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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 {
return nil, err
}
interfaces := make([]*stanzaInterface, 0, len(stanzas))
for _, stanza := range stanzas {
switch s := stanza.(type) {
case *stanzaInterface:
interfaces = append(interfaces, s)
}
}
log.Printf("Parsed %d network interfaces\n", len(interfaces))
log.Println("Processed Debian network config")
return buildInterfaces(interfaces), nil
}
func formatConfig(config string) []string {
lines := []string{}
config = strings.Replace(config, "\\\n", "", -1)
for config != "" {
split := strings.SplitN(config, "\n", 2)
line := strings.TrimSpace(split[0])
if len(split) == 2 {
config = split[1]
} else {
config = ""
}
if strings.HasPrefix(line, "#") || line == "" {
continue
}
lines = append(lines, line)
}
return lines
}

View File

@@ -1,58 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package network
import (
"testing"
)
func TestFormatConfigs(t *testing.T) {
for in, n := range map[string]int{
"": 0,
"line1\\\nis long": 1,
"#comment": 0,
"#comment\\\ncomment": 0,
" #comment \\\n comment\nline 1\nline 2\\\n is long": 2,
} {
lines := formatConfig(in)
if len(lines) != n {
t.Fatalf("bad number of lines for config %q: got %d, want %d", in, len(lines), n)
}
}
}
func TestProcessDebianNetconf(t *testing.T) {
for _, tt := range []struct {
in string
fail bool
n int
}{
{"", false, 0},
{"iface", true, -1},
{"auto eth1\nauto eth2", false, 0},
{"iface eth1 inet manual", false, 1},
} {
interfaces, err := ProcessDebianNetconf(tt.in)
failed := err != nil
if tt.fail != failed {
t.Fatalf("bad failure state for %q: got %t, want %t", tt.in, failed, tt.fail)
}
if tt.n != -1 && tt.n != len(interfaces) {
t.Fatalf("bad number of interfaces for %q: got %d, want %q", tt.in, len(interfaces), tt.n)
}
}
}

View File

@@ -1,158 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
}

View File

@@ -1,401 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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{Nameservers: []string{"1.2.3.4"}},
nss: []net.IP{net.ParseIP("1.2.3.4")},
},
{
dns: digitalocean.DNS{Nameservers: []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{
IP: net.ParseIP("1.2.3.4"),
Mask: 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{
IP: net.ParseIP("1.2.3.4"),
Mask: net.IPMask(net.ParseIP("255.255.0.0")),
}},
nameservers: []net.IP{},
routes: []route{route{
net.IPNet{IP: net.IPv4zero, Mask: 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{
IP: net.ParseIP("fe00::"),
Mask: 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{
IP: net.ParseIP("fe00::"),
Mask: net.IPMask(net.ParseIP("ffff::")),
}},
nameservers: []net.IP{},
routes: []route{route{
net.IPNet{IP: net.IPv6zero, Mask: 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())
}

View File

@@ -1,332 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package network
import (
"fmt"
"net"
"sort"
"strconv"
"strings"
)
type InterfaceGenerator interface {
Name() string
Filename() string
Netdev() string
Link() string
Network() string
Type() string
ModprobeParams() string
}
type networkInterface interface {
InterfaceGenerator
Children() []networkInterface
setConfigDepth(int)
}
type logicalInterface struct {
name string
hwaddr net.HardwareAddr
config configMethod
children []networkInterface
configDepth int
}
func (i *logicalInterface) Name() string {
return i.name
}
func (i *logicalInterface) Network() string {
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) {
case *vlanInterface:
config += fmt.Sprintf("VLAN=%s\n", iface.name)
case *bondInterface:
config += fmt.Sprintf("Bond=%s\n", iface.name)
}
}
switch conf := i.config.(type) {
case configMethodStatic:
for _, nameserver := range conf.nameservers {
config += fmt.Sprintf("DNS=%s\n", nameserver)
}
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)
}
case configMethodDHCP:
config += "DHCP=true\n"
}
return config
}
func (i *logicalInterface) Link() string {
return ""
}
func (i *logicalInterface) Netdev() string {
return ""
}
func (i *logicalInterface) Filename() string {
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
}
type physicalInterface struct {
logicalInterface
}
func (p *physicalInterface) Type() string {
return "physical"
}
type bondInterface struct {
logicalInterface
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) Netdev() string {
config := fmt.Sprintf("[NetDev]\nKind=vlan\nName=%s\n", v.name)
switch c := v.config.(type) {
case configMethodStatic:
if c.hwaddress != nil {
config += fmt.Sprintf("MACAddress=%s\n", c.hwaddress)
}
case configMethodDHCP:
if c.hwaddress != nil {
config += fmt.Sprintf("MACAddress=%s\n", c.hwaddress)
}
}
config += fmt.Sprintf("\n[VLAN]\nId=%d\n", v.id)
return config
}
func (v *vlanInterface) Type() string {
return "vlan"
}
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
interfaceMap := createInterfaces(stanzas)
linkAncestors(interfaceMap)
markConfigDepths(interfaceMap)
interfaces := make([]InterfaceGenerator, 0, len(interfaceMap))
for _, name := range sortedInterfaces(interfaceMap) {
interfaces = append(interfaces, interfaceMap[name])
}
return interfaces
}
func createInterfaces(stanzas []*stanzaInterface) map[string]networkInterface {
interfaceMap := make(map[string]networkInterface)
for _, iface := range stanzas {
switch iface.kind {
case interfaceBond:
bondOptions := make(map[string]string)
for _, k := range []string{"mode", "miimon", "lacp-rate"} {
if v, ok := iface.options["bond-"+k]; ok && len(v) > 0 {
bondOptions[k] = v[0]
}
}
interfaceMap[iface.name] = &bondInterface{
logicalInterface{
name: iface.name,
config: iface.configMethod,
children: []networkInterface{},
},
iface.options["bond-slaves"],
bondOptions,
}
for _, slave := range iface.options["bond-slaves"] {
if _, ok := interfaceMap[slave]; !ok {
interfaceMap[slave] = &physicalInterface{
logicalInterface{
name: slave,
config: configMethodManual{},
children: []networkInterface{},
},
}
}
}
case interfacePhysical:
if _, ok := iface.configMethod.(configMethodLoopback); ok {
continue
}
interfaceMap[iface.name] = &physicalInterface{
logicalInterface{
name: iface.name,
config: iface.configMethod,
children: []networkInterface{},
},
}
case interfaceVLAN:
var rawDevice string
id, _ := strconv.Atoi(iface.options["id"][0])
if device := iface.options["raw_device"]; len(device) == 1 {
rawDevice = device[0]
if _, ok := interfaceMap[rawDevice]; !ok {
interfaceMap[rawDevice] = &physicalInterface{
logicalInterface{
name: rawDevice,
config: configMethodManual{},
children: []networkInterface{},
},
}
}
}
interfaceMap[iface.name] = &vlanInterface{
logicalInterface{
name: iface.name,
config: iface.configMethod,
children: []networkInterface{},
},
id,
rawDevice,
}
}
}
return interfaceMap
}
func linkAncestors(interfaceMap map[string]networkInterface) {
for _, name := range sortedInterfaces(interfaceMap) {
iface := interfaceMap[name]
switch i := iface.(type) {
case *vlanInterface:
if parent, ok := interfaceMap[i.rawDevice]; ok {
switch p := parent.(type) {
case *physicalInterface:
p.children = append(p.children, iface)
case *bondInterface:
p.children = append(p.children, iface)
}
}
case *bondInterface:
for _, slave := range i.slaves {
if parent, ok := interfaceMap[slave]; ok {
switch p := parent.(type) {
case *physicalInterface:
p.children = append(p.children, iface)
case *bondInterface:
p.children = append(p.children, iface)
}
}
}
}
}
}
func markConfigDepths(interfaceMap map[string]networkInterface) {
rootInterfaceMap := make(map[string]networkInterface)
for k, v := range interfaceMap {
rootInterfaceMap[k] = v
}
for _, iface := range interfaceMap {
for _, child := range iface.Children() {
delete(rootInterfaceMap, child.Name())
}
}
for _, iface := range rootInterfaceMap {
setDepth(iface)
}
}
func setDepth(iface networkInterface) int {
maxDepth := 0
for _, child := range iface.Children() {
if depth := setDepth(child); depth > maxDepth {
maxDepth = depth
}
}
iface.setConfigDepth(maxDepth)
return (maxDepth + 1)
}
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
}

View File

@@ -1,370 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package network
import (
"net"
"reflect"
"testing"
)
func TestInterfaceGenerators(t *testing.T) {
for _, tt := range []struct {
name string
netdev string
link string
network string
kind string
iface InterfaceGenerator
}{
{
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}),
}},
},
{
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},
},
}},
},
{
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 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 TestModprobeParams(t *testing.T) {
for _, tt := range []struct {
i InterfaceGenerator
p string
}{
{
i: &physicalInterface{},
p: "",
},
{
i: &vlanInterface{},
p: "",
},
{
i: &bondInterface{
logicalInterface{},
nil,
map[string]string{
"a": "1",
"b": "2",
},
},
p: "a=1 b=2",
},
} {
if p := tt.i.ModprobeParams(); p != tt.p {
t.Fatalf("bad params (%q): got %s, want %s", tt.i, p, tt.p)
}
}
}
func TestBuildInterfacesLo(t *testing.T) {
stanzas := []*stanzaInterface{
&stanzaInterface{
name: "lo",
kind: interfacePhysical,
auto: false,
configMethod: configMethodLoopback{},
options: map[string][]string{},
},
}
interfaces := buildInterfaces(stanzas)
if len(interfaces) != 0 {
t.FailNow()
}
}
func TestBuildInterfacesBlindBond(t *testing.T) {
stanzas := []*stanzaInterface{
{
name: "bond0",
kind: interfaceBond,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"bond-slaves": []string{"eth0"},
},
},
}
interfaces := buildInterfaces(stanzas)
bond0 := &bondInterface{
logicalInterface{
name: "bond0",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
[]string{"eth0"},
map[string]string{},
}
eth0 := &physicalInterface{
logicalInterface{
name: "eth0",
config: configMethodManual{},
children: []networkInterface{bond0},
configDepth: 1,
},
}
expect := []InterfaceGenerator{bond0, eth0}
if !reflect.DeepEqual(interfaces, expect) {
t.FailNow()
}
}
func TestBuildInterfacesBlindVLAN(t *testing.T) {
stanzas := []*stanzaInterface{
{
name: "vlan0",
kind: interfaceVLAN,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"id": []string{"0"},
"raw_device": []string{"eth0"},
},
},
}
interfaces := buildInterfaces(stanzas)
vlan0 := &vlanInterface{
logicalInterface{
name: "vlan0",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
0,
"eth0",
}
eth0 := &physicalInterface{
logicalInterface{
name: "eth0",
config: configMethodManual{},
children: []networkInterface{vlan0},
configDepth: 1,
},
}
expect := []InterfaceGenerator{eth0, vlan0}
if !reflect.DeepEqual(interfaces, expect) {
t.FailNow()
}
}
func TestBuildInterfaces(t *testing.T) {
stanzas := []*stanzaInterface{
&stanzaInterface{
name: "eth0",
kind: interfacePhysical,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{},
},
&stanzaInterface{
name: "bond0",
kind: interfaceBond,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"bond-slaves": []string{"eth0"},
"bond-mode": []string{"4"},
"bond-miimon": []string{"100"},
},
},
&stanzaInterface{
name: "bond1",
kind: interfaceBond,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"bond-slaves": []string{"bond0"},
},
},
&stanzaInterface{
name: "vlan0",
kind: interfaceVLAN,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"id": []string{"0"},
"raw_device": []string{"eth0"},
},
},
&stanzaInterface{
name: "vlan1",
kind: interfaceVLAN,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"id": []string{"1"},
"raw_device": []string{"bond0"},
},
},
}
interfaces := buildInterfaces(stanzas)
vlan1 := &vlanInterface{
logicalInterface{
name: "vlan1",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
1,
"bond0",
}
vlan0 := &vlanInterface{
logicalInterface{
name: "vlan0",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
0,
"eth0",
}
bond1 := &bondInterface{
logicalInterface{
name: "bond1",
config: configMethodManual{},
children: []networkInterface{},
configDepth: 0,
},
[]string{"bond0"},
map[string]string{},
}
bond0 := &bondInterface{
logicalInterface{
name: "bond0",
config: configMethodManual{},
children: []networkInterface{bond1, vlan1},
configDepth: 1,
},
[]string{"eth0"},
map[string]string{
"mode": "4",
"miimon": "100",
},
}
eth0 := &physicalInterface{
logicalInterface{
name: "eth0",
config: configMethodManual{},
children: []networkInterface{bond0, vlan0},
configDepth: 2,
},
}
expect := []InterfaceGenerator{bond0, bond1, eth0, vlan0, vlan1}
if !reflect.DeepEqual(interfaces, expect) {
t.FailNow()
}
}
func TestFilename(t *testing.T) {
for _, tt := range []struct {
i logicalInterface
f string
}{
{logicalInterface{name: "iface", configDepth: 0}, "00-iface"},
{logicalInterface{name: "iface", configDepth: 9}, "09-iface"},
{logicalInterface{name: "iface", configDepth: 10}, "0a-iface"},
{logicalInterface{name: "iface", configDepth: 53}, "35-iface"},
{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)
}
}
}

View File

@@ -1,342 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package network
import (
"fmt"
"net"
"strconv"
"strings"
)
type stanza interface{}
type stanzaAuto struct {
interfaces []string
}
type stanzaInterface struct {
name string
kind interfaceKind
auto bool
configMethod configMethod
options map[string][]string
}
type interfaceKind int
const (
interfaceBond = interfaceKind(iota)
interfacePhysical
interfaceVLAN
)
type route struct {
destination net.IPNet
gateway net.IP
}
type configMethod interface{}
type configMethodStatic struct {
addresses []net.IPNet
nameservers []net.IP
routes []route
hwaddress net.HardwareAddr
}
type configMethodLoopback struct{}
type configMethodManual struct{}
type configMethodDHCP struct {
hwaddress net.HardwareAddr
}
func parseStanzas(lines []string) (stanzas []stanza, err error) {
rawStanzas, err := splitStanzas(lines)
if err != nil {
return nil, err
}
stanzas = make([]stanza, 0, len(rawStanzas))
for _, rawStanza := range rawStanzas {
if stanza, err := parseStanza(rawStanza); err == nil {
stanzas = append(stanzas, stanza)
} else {
return nil, err
}
}
autos := make([]string, 0)
interfaceMap := make(map[string]*stanzaInterface)
for _, stanza := range stanzas {
switch c := stanza.(type) {
case *stanzaAuto:
autos = append(autos, c.interfaces...)
case *stanzaInterface:
interfaceMap[c.name] = c
}
}
// Apply the auto attribute
for _, auto := range autos {
if iface, ok := interfaceMap[auto]; ok {
iface.auto = true
}
}
return stanzas, nil
}
func splitStanzas(lines []string) ([][]string, error) {
var curStanza []string
stanzas := make([][]string, 0)
for _, line := range lines {
if isStanzaStart(line) {
if curStanza != nil {
stanzas = append(stanzas, curStanza)
}
curStanza = []string{line}
} else if curStanza != nil {
curStanza = append(curStanza, line)
} else {
return nil, fmt.Errorf("missing stanza start %q", line)
}
}
if curStanza != nil {
stanzas = append(stanzas, curStanza)
}
return stanzas, nil
}
func isStanzaStart(line string) bool {
switch strings.Split(line, " ")[0] {
case "auto":
fallthrough
case "iface":
fallthrough
case "mapping":
return true
}
if strings.HasPrefix(line, "allow-") {
return true
}
return false
}
func parseStanza(rawStanza []string) (stanza, error) {
if len(rawStanza) == 0 {
panic("empty stanza")
}
tokens := strings.Fields(rawStanza[0])
if len(tokens) < 2 {
return nil, fmt.Errorf("malformed stanza start %q", rawStanza[0])
}
kind := tokens[0]
attributes := tokens[1:]
switch kind {
case "auto":
return parseAutoStanza(attributes, rawStanza[1:])
case "iface":
return parseInterfaceStanza(attributes, rawStanza[1:])
default:
return nil, fmt.Errorf("unknown stanza %q", kind)
}
}
func parseAutoStanza(attributes []string, options []string) (*stanzaAuto, error) {
return &stanzaAuto{interfaces: attributes}, nil
}
func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterface, error) {
if len(attributes) != 3 {
return nil, fmt.Errorf("incorrect number of attributes")
}
iface := attributes[0]
confMethod := attributes[2]
optionMap := make(map[string][]string, 0)
for _, option := range options {
if strings.HasPrefix(option, "post-up") {
tokens := strings.SplitAfterN(option, " ", 2)
if len(tokens) != 2 {
continue
}
if v, ok := optionMap["post-up"]; ok {
optionMap["post-up"] = append(v, tokens[1])
} else {
optionMap["post-up"] = []string{tokens[1]}
}
} else if strings.HasPrefix(option, "pre-down") {
tokens := strings.SplitAfterN(option, " ", 2)
if len(tokens) != 2 {
continue
}
if v, ok := optionMap["pre-down"]; ok {
optionMap["pre-down"] = append(v, tokens[1])
} else {
optionMap["pre-down"] = []string{tokens[1]}
}
} else {
tokens := strings.Fields(option)
optionMap[tokens[0]] = tokens[1:]
}
}
var conf configMethod
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.addresses[0].IP = net.ParseIP(addresses[0])
}
}
if netmasks, ok := optionMap["netmask"]; ok {
if len(netmasks) == 1 {
config.addresses[0].Mask = net.IPMask(net.ParseIP(netmasks[0]).To4())
}
}
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 {
if len(gateways) == 1 {
config.routes = append(config.routes, route{
destination: net.IPNet{
IP: net.IPv4(0, 0, 0, 0),
Mask: net.IPv4Mask(0, 0, 0, 0),
},
gateway: net.ParseIP(gateways[0]),
})
}
}
if hwaddress, err := parseHwaddress(optionMap, iface); err == nil {
config.hwaddress = hwaddress
} else {
return nil, err
}
for _, nameserver := range optionMap["dns-nameservers"] {
config.nameservers = append(config.nameservers, net.ParseIP(nameserver))
}
for _, postup := range optionMap["post-up"] {
if strings.HasPrefix(postup, "route add") {
route := route{}
fields := strings.Fields(postup)
for i, field := range fields[:len(fields)-1] {
switch field {
case "-net":
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":
route.gateway = net.ParseIP(fields[i+1])
}
}
if route.destination.IP != nil && route.destination.Mask != nil && route.gateway != nil {
config.routes = append(config.routes, route)
}
}
}
conf = config
case "loopback":
conf = configMethodLoopback{}
case "manual":
conf = configMethodManual{}
case "dhcp":
config := configMethodDHCP{}
if hwaddress, err := parseHwaddress(optionMap, iface); err == nil {
config.hwaddress = hwaddress
} else {
return nil, err
}
conf = config
default:
return nil, fmt.Errorf("invalid config method %q", confMethod)
}
if _, ok := optionMap["vlan_raw_device"]; ok {
return parseVLANStanza(iface, conf, attributes, optionMap)
}
if strings.Contains(iface, ".") {
return parseVLANStanza(iface, conf, attributes, optionMap)
}
if _, ok := optionMap["bond-slaves"]; ok {
return parseBondStanza(iface, conf, attributes, optionMap)
}
return parsePhysicalStanza(iface, conf, attributes, optionMap)
}
func parseHwaddress(options map[string][]string, iface string) (net.HardwareAddr, error) {
if hwaddress, ok := options["hwaddress"]; ok && len(hwaddress) == 2 {
switch hwaddress[0] {
case "ether":
if address, err := net.ParseMAC(hwaddress[1]); err == nil {
return address, nil
}
return nil, fmt.Errorf("malformed hwaddress option for %q", iface)
}
}
return nil, nil
}
func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
}
func parsePhysicalStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
return &stanzaInterface{name: iface, kind: interfacePhysical, configMethod: conf, options: options}, nil
}
func parseVLANStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
var id string
if strings.Contains(iface, ".") {
tokens := strings.Split(iface, ".")
id = tokens[len(tokens)-1]
} else if strings.HasPrefix(iface, "vlan") {
id = strings.TrimPrefix(iface, "vlan")
} else {
return nil, fmt.Errorf("malformed vlan name %q", iface)
}
if _, err := strconv.Atoi(id); err != nil {
return nil, fmt.Errorf("malformed vlan name %q", iface)
}
options["id"] = []string{id}
options["raw_device"] = options["vlan_raw_device"]
return &stanzaInterface{name: iface, kind: interfaceVLAN, configMethod: conf, options: options}, nil
}

View File

@@ -1,584 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package network
import (
"net"
"reflect"
"strings"
"testing"
)
func TestSplitStanzasNoParent(t *testing.T) {
in := []string{"test"}
e := "missing stanza start"
_, err := splitStanzas(in)
if err == nil || !strings.HasPrefix(err.Error(), e) {
t.Fatalf("bad error for splitStanzas(%q): got %q, want %q", in, err, e)
}
}
func TestBadParseStanzas(t *testing.T) {
for in, e := range map[string]string{
"": "missing stanza start",
"iface": "malformed stanza start",
"allow-?? unknown": "unknown stanza",
} {
_, err := parseStanzas([]string{in})
if err == nil || !strings.HasPrefix(err.Error(), e) {
t.Fatalf("bad error for parseStanzas(%q): got %q, want %q", in, err, e)
}
}
}
func TestBadParseInterfaceStanza(t *testing.T) {
for _, tt := range []struct {
in []string
opts []string
e string
}{
{[]string{}, nil, "incorrect number of attributes"},
{[]string{"eth", "inet", "invalid"}, nil, "invalid config method"},
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"netmask 255.255.255.0"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"address invalid", "netmask 255.255.255.0"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100", "netmask invalid"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100", "netmask 255.255.255.0", "hwaddress ether NotAnAddress"}, "malformed hwaddress option"},
{[]string{"eth", "inet", "dhcp"}, []string{"hwaddress ether NotAnAddress"}, "malformed hwaddress option"},
} {
_, err := parseInterfaceStanza(tt.in, tt.opts)
if err == nil || !strings.HasPrefix(err.Error(), tt.e) {
t.Fatalf("bad error parsing interface stanza %q: got %q, want %q", tt.in, err.Error(), tt.e)
}
}
}
func TestBadParseVLANStanzas(t *testing.T) {
conf := configMethodManual{}
options := map[string][]string{}
for _, in := range []string{"myvlan", "eth.vlan"} {
_, err := parseVLANStanza(in, conf, nil, options)
if err == nil || !strings.HasPrefix(err.Error(), "malformed vlan name") {
t.Fatalf("did not error on bad vlan %q", in)
}
}
}
func TestSplitStanzas(t *testing.T) {
expect := [][]string{
{"auto lo"},
{"iface eth1", "option: 1"},
{"mapping"},
{"allow-"},
}
lines := make([]string, 0, 5)
for _, stanza := range expect {
for _, line := range stanza {
lines = append(lines, line)
}
}
stanzas, err := splitStanzas(lines)
if err != nil {
t.FailNow()
}
for i, stanza := range stanzas {
if len(stanza) != len(expect[i]) {
t.FailNow()
}
for j, line := range stanza {
if line != expect[i][j] {
t.FailNow()
}
}
}
}
func TestParseStanzaNil(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatal("parseStanza(nil) did not panic")
}
}()
parseStanza(nil)
}
func TestParseStanzaSuccess(t *testing.T) {
for _, in := range []string{
"auto a",
"iface a inet manual",
} {
if _, err := parseStanza([]string{in}); err != nil {
t.Fatalf("unexpected error parsing stanza %q: %s", in, err)
}
}
}
func TestParseAutoStanza(t *testing.T) {
interfaces := []string{"test", "attribute"}
stanza, err := parseAutoStanza(interfaces, nil)
if err != nil {
t.Fatalf("unexpected error parsing auto stanza %q: %s", interfaces, err)
}
if !reflect.DeepEqual(stanza.interfaces, interfaces) {
t.FailNow()
}
}
func TestParseBondStanzaNoSlaves(t *testing.T) {
bond, err := parseBondStanza("", nil, nil, map[string][]string{})
if err != nil {
t.FailNow()
}
if bond.options["bond-slaves"] != nil {
t.FailNow()
}
}
func TestParseBondStanza(t *testing.T) {
conf := configMethodManual{}
options := map[string][]string{
"bond-slaves": []string{"1", "2"},
}
bond, err := parseBondStanza("test", conf, nil, options)
if err != nil {
t.FailNow()
}
if bond.name != "test" {
t.FailNow()
}
if bond.kind != interfaceBond {
t.FailNow()
}
if bond.configMethod != conf {
t.FailNow()
}
}
func TestParsePhysicalStanza(t *testing.T) {
conf := configMethodManual{}
options := map[string][]string{
"a": []string{"1", "2"},
"b": []string{"1"},
}
physical, err := parsePhysicalStanza("test", conf, nil, options)
if err != nil {
t.FailNow()
}
if physical.name != "test" {
t.FailNow()
}
if physical.kind != interfacePhysical {
t.FailNow()
}
if physical.configMethod != conf {
t.FailNow()
}
if !reflect.DeepEqual(physical.options, options) {
t.FailNow()
}
}
func TestParseVLANStanzas(t *testing.T) {
conf := configMethodManual{}
options := map[string][]string{}
for _, in := range []string{"vlan25", "eth.25"} {
vlan, err := parseVLANStanza(in, conf, nil, options)
if err != nil {
t.Fatalf("unexpected error from parseVLANStanza(%q): %s", in, err)
}
if !reflect.DeepEqual(vlan.options["id"], []string{"25"}) {
t.FailNow()
}
}
}
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),
},
}
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.addresses, expect) {
t.FailNow()
}
}
func TestParseInterfaceStanzaStaticGateway(t *testing.T) {
options := []string{"address 192.168.1.100", "netmask 255.255.255.0", "gateway 192.168.1.1"}
expect := []route{
{
destination: net.IPNet{
IP: net.IPv4(0, 0, 0, 0),
Mask: net.IPv4Mask(0, 0, 0, 0),
},
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()
}
}
func TestParseInterfaceStanzaStaticDNS(t *testing.T) {
options := []string{"address 192.168.1.100", "netmask 255.255.255.0", "dns-nameservers 192.168.1.10 192.168.1.11 192.168.1.12"}
expect := []net.IP{
net.IPv4(192, 168, 1, 10),
net.IPv4(192, 168, 1, 11),
net.IPv4(192, 168, 1, 12),
}
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.nameservers, expect) {
t.FailNow()
}
}
func TestBadParseInterfaceStanzasStaticPostUp(t *testing.T) {
for _, in := range []string{
"post-up invalid",
"post-up route add",
"post-up route add -net",
"post-up route add gw",
"post-up route add netmask",
"gateway",
"gateway 192.168.1.1 192.168.1.2",
} {
options := []string{"address 192.168.1.100", "netmask 255.255.255.0", in}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
if err != nil {
t.Fatalf("parseInterfaceStanza with options %s got unexpected error", options)
}
static, ok := iface.configMethod.(configMethodStatic)
if !ok {
t.Fatalf("parseInterfaceStanza with options %s did not return configMethodStatic", options)
}
if len(static.routes) != 0 {
t.Fatalf("parseInterfaceStanza with options %s did not return zero-length static routes", options)
}
}
}
func TestParseInterfaceStanzaStaticPostUp(t *testing.T) {
for _, tt := range []struct {
options []string
expect []route
}{
{
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),
},
},
},
{
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)
}
}
}
func TestParseInterfaceStanzaLoopback(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"eth", "inet", "loopback"}, nil)
if err != nil {
t.FailNow()
}
if _, ok := iface.configMethod.(configMethodLoopback); !ok {
t.FailNow()
}
}
func TestParseInterfaceStanzaManual(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, nil)
if err != nil {
t.FailNow()
}
if _, ok := iface.configMethod.(configMethodManual); !ok {
t.FailNow()
}
}
func TestParseInterfaceStanzaDHCP(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"eth", "inet", "dhcp"}, nil)
if err != nil {
t.FailNow()
}
if _, ok := iface.configMethod.(configMethodDHCP); !ok {
t.FailNow()
}
}
func TestParseInterfaceStanzaPostUpOption(t *testing.T) {
options := []string{
"post-up",
"post-up 1 2",
"post-up 3 4",
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
if err != nil {
t.FailNow()
}
if !reflect.DeepEqual(iface.options["post-up"], []string{"1 2", "3 4"}) {
t.Log(iface.options["post-up"])
t.FailNow()
}
}
func TestParseInterfaceStanzaPreDownOption(t *testing.T) {
options := []string{
"pre-down",
"pre-down 3",
"pre-down 4",
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
if err != nil {
t.FailNow()
}
if !reflect.DeepEqual(iface.options["pre-down"], []string{"3", "4"}) {
t.Log(iface.options["pre-down"])
t.FailNow()
}
}
func TestParseInterfaceStanzaEmptyOption(t *testing.T) {
options := []string{
"test",
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
if err != nil {
t.FailNow()
}
if !reflect.DeepEqual(iface.options["test"], []string{}) {
t.FailNow()
}
}
func TestParseInterfaceStanzaOptions(t *testing.T) {
options := []string{
"test1 1",
"test2 2 3",
"test1 5 6",
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
if err != nil {
t.FailNow()
}
if !reflect.DeepEqual(iface.options["test1"], []string{"5", "6"}) {
t.Log(iface.options["test1"])
t.FailNow()
}
if !reflect.DeepEqual(iface.options["test2"], []string{"2", "3"}) {
t.Log(iface.options["test2"])
t.FailNow()
}
}
func TestParseInterfaceStanzaHwaddress(t *testing.T) {
for _, tt := range []struct {
attr []string
opt []string
hw net.HardwareAddr
}{
{
[]string{"mybond", "inet", "dhcp"},
[]string{},
nil,
},
{
[]string{"mybond", "inet", "dhcp"},
[]string{"hwaddress ether 00:01:02:03:04:05"},
net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}),
},
{
[]string{"mybond", "inet", "static"},
[]string{"hwaddress ether 00:01:02:03:04:05", "address 192.168.1.100", "netmask 255.255.255.0"},
net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}),
},
} {
iface, err := parseInterfaceStanza(tt.attr, tt.opt)
if err != nil {
t.Fatalf("error in parseInterfaceStanza (%q, %q): %q", tt.attr, tt.opt, err)
}
switch c := iface.configMethod.(type) {
case configMethodStatic:
if !reflect.DeepEqual(c.hwaddress, tt.hw) {
t.Fatalf("bad hwaddress (%q, %q): got %q, want %q", tt.attr, tt.opt, c.hwaddress, tt.hw)
}
case configMethodDHCP:
if !reflect.DeepEqual(c.hwaddress, tt.hw) {
t.Fatalf("bad hwaddress (%q, %q): got %q, want %q", tt.attr, tt.opt, c.hwaddress, tt.hw)
}
}
}
}
func TestParseInterfaceStanzaBond(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"mybond", "inet", "manual"}, []string{"bond-slaves eth"})
if err != nil {
t.FailNow()
}
if iface.kind != interfaceBond {
t.FailNow()
}
}
func TestParseInterfaceStanzaVLANName(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"eth0.1", "inet", "manual"}, nil)
if err != nil {
t.FailNow()
}
if iface.kind != interfaceVLAN {
t.FailNow()
}
}
func TestParseInterfaceStanzaVLANOption(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"vlan1", "inet", "manual"}, []string{"vlan_raw_device eth"})
if err != nil {
t.FailNow()
}
if iface.kind != interfaceVLAN {
t.FailNow()
}
}
func TestParseStanzasNone(t *testing.T) {
stanzas, err := parseStanzas(nil)
if err != nil {
t.FailNow()
}
if len(stanzas) != 0 {
t.FailNow()
}
}
func TestParseStanzas(t *testing.T) {
lines := []string{
"auto lo",
"iface lo inet loopback",
"iface eth1 inet manual",
"iface eth2 inet manual",
"iface eth3 inet manual",
"auto eth1 eth3",
}
expect := []stanza{
&stanzaAuto{
interfaces: []string{"lo"},
},
&stanzaInterface{
name: "lo",
kind: interfacePhysical,
auto: true,
configMethod: configMethodLoopback{},
options: map[string][]string{},
},
&stanzaInterface{
name: "eth1",
kind: interfacePhysical,
auto: true,
configMethod: configMethodManual{},
options: map[string][]string{},
},
&stanzaInterface{
name: "eth2",
kind: interfacePhysical,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{},
},
&stanzaInterface{
name: "eth3",
kind: interfacePhysical,
auto: true,
configMethod: configMethodManual{},
options: map[string][]string{},
},
&stanzaAuto{
interfaces: []string{"eth1", "eth3"},
},
}
stanzas, err := parseStanzas(lines)
if err != err {
t.FailNow()
}
if !reflect.DeepEqual(stanzas, expect) {
t.FailNow()
}
}

View File

@@ -1,177 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pkg
import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
neturl "net/url"
"strings"
"time"
)
const (
HTTP_2xx = 2
HTTP_4xx = 4
)
type Err error
type ErrTimeout struct {
Err
}
type ErrNotFound struct {
Err
}
type ErrInvalid struct {
Err
}
type ErrServer struct {
Err
}
type ErrNetwork struct {
Err
}
type HttpClient struct {
// Maximum exp backoff duration. Defaults to 5 seconds
MaxBackoff time.Duration
// Maximum number of connection retries. Defaults to 15
MaxRetries int
// HTTP client timeout, this is suggested to be low since exponential
// backoff will kick off too. Defaults to 2 seconds
Timeout time.Duration
// Whether or not to skip TLS verification. Defaults to false
SkipTLS bool
client *http.Client
}
type Getter interface {
Get(string) ([]byte, error)
GetRetry(string) ([]byte, error)
}
func NewHttpClient() *HttpClient {
hc := &HttpClient{
MaxBackoff: time.Second * 5,
MaxRetries: 15,
Timeout: time.Duration(2) * time.Second,
SkipTLS: false,
}
// We need to create our own client in order to add timeout support.
// TODO(c4milo) Replace it once Go 1.3 is officially used by CoreOS
// More info: https://code.google.com/p/go/source/detail?r=ada6f2d5f99f
hc.client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: hc.SkipTLS,
},
Dial: func(network, addr string) (net.Conn, error) {
deadline := time.Now().Add(hc.Timeout)
c, err := net.DialTimeout(network, addr, hc.Timeout)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
},
},
}
return hc
}
func ExpBackoff(interval, max time.Duration) time.Duration {
interval = interval * 2
if interval > max {
interval = max
}
return interval
}
// GetRetry fetches a given URL with support for exponential backoff and maximum retries
func (h *HttpClient) GetRetry(rawurl string) ([]byte, error) {
if rawurl == "" {
return nil, ErrInvalid{errors.New("URL is empty. Skipping.")}
}
url, err := neturl.Parse(rawurl)
if err != nil {
return nil, ErrInvalid{err}
}
// Unfortunately, url.Parse is too generic to throw errors if a URL does not
// have a valid HTTP scheme. So, we have to do this extra validation
if !strings.HasPrefix(url.Scheme, "http") {
return nil, ErrInvalid{fmt.Errorf("URL %s does not have a valid HTTP scheme. Skipping.", rawurl)}
}
dataURL := url.String()
duration := 50 * time.Millisecond
for retry := 1; retry <= h.MaxRetries; retry++ {
log.Printf("Fetching data from %s. Attempt #%d", dataURL, retry)
data, err := h.Get(dataURL)
switch err.(type) {
case ErrNetwork:
log.Printf(err.Error())
case ErrServer:
log.Printf(err.Error())
case ErrNotFound:
return data, err
default:
return data, err
}
duration = ExpBackoff(duration, h.MaxBackoff)
log.Printf("Sleeping for %v...", duration)
time.Sleep(duration)
}
return nil, ErrTimeout{fmt.Errorf("Unable to fetch data. Maximum retries reached: %d", h.MaxRetries)}
}
func (h *HttpClient) Get(dataURL string) ([]byte, error) {
if resp, err := h.client.Get(dataURL); err == nil {
defer resp.Body.Close()
switch resp.StatusCode / 100 {
case HTTP_2xx:
return ioutil.ReadAll(resp.Body)
case HTTP_4xx:
return nil, ErrNotFound{fmt.Errorf("Not found. HTTP status code: %d", resp.StatusCode)}
default:
return nil, ErrServer{fmt.Errorf("Server error. HTTP status code: %d", resp.StatusCode)}
}
} else {
return nil, ErrNetwork{fmt.Errorf("Unable to fetch data: %s", err.Error())}
}
}

View File

@@ -1,156 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pkg
import (
"fmt"
"io"
"math"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestExpBackoff(t *testing.T) {
duration := time.Millisecond
max := time.Hour
for i := 0; i < math.MaxUint16; i++ {
duration = ExpBackoff(duration, max)
if duration < 0 {
t.Fatalf("duration too small: %v %v", duration, i)
}
if duration > max {
t.Fatalf("duration too large: %v %v", duration, i)
}
}
}
// Test exponential backoff and that it continues retrying if a 5xx response is
// received
func TestGetURLExpBackOff(t *testing.T) {
var expBackoffTests = []struct {
count int
body string
}{
{0, "number of attempts: 0"},
{1, "number of attempts: 1"},
{2, "number of attempts: 2"},
}
client := NewHttpClient()
for i, tt := range expBackoffTests {
mux := http.NewServeMux()
count := 0
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if count == tt.count {
io.WriteString(w, fmt.Sprintf("number of attempts: %d", count))
return
}
count++
http.Error(w, "", 500)
})
ts := httptest.NewServer(mux)
defer ts.Close()
data, err := client.GetRetry(ts.URL)
if err != nil {
t.Errorf("Test case %d produced error: %v", i, err)
}
if count != tt.count {
t.Errorf("Test case %d failed: %d != %d", i, count, tt.count)
}
if string(data) != tt.body {
t.Errorf("Test case %d failed: %s != %s", i, tt.body, data)
}
}
}
// Test that it stops retrying if a 4xx response comes back
func TestGetURL4xx(t *testing.T) {
client := NewHttpClient()
retries := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
retries++
http.Error(w, "", 404)
}))
defer ts.Close()
_, err := client.GetRetry(ts.URL)
if err == nil {
t.Errorf("Incorrect result\ngot: %s\nwant: %s", err.Error(), "Not found. HTTP status code: 404")
}
if retries > 1 {
t.Errorf("Number of retries:\n%d\nExpected number of retries:\n%d", retries, 1)
}
}
// Test that it fetches and returns user-data just fine
func TestGetURL2xx(t *testing.T) {
var cloudcfg = `
#cloud-config
coreos:
oem:
id: test
name: CoreOS.box for Test
version-id: %VERSION_ID%+%BUILD_ID%
home-url: https://github.com/coreos/coreos-cloudinit
bug-report-url: https://github.com/coreos/coreos-cloudinit
update:
reboot-strategy: best-effort
`
client := NewHttpClient()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, cloudcfg)
}))
defer ts.Close()
data, err := client.GetRetry(ts.URL)
if err != nil {
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, nil)
}
if string(data) != cloudcfg {
t.Errorf("Incorrect result\ngot: %s\nwant: %s", string(data), cloudcfg)
}
}
// Test attempt to fetching using malformed URL
func TestGetMalformedURL(t *testing.T) {
client := NewHttpClient()
var tests = []struct {
url string
want string
}{
{"boo", "URL boo does not have a valid HTTP scheme. Skipping."},
{"mailto://boo", "URL mailto://boo does not have a valid HTTP scheme. Skipping."},
{"ftp://boo", "URL ftp://boo does not have a valid HTTP scheme. Skipping."},
{"", "URL is empty. Skipping."},
}
for _, test := range tests {
_, err := client.GetRetry(test.url)
if err == nil || err.Error() != test.want {
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, test.want)
}
}
}

View File

@@ -0,0 +1 @@
../../../

View File

@@ -1,42 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
"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
}

View File

@@ -1,116 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
"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
}

View File

@@ -1,442 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
"io/ioutil"
"os"
"path"
"strings"
"syscall"
"testing"
)
const (
base = "# a file\nFOO=base\n\nBAR= hi there\n"
baseNoNewline = "# a file\nFOO=base\n\nBAR= hi there"
baseDos = "# a file\r\nFOO=base\r\n\r\nBAR= hi there\r\n"
expectUpdate = "# a file\nFOO=test\n\nBAR= hi there\nNEW=a value\n"
expectCreate = "FOO=test\nNEW=a value\n"
)
var (
valueUpdate = map[string]string{
"FOO": "test",
"NEW": "a value",
}
valueNoop = map[string]string{
"FOO": "base",
}
valueEmpty = map[string]string{}
valueInvalid = map[string]string{
"FOO-X": "test",
}
)
func TestWriteEnvFileUpdate(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(base), 0644)
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != expectUpdate {
t.Fatalf("File has incorrect contents: %q", contents)
}
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was not replaced: %s", fullPath)
}
}
func TestWriteEnvFileUpdateNoNewline(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(baseNoNewline), 0644)
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != expectUpdate {
t.Fatalf("File has incorrect contents: %q", contents)
}
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was not replaced: %s", fullPath)
}
}
func TestWriteEnvFileCreate(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != expectCreate {
t.Fatalf("File has incorrect contents: %q", contents)
}
}
func TestWriteEnvFileNoop(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(base), 0644)
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueNoop,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != base {
t.Fatalf("File has incorrect contents: %q", contents)
}
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was replaced: %s", fullPath)
}
}
func TestWriteEnvFileUpdateDos(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(baseDos), 0644)
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != expectUpdate {
t.Fatalf("File has incorrect contents: %q", contents)
}
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was not replaced: %s", fullPath)
}
}
// A middle ground noop, values are unchanged but we did have a value.
// Seems reasonable to rewrite the file in Unix format anyway.
func TestWriteEnvFileDos2Unix(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(baseDos), 0644)
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueNoop,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != base {
t.Fatalf("File has incorrect contents: %q", contents)
}
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was not replaced: %s", fullPath)
}
}
// If it really is a noop (structure is empty) don't even do dos2unix
func TestWriteEnvFileEmpty(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(baseDos), 0644)
oldStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueEmpty,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != baseDos {
t.Fatalf("File has incorrect contents: %q", contents)
}
newStat, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino {
t.Fatalf("File was replaced: %s", fullPath)
}
}
// no point in creating empty files
func TestWriteEnvFileEmptyNoCreate(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueEmpty,
}
err = WriteEnvFile(&ef, dir)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
contents, err := ioutil.ReadFile(fullPath)
if err == nil {
t.Fatalf("File has incorrect contents: %q", contents)
} else if !os.IsNotExist(err) {
t.Fatalf("Unexpected error while reading file: %v", err)
}
}
func TestWriteEnvFilePermFailure(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(base), 0000)
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueUpdate,
}
err = WriteEnvFile(&ef, dir)
if !os.IsPermission(err) {
t.Fatalf("Not a pemission denied error: %v", err)
}
}
func TestWriteEnvFileNameFailure(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
name := "foo.conf"
ef := EnvFile{
File: &File{
Path: name,
},
Vars: valueInvalid,
}
err = WriteEnvFile(&ef, dir)
if err == nil || !strings.HasPrefix(err.Error(), "Invalid name") {
t.Fatalf("Not an invalid name error: %v", err)
}
}

View File

@@ -1,19 +1,3 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
@@ -47,55 +31,33 @@ func (f *File) Permissions() (os.FileMode, error) {
return os.FileMode(perm), nil
}
func WriteFile(f *File, root string) (string, error) {
func WriteFile(f *File) error {
if f.Encoding != "" {
return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
return fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
}
fullpath := path.Join(root, f.Path)
dir := path.Dir(fullpath)
if err := EnsureDirectoryExists(dir); err != nil {
return "", err
if err := os.MkdirAll(path.Dir(f.Path), os.FileMode(0755)); err != nil {
return err
}
perm, err := f.Permissions()
if err != nil {
return "", err
return err
}
var tmp *os.File
// Create a temporary file in the same directory to ensure it's on the same filesystem
if tmp, err = ioutil.TempFile(dir, "cloudinit-temp"); err != nil {
return "", err
}
if err := ioutil.WriteFile(tmp.Name(), []byte(f.Content), perm); err != nil {
return "", err
}
if err := tmp.Close(); err != nil {
return "", err
}
// Ensure the permissions are as requested (since WriteFile can be affected by sticky bit)
if err := os.Chmod(tmp.Name(), perm); err != nil {
return "", err
if err := ioutil.WriteFile(f.Path, []byte(f.Content), perm); err != nil {
return err
}
if f.Owner != "" {
// We shell out since we don't have a way to look up unix groups natively
cmd := exec.Command("chown", f.Owner, tmp.Name())
cmd := exec.Command("chown", f.Owner, f.Path)
if err := cmd.Run(); err != nil {
return "", err
return err
}
}
if err := os.Rename(tmp.Name(), fullpath); err != nil {
return "", err
}
return fullpath, nil
return nil
}
func EnsureDirectoryExists(dir string) error {

View File

@@ -1,25 +1,10 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
"io/ioutil"
"os"
"path"
"syscall"
"testing"
)
@@ -28,22 +13,18 @@ func TestWriteFileUnencodedContent(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
defer syscall.Rmdir(dir)
fn := "foo"
fullPath := path.Join(dir, fn)
fullPath := path.Join(dir, "tmp", "foo")
wf := File{
Path: fn,
Content: "bar",
Path: fullPath,
Content: "bar",
RawFilePermissions: "0644",
}
path, err := WriteFile(&wf, dir)
if err != nil {
if err := WriteFile(&wf); err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
} else if path != fullPath {
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
}
fi, err := os.Stat(fullPath)
@@ -70,15 +51,15 @@ func TestWriteFileInvalidPermission(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
defer syscall.Rmdir(dir)
wf := File{
Path: path.Join(dir, "tmp", "foo"),
Content: "bar",
Path: path.Join(dir, "tmp", "foo"),
Content: "bar",
RawFilePermissions: "pants",
}
if _, err := WriteFile(&wf, dir); err == nil {
if err := WriteFile(&wf); err == nil {
t.Fatalf("Expected error to be raised when writing file with invalid permission")
}
}
@@ -88,21 +69,17 @@ func TestWriteFilePermissions(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
defer syscall.Rmdir(dir)
fn := "foo"
fullPath := path.Join(dir, fn)
fullPath := path.Join(dir, "tmp", "foo")
wf := File{
Path: fn,
Path: fullPath,
RawFilePermissions: "0755",
}
path, err := WriteFile(&wf, dir)
if err != nil {
if err := WriteFile(&wf); err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
} else if path != fullPath {
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
}
fi, err := os.Stat(fullPath)
@@ -120,15 +97,15 @@ func TestWriteFileEncodedContent(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
defer syscall.Rmdir(dir)
wf := File{
Path: path.Join(dir, "tmp", "foo"),
Content: "",
Path: path.Join(dir, "tmp", "foo"),
Content: "",
Encoding: "base64",
}
if _, err := WriteFile(&wf, dir); err == nil {
if err := WriteFile(&wf); err == nil {
t.Fatalf("Expected error to be raised when writing file with encoding")
}
}

View File

@@ -1,127 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
"fmt"
"log"
"net"
"os/exec"
"strings"
"github.com/coreos/coreos-cloudinit/network"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/dotcloud/docker/pkg/netlink"
)
const (
runtimeNetworkPath = "/run/systemd/network"
)
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
defer func() {
if e := restartNetworkd(); e != nil {
err = e
}
}()
if err = downNetworkInterfaces(interfaces); err != nil {
return
}
if err = maybeProbe8012q(interfaces); err != nil {
return
}
return maybeProbeBonding(interfaces)
}
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
sysInterfaceMap := make(map[string]*net.Interface)
if systemInterfaces, err := net.Interfaces(); err == nil {
for _, iface := range systemInterfaces {
iface := iface
sysInterfaceMap[iface.Name] = &iface
}
} else {
return err
}
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)
}
}
}
return nil
}
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 {
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 := fmt.Sprintf("%s.netdev", iface.Filename())
if err := writeConfig(filename, iface.Netdev()); err != nil {
return err
}
filename = fmt.Sprintf("%s.link", iface.Filename())
if err := writeConfig(filename, iface.Link()); err != nil {
return err
}
filename = fmt.Sprintf("%s.network", iface.Filename())
if err := writeConfig(filename, iface.Network()); err != nil {
return err
}
}
return nil
}
func writeConfig(filename string, config string) error {
if config == "" {
return nil
}
log.Printf("Writing networkd unit %q\n", filename)
_, err := WriteFile(&File{Content: config, Path: filename}, runtimeNetworkPath)
return err
}

View File

@@ -1,19 +1,3 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (

View File

@@ -1,19 +1,3 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
@@ -26,24 +10,53 @@ import (
"path/filepath"
"strings"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/go-systemd/dbus"
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
)
func NewUnitManager(root string) UnitManager {
return &systemd{root}
}
type systemd struct {
root string
}
// fakeMachineID is placed on non-usr CoreOS images and should
// never be used as a true MachineID
const fakeMachineID = "42000000000000000000000000000042"
type Unit struct {
Name string
Enable bool
Runtime bool
Content string
Command string
}
func (u *Unit) Type() string {
ext := filepath.Ext(u.Name)
return strings.TrimLeft(ext, ".")
}
func (u *Unit) Group() (group string) {
t := u.Type()
if t == "network" || t == "netdev" || t == "link" {
group = "network"
} else {
group = "system"
}
return
}
type Script []byte
// UnitDestination builds the appropriate absolte file path for
// the given unit. The root argument indicates the effective base
// directory of the system (similar to a chroot).
func UnitDestination(u *Unit, root string) string {
dir := "etc"
if u.Runtime {
dir = "run"
}
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 (s *systemd) PlaceUnit(u *Unit, dst string) error {
func 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 {
@@ -52,12 +65,12 @@ func (s *systemd) PlaceUnit(u *Unit, dst string) error {
}
file := File{
Path: filepath.Base(dst),
Path: dst,
Content: u.Content,
RawFilePermissions: "0644",
}
_, err := WriteFile(&file, dir)
err := WriteFile(&file)
if err != nil {
return err
}
@@ -65,18 +78,18 @@ func (s *systemd) PlaceUnit(u *Unit, dst string) error {
return nil
}
func (s *systemd) EnableUnitFile(unit string, runtime bool) error {
func EnableUnitFile(file string, runtime bool) error {
conn, err := dbus.New()
if err != nil {
return err
}
units := []string{unit}
_, _, err = conn.EnableUnitFiles(units, runtime, true)
files := []string{file}
_, _, err = conn.EnableUnitFiles(files, runtime, true)
return err
}
func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
func RunUnitCommand(command, unit string) (string, error) {
conn, err := dbus.New()
if err != nil {
return "", err
@@ -105,7 +118,7 @@ func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
return fn(unit, "replace")
}
func (s *systemd) DaemonReload() error {
func DaemonReload() error {
conn, err := dbus.New()
if err != nil {
return err
@@ -114,57 +127,6 @@ func (s *systemd) 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"),
@@ -203,3 +165,11 @@ func MachineID(root string) string {
return id
}
func MaskUnit(unit string, root string) error {
masked := path.Join(root, "etc", "systemd", "system", unit)
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
return err
}
return os.Symlink("/dev/null", masked)
}

View File

@@ -1,19 +1,3 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
@@ -41,15 +25,13 @@ Address=10.209.171.177/19
}
defer os.RemoveAll(dir)
sd := &systemd{dir}
dst := u.Destination(dir)
dst := UnitDestination(&u, dir)
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
if dst != expectDst {
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
}
if err := sd.PlaceUnit(&u, dst); err != nil {
if err := PlaceUnit(&u, dst); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
@@ -78,30 +60,6 @@ Address=10.209.171.177/19
}
}
func TestUnitDestination(t *testing.T) {
dir := "/some/dir"
name := "foobar.service"
u := Unit{
Name: name,
DropIn: false,
}
dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
if dst != expectDst {
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
}
u.DropIn = true
dst = u.Destination(dir)
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
if dst != expectDst {
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
}
}
func TestPlaceMountUnit(t *testing.T) {
u := Unit{
Name: "media-state.mount",
@@ -118,15 +76,13 @@ Where=/media/state
}
defer os.RemoveAll(dir)
sd := &systemd{dir}
dst := u.Destination(dir)
dst := UnitDestination(&u, dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
if dst != expectDst {
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
}
if err := sd.PlaceUnit(&u, dst); err != nil {
if err := PlaceUnit(&u, dst); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
@@ -167,137 +123,22 @@ func TestMachineID(t *testing.T) {
t.Fatalf("File has incorrect contents")
}
}
func TestMaskUnit(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
sd := &systemd{dir}
// Ensure mask works with units that do not currently exist
uf := &Unit{Name: "foo.service"}
if err := sd.MaskUnit(uf); err != nil {
t.Fatalf("Unable to mask new unit: %v", err)
if err := MaskUnit("foo.service", dir); err != nil {
t.Fatalf("Unable to mask unit: %v", err)
}
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
fooTgt, err := os.Readlink(fooPath)
fullPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
target, err := os.Readlink(fullPath)
if err != nil {
t.Fatal("Unable to read link", err)
t.Fatalf("Unable to read link", err)
}
if fooTgt != "/dev/null" {
t.Fatal("unit not masked, got unit target", fooTgt)
}
// Ensure mask works with unit files that already exist
ub := &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 := sd.MaskUnit(ub); err != nil {
t.Fatalf("Unable to mask existing unit: %v", err)
}
barTgt, err := os.Readlink(barPath)
if err != nil {
t.Fatal("Unable to read link", err)
}
if barTgt != "/dev/null" {
t.Fatal("unit not masked, got unit target", barTgt)
if target != "/dev/null" {
t.Fatalf("unit not masked, got unit target", target)
}
}
func TestUnmaskUnit(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
sd := &systemd{dir}
nilUnit := &Unit{Name: "null.service"}
if err := sd.UnmaskUnit(nilUnit); err != nil {
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
}
uf := &Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}
dst := uf.Destination(dir)
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
t.Fatalf("Unable to create unit directory: %v", err)
}
if _, err := os.Create(dst); err != nil {
t.Fatalf("Unable to write unit file: %v", err)
}
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
t.Fatalf("Unable to write unit file: %v", err)
}
if err := sd.UnmaskUnit(uf); err != nil {
t.Errorf("unmask of non-empty unit returned unexpected error: %v", err)
}
got, _ := ioutil.ReadFile(dst)
if string(got) != uf.Content {
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
}
ub := &Unit{Name: "bar.service"}
dst = ub.Destination(dir)
if err := os.Symlink("/dev/null", dst); err != nil {
t.Fatalf("Unable to create masked unit: %v", err)
}
if err := sd.UnmaskUnit(ub); err != nil {
t.Errorf("unmask of unit returned unexpected error: %v", err)
}
if _, err := os.Stat(dst); !os.IsNotExist(err) {
t.Errorf("expected %s to not exist after unmask, but got err: %s", dst, err)
}
}
func TestNullOrEmpty(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
non := path.Join(dir, "does_not_exist")
ne, err := nullOrEmpty(non)
if !os.IsNotExist(err) {
t.Errorf("nullOrEmpty on nonexistent file returned bad error: %v", err)
}
if ne {
t.Errorf("nullOrEmpty returned true unxpectedly")
}
regEmpty := path.Join(dir, "regular_empty_file")
_, err = os.Create(regEmpty)
if err != nil {
t.Fatalf("Unable to create tempfile: %v", err)
}
gotNe, gotErr := nullOrEmpty(regEmpty)
if !gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of regular empty file returned %t, %v - want true, nil", gotNe, gotErr)
}
reg := path.Join(dir, "regular_file")
if err := ioutil.WriteFile(reg, []byte("asdf"), 700); err != nil {
t.Fatalf("Unable to create tempfile: %v", err)
}
gotNe, gotErr = nullOrEmpty(reg)
if gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of regular file returned %t, %v - want false, nil", gotNe, gotErr)
}
null := path.Join(dir, "null")
if err := os.Symlink(os.DevNull, null); err != nil {
t.Fatalf("Unable to create /dev/null link: %s", err)
}
gotNe, gotErr = nullOrEmpty(null)
if !gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of null symlink returned %t, %v - want true, nil", gotNe, gotErr)
}
}

View File

@@ -1,83 +0,0 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package system
import (
"fmt"
"path"
"path/filepath"
"strings"
)
// Name for drop-in service configuration files created by cloudconfig
const cloudConfigDropIn = "20-cloudinit.conf"
type UnitManager interface {
PlaceUnit(unit *Unit, dst string) error
EnableUnitFile(unit string, runtime bool) error
RunUnitCommand(command, unit string) (string, error)
DaemonReload() error
MaskUnit(unit *Unit) error
UnmaskUnit(unit *Unit) error
}
type Unit struct {
Name string
Mask bool
Enable bool
Runtime bool
Content string
Command string
// For drop-in units, a cloudinit.conf is generated.
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
// until the correct behaviour for multiple drop-in units is determined.
DropIn bool `yaml:"-"`
}
func (u *Unit) Type() string {
ext := filepath.Ext(u.Name)
return strings.TrimLeft(ext, ".")
}
func (u *Unit) Group() (group string) {
t := u.Type()
if t == "network" || t == "netdev" || t == "link" {
group = "network"
} else {
group = "system"
}
return
}
type Script []byte
// Destination builds the appropriate absolute file path for
// the Unit. The root argument indicates the effective base
// directory of the system (similar to a chroot).
func (u *Unit) Destination(root string) string {
dir := "etc"
if u.Runtime {
dir = "run"
}
if u.DropIn {
return path.Join(root, dir, "systemd", u.Group(), fmt.Sprintf("%s.d", u.Name), cloudConfigDropIn)
} else {
return path.Join(root, dir, "systemd", u.Group(), u.Name)
}
}

Some files were not shown because too many files have changed in this diff Show More