Compare commits

..

100 Commits

Author SHA1 Message Date
Michael Marineau
b1a897d75c coreos-cloudinit: bump to 0.8.8 2014-07-11 11:12:15 -07:00
Michael Marineau
a55e2cd49b Merge pull request #178 from marineam/env
Write /etc/environment
2014-07-11 10:39:33 -07:00
Michael Marineau
983501e43b environment: add support for updating /etc/environment with IP values
To maintain the behavior of the coreos-setup-environment that has
started to move into cloudinit we need to write out /etc/environment
with the public and private addresses, if known. The file is updated so
that other contents are not replaced. This behavior is disabled entirely
if /etc/environment was written by a write_files entry.
2014-07-11 10:34:44 -07:00
Michael Marineau
81824be3bf system: new file writer for updating env-style files
This can be used to safely update config files cloudinit does not have
exclusive control over. For example update.conf or /etc/environment.
2014-07-10 15:53:32 -07:00
Michael Marineau
98c26440be Merge pull request #176 from jayofdoom/master
Document need for #cloud-config in cloud-config.yml
2014-07-09 16:41:00 -07:00
Jay Faulkner
3b5fcc393b Document need for #cloud-config in cloud-config.yml
- cloud-config.yml does not work if it's missing the #cloud-config
  directive at the top. This is undocumented, except in the examples.
2014-07-09 16:36:11 -07:00
Alex Crawford
9528077340 coreos-cloudinit: bump to 0.8.7+git 2014-07-02 15:20:45 -07:00
Alex Crawford
4355a05d55 coreos-cloudinit: bump to 0.8.7 2014-07-02 15:20:26 -07:00
Alex Crawford
52c44923dd Merge pull request #173 from crawford/metadata
metadata-service: remove check for OpenStack meta_data.json
2014-07-02 15:19:37 -07:00
Alex Crawford
47748ef4b6 metadata-service: remove check for OpenStack meta_data.json
The meta_data.json blob under OpenStack doesn't actually contain all
of the metadata... Fall back to explicitly requesting each attribute.
2014-07-02 14:38:23 -07:00
Alex Crawford
8eca10200e coreos-cloudinit: bump to 0.8.6+git 2014-07-01 16:17:00 -07:00
Alex Crawford
43be8c8996 coreos-cloudinit: bump to 0.8.6 2014-07-01 16:16:41 -07:00
Alex Crawford
19b4b1160e Merge pull request #171 from crawford/err
metadata-service: Handle no user-data
2014-07-01 16:15:32 -07:00
Alex Crawford
ce6fccfb3c metadata-service: Handle no user-data 2014-07-01 16:10:18 -07:00
Alex Crawford
7d89aefb82 coreos-cloudinit: bump to 0.8.5+git 2014-07-01 15:45:49 -07:00
Alex Crawford
2369e2a920 coreos-cloudinit: bump to 0.8.5 2014-07-01 15:45:23 -07:00
Alex Crawford
6d808048d3 Merge pull request #170 from crawford/metadata
metadata: Fetch the public and private IP addresses
2014-07-01 15:44:14 -07:00
Alex Crawford
276f0b5d99 metadata: Fetch the public and private IP addresses 2014-07-01 14:43:19 -07:00
Jonathan Boulle
92bd5ca5d4 coreos-cloudinit: bump to 0.8.4+git 2014-07-01 12:16:09 -07:00
Jonathan Boulle
5b5ffea126 coreos-cloudinit: bump to 0.8.4 2014-07-01 12:15:48 -07:00
Jonathan Boulle
18068e9375 Merge pull request #169 from jonboulle/pebkac
coreos-cloudinit: apply environment to userdata string
2014-07-01 12:15:06 -07:00
Jonathan Boulle
1b3cabb035 coreos-cloudinit: apply environment to userdata string 2014-07-01 12:08:42 -07:00
Jonathan Boulle
1be2bec1c2 coreos-cloudinit: bump to 0.8.3+git 2014-06-30 22:12:13 -07:00
Jonathan Boulle
f3bd5f543e coreos-cloudinit: bump to 0.8.3 2014-06-30 22:11:15 -07:00
Jonathan Boulle
660feb59b9 Merge pull request #168 from jonboulle/foo
fix ordering error in mergeCloudConfig
2014-06-30 22:08:47 -07:00
Jonathan Boulle
9673dbe12b coreos-cloudinit: fix ordering error in merge invocation 2014-06-30 22:07:05 -07:00
Alex Crawford
2be435dd83 coreos-cloudinit: bump to 0.8.2+git 2014-06-30 18:11:14 -07:00
Alex Crawford
2d91369596 coreos-cloudinit: bump to 0.8.2 2014-06-30 18:10:20 -07:00
Alex Crawford
d8d3928978 Merge pull request #167 from crawford/sshkeys
metadata-service: fix ssh key retrieval and application
2014-06-30 18:08:04 -07:00
Alex Crawford
7fcc540154 metadata-service: fix ssh key retrieval and application
The metadata service wasn't properly fetching the ssh keys from metadata.
Drop the key traversal in favor of explict key urls.
2014-06-30 17:45:08 -07:00
Jonathan Boulle
cb7fbd4668 Merge pull request #166 from jonboulle/merge
cloudinit: merge cloudconfig info from user-data and meta-data
2014-06-30 11:27:47 -07:00
Jonathan Boulle
d4e048a1f4 ParseUserData: return nil on empty input string 2014-06-30 11:27:33 -07:00
Jonathan Boulle
231c0fa20b initialize: add tests for ParseMetadata 2014-06-27 23:53:06 -07:00
Jonathan Boulle
1aabacc769 cloudinit: merge cloudconfig info from user-data and meta-data
This attempts to retrieve cloudconfigs from two sources: the meta-data
service, and the user-data service. If only one cloudconfig is found,
that is applied to the system. If both services return a cloudconfig,
the two are merged into a single cloudconfig which is then applied to
the system.

Only a subset of parameters are merged (because the meta-data service
currently only partially populates a cloudconfig). In the event of any
conflicts, parameters in the user-data cloudconfig take precedence over
those in the meta-data cloudconfig.
2014-06-27 23:48:48 -07:00
Alex Crawford
6a2927d701 coreos-cloudinit: bump to 0.8.1+git 2014-06-27 15:00:05 -07:00
Alex Crawford
126188510b coreos-cloudinit: bump to 0.8.1 2014-06-27 14:59:56 -07:00
Alex Crawford
4627ccb444 Merge pull request #165 from crawford/units
units: update dependencies
2014-06-27 14:55:48 -07:00
Alex Crawford
aff372111a units: update dependencies 2014-06-27 14:29:59 -07:00
Alex Crawford
c7081b9918 coreos-cloudinit: bump to 0.8.0+git 2014-06-27 11:33:56 -07:00
Alex Crawford
9ba3b18b59 coreos-cloudinit: bump to 0.8.0 2014-06-27 11:32:52 -07:00
Alex Crawford
099de62e9a Merge pull request #164 from crawford/datasources
datasources: Add support for specifying multiple datasources
2014-06-27 00:25:09 -07:00
Alex Crawford
c089216cb5 datasources: Add support for specifying multiple datasources
If multiple sources are specified, the first available source is used.
2014-06-26 22:32:39 -07:00
Alex Crawford
68dc902ed1 HttpClient: Refactor timeout into two seperate functions 2014-06-26 15:16:22 -07:00
Jonathan Boulle
ad66b1c92f Merge pull request #163 from jonboulle/net
coreos-cloudinit: restrict convert-netconf to configdrive
2014-06-25 14:35:27 -07:00
Jonathan Boulle
fbdece2762 coreos-cloudinit: restrict convert-netconf to configdrive 2014-06-25 14:28:11 -07:00
Jonathan Boulle
f85eafb7ca Merge pull request #162 from jonboulle/fffffffffffffffffff
initialize/env: handle nil substitution maps properly
2014-06-25 12:10:00 -07:00
Jonathan Boulle
f0dba2294e initialize/env: handle nil substitution maps properly 2014-06-25 12:07:48 -07:00
Alex Crawford
bda3948382 Merge pull request #159 from crawford/metadata
metadataService: Check both ec2 and openstack urls more explicitly
2014-06-25 11:26:19 -07:00
Alex Crawford
fae81c78f3 metadataService: Check both ec2 and openstack urls more explicitly
Remove the root url parameter for -from-metadata-service since this
is a guarenteed value. Additionally, check for both ec2 and openstack
urls for the metadata and userdata. Fix a bug with the -from-url
option and a panic on an empty response.
2014-06-25 11:19:11 -07:00
Alex Crawford
a5dec7d7bd cloudconfig: Process metadata before userdata
This gives the options in userdata a higher precedence over metadata.
2014-06-25 10:35:26 -07:00
Jonathan Boulle
e1222c9885 Merge pull request #161 from jonboulle/doc
metadata: add links to metadata source information
2014-06-25 08:55:26 -07:00
Jonathan Boulle
ded3bcf122 metadata: add links to metadata source information 2014-06-24 19:26:07 -07:00
Jonathan Boulle
80d00cde94 Merge pull request #158 from jonboulle/nettttt
cloudinit: retrieve IPv4 addresses from metadata
2014-06-24 19:11:15 -07:00
Jonathan Boulle
2805d70ece initialize/env: add notes about tests 2014-06-24 18:52:08 -07:00
Jonathan Boulle
439b7e8b98 initialize/env: fall back to COREOS_*_IPV4 env variables 2014-06-24 18:49:49 -07:00
Jonathan Boulle
ba1c1e97d0 cloudinit: retrieve IPv4 addresses from metadata
This uses the new MetadataService implementation to retrieve values for
$private_ipv4 and $public_ipv4 substitutions, instead of using
environment variables.
2014-06-24 17:46:06 -07:00
Alex Crawford
8a50fd8595 Merge pull request #154 from crawford/metadata
metadata-service: Add new datasource to download from metadata service
2014-06-24 15:18:27 -07:00
Alex Crawford
465bcce72c metadata_service: Add tests for constructing metadata 2014-06-24 15:08:03 -07:00
Alex Crawford
361edeebc6 metadata-service: Add metadata-service datasource
Move the old metadata-service datasource to url datasource. This new datasource
checks for the existance of meta-data.json and if it doesn't exist, walks the
meta-data directory to build a metadata blob.
2014-06-24 15:08:03 -07:00
Jonathan Boulle
29a7b0e34f Merge pull request #155 from jonboulle/etcd
etcdenvironment: order map keys consistently
2014-06-24 15:02:04 -07:00
Alex Crawford
8496ffb53a HttpClient: Wrap errors with error classes 2014-06-24 15:01:31 -07:00
Jonathan Boulle
2c717a6cd1 Merge pull request #157 from jonboulle/core
coreos-cloudinit: clean up flag handling
2014-06-24 14:57:18 -07:00
Jonathan Boulle
13a91c9181 coreos-cloudinit: clean up flag handling 2014-06-24 14:19:55 -07:00
Jonathan Boulle
338e1b64ab etcdenvironment: order map keys consistently 2014-06-23 15:13:11 -07:00
Alex Crawford
8eb0636034 Merge pull request #149 from crawford/network
feat(network): Add support for blind interfaces, support for hwaddress, and bug fixes
2014-06-20 17:53:37 -07:00
Alex Crawford
f7c25a1b83 doc(debian-interfaces): Add basic docs for convert-netconf 2014-06-20 17:51:57 -07:00
Alex Crawford
d6a0d0908c fix(network): Generate prefixes to ensure proper lexicographical ordering
In order for networkd to properly configure the network interfaces, the configs must be
prefixed to ensure that they load in the correct order (parent interfaces have a lower
prefix than their children).
2014-06-20 17:51:57 -07:00
Alex Crawford
5c89afc18a Merge pull request #152 from crawford/metadata
feat(meta_data): Add partial support for meta_data.json
2014-06-20 17:47:08 -07:00
Michael Marineau
376cc4bcac chore(coreos-cloudinit): bump to 0.7.7+git 2014-06-18 15:01:13 -07:00
Michael Marineau
d0a6d6f92f chore(coreos-cloudinit): bump to 0.7.7 2014-06-18 14:55:38 -07:00
Michael Marineau
2be1e52f32 Merge pull request #151 from marineam/mount
fix(configdrive): Use mount units, give virtfs a new mount point.
2014-06-18 13:51:11 -07:00
Michael Marineau
784a71e2bf fix(configdrive): Use mount units, give virtfs a new mount point.
Currently systemd cannot track dependencies on configdrive very well
because it is mounted via a service instead of a mount unit. Also since
the interaction between path and mount units can lead to unexpected
behavior if something goes wrong the cloudinit service is now triggered
explicitly by the mount again. The configdrive path unit remains only as
a fall back for containers where the mount unit doesn't kick in. Better
to have two mechanisms that trigger the cloudinit service than none. :)

Since mounting a virtfs based configdrive requires different mount
options and two different mount units cannot refer to the same path the
virtfs version now mounts to /media/configvirtfs.

There are also two new kernel options:
- `coreos.configdrive=1`: enable config drive on physical hardware.
- `coreos.configdrive=0`: disable config drive on virtual machines.
2014-06-18 13:01:19 -07:00
Alex Crawford
e6cf83a2e5 refactor(netconf): Move netconf processing and handle metadata 2014-06-18 12:43:41 -07:00
Alex Crawford
840c208b60 feat(metadata): Distinguish between userdata and metadata for datasources 2014-06-18 12:34:31 -07:00
Alex Crawford
29ed6b38bd refactor(env): Add the config root and netconf type to datasource and env 2014-06-18 12:27:15 -07:00
Alex Crawford
259c7e1fe2 fix(sshKeyName): Use the SSH key name provided 2014-06-18 11:47:17 -07:00
Alex Crawford
033c8d352f feat(network): Add support for hwaddress
Currently only supports the ether mode of hwaddress. No immediate plans
to support ax25, ARCnet, or netrom.
2014-06-14 21:30:14 -07:00
Alex Crawford
16d7e8af48 fix(network): Take down all interfaces properly
The map of interfaces wasn't being populated correctly. Also, clean up some prints.
2014-06-13 20:53:59 -07:00
Alex Crawford
159f4a2c7c feat(network): Add support for blind interfaces
It is valid for an interface to reference another, otherwise undeclared,
interface (i.e. a bond enslaves eth0 without eth0 having its own iface stanza).
In order to associate the two interfaces, the undeclared interface needs to be
implicitly created so that it can be referenced by the other. This adds the
capability to forward-declare interfaces in addition to cleaning up the
process a little bit.
2014-06-11 22:07:33 -07:00
Michael Marineau
160668284c chore(coreos-cloudinit): bump to 0.7.6+git 2014-06-07 16:04:33 -04:00
Michael Marineau
41b9dfcb1c chore(coreos-cloudinit): bump to 0.7.6 2014-06-07 16:01:31 -04:00
Michael Marineau
ef4c3483b6 Merge pull request #146 from marineam/fix
fix(update): Fix restart of update-engine
2014-06-07 13:04:49 -07:00
Michael Marineau
4bdf633075 fix(update): Fix restart of update-engine
The name was missing .service.
2014-06-07 12:08:22 -07:00
Brian Waldon
c9fc718e18 Merge pull request #145 from bcwaldon/drop-group-req
Relax requirements of update group value
2014-06-06 11:43:22 -07:00
Brian Waldon
4461b3d33d fix(update): Relax requirements of update group value 2014-06-06 11:29:09 -07:00
Jonathan Boulle
c6a1412f6b chore(coreos-cloudinit): bump to 0.7.5+git 2014-06-06 11:14:39 -07:00
Jonathan Boulle
d0cbbd2007 chore(coreos-cloudinit): bump to 0.7.5 2014-06-06 11:10:48 -07:00
Jonathan Boulle
7b5e542eb4 Merge pull request #132 from jonboulle/locksmith
reboot-strategy=off breaks subsequent reboot strategies
2014-06-06 11:08:06 -07:00
Jonathan Boulle
376d82ba63 doc(*): add note about runtime locksmithd unit file 2014-06-06 10:55:42 -07:00
Jonathan Boulle
a6aa9f82b8 fix(systemd): unmask runtime units when mask=False 2014-06-06 10:55:42 -07:00
Jonathan Boulle
00ee047753 fix(locksmith): use a runtime unit for locksmith 2014-06-06 10:55:42 -07:00
Jonathan Boulle
f127406d01 Merge pull request #140 from jonboulle/atomic
fix(system): write all files atomically
2014-06-06 10:37:09 -07:00
Jonathan Boulle
0ddc08d55a fix(system): write all files atomically 2014-06-06 10:36:36 -07:00
Jonathan Boulle
56f455f890 Merge pull request #141 from jonboulle/141
cloudinit doesn't restart update-engine.service
2014-06-06 10:25:24 -07:00
Jonathan Boulle
dd861b9f88 fix(initialize): ensure update-engine is restarted after group/server
changes
2014-06-05 16:12:40 -07:00
Alex Crawford
f7d01da267 Merge pull request #138 from spkane/github-ent-key-docs
Add a valid URL example for Github Enterprise token based API auth
2014-06-04 16:15:04 -07:00
Sean P. Kane
fc8f30bf08 Add a valid URL example for Github Enterprise token based API auth 2014-06-04 16:03:02 -07:00
Brandon Philips
075c0557e7 Merge pull request #137 from robszumski/patch-1
fix(docs): remove unneeded install section
2014-06-04 14:22:55 -07:00
Rob Szumski
d25e13a2c6 fix(docs): remove unneeded install section 2014-06-04 13:57:18 -07:00
Alex Crawford
cf1ffad533 chore(coreos-cloudinit): bump to 0.7.4+git 2014-06-03 14:14:47 -07:00
51 changed files with 2443 additions and 556 deletions

View File

@@ -16,7 +16,7 @@ We've designed our implementation to allow the same cloud-config file to work ac
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values. The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
A cloud-config file should contain an associative array which has zero or more of the following keys: A cloud-config file should contain `#cloud-config`, followed by an associative array which has zero or more of the following keys:
- `coreos` - `coreos`
- `ssh_authorized_keys` - `ssh_authorized_keys`
@@ -100,6 +100,7 @@ For more information on fleet configuration, see the [fleet documentation][fleet
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated. The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field. These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field.
The `reboot-strategy` parameter also affects the behaviour of [locksmith](https://github.com/coreos/locksmith).
- **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed. - **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed.
- _reboot_: Reboot immediately after an update is applied. - _reboot_: Reboot immediately after an update is applied.
@@ -109,6 +110,10 @@ These fields will be written out to and replace `/etc/coreos/update.conf`. If on
- **server**: is the omaha endpoint URL which will be queried for updates. - **server**: is the omaha endpoint URL which will be queried for updates.
- **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable") - **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable")
*Note: cloudinit will only manipulate the locksmith unit file in the systemd runtime directory (`/run/systemd/system/locksmithd.service`). If any manual modifications are made to an overriding unit configuration file (e.g. `/etc/systemd/system/locksmithd.service`), cloudinit will no longer be able to control the locksmith service unit.*
##### Example
``` ```
#cloud-config #cloud-config
coreos: coreos:
@@ -150,9 +155,6 @@ coreos:
Restart=always Restart=always
ExecStart=/usr/bin/docker start -a redis_server ExecStart=/usr/bin/docker start -a redis_server
ExecStop=/usr/bin/docker stop -t 2 redis_server ExecStop=/usr/bin/docker stop -t 2 redis_server
[Install]
WantedBy=local.target
``` ```
Start the built-in `etcd` and `fleet` services: Start the built-in `etcd` and `fleet` services:
@@ -277,7 +279,7 @@ For example, if you have an installation of GitHub Enterprise, you can provide a
users: users:
- name: elroy - name: elroy
coreos-ssh-import-url: https://token:<OAUTH-TOKEN>@github-enterprise.example.com/users/elroy/keys coreos-ssh-import-url: https://github-enterprise.example.com/api/v3/users/elroy/keys?access_token=<TOKEN>
``` ```
You can also specify any URL whose response matches the JSON format for public keys: You can also specify any URL whose response matches the JSON format for public keys:

View File

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

View File

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

110
coreos-cloudinit_test.go Normal file
View File

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

View File

@@ -7,21 +7,42 @@ import (
) )
type configDrive struct { type configDrive struct {
path string root string
} }
func NewConfigDrive(path string) *configDrive { func NewConfigDrive(root string) *configDrive {
return &configDrive{path} return &configDrive{path.Join(root, "openstack")}
} }
func (self *configDrive) Fetch() ([]byte, error) { func (cd *configDrive) IsAvailable() bool {
data, err := ioutil.ReadFile(path.Join(self.path, "openstack", "latest", "user_data")) _, err := os.Stat(cd.root)
return !os.IsNotExist(err)
}
func (cd *configDrive) AvailabilityChanges() bool {
return true
}
func (cd *configDrive) ConfigRoot() string {
return cd.root
}
func (cd *configDrive) FetchMetadata() ([]byte, error) {
return cd.readFile("meta_data.json")
}
func (cd *configDrive) FetchUserdata() ([]byte, error) {
return cd.readFile("user_data")
}
func (cd *configDrive) Type() string {
return "cloud-drive"
}
func (cd *configDrive) readFile(filename string) ([]byte, error) {
data, err := ioutil.ReadFile(path.Join(cd.root, "latest", filename))
if os.IsNotExist(err) { if os.IsNotExist(err) {
err = nil err = nil
} }
return data, err return data, err
} }
func (self *configDrive) Type() string {
return "cloud-drive"
}

View File

@@ -1,6 +1,10 @@
package datasource package datasource
type Datasource interface { type Datasource interface {
Fetch() ([]byte, error) IsAvailable() bool
AvailabilityChanges() bool
ConfigRoot() string
FetchMetadata() ([]byte, error)
FetchUserdata() ([]byte, error)
Type() string Type() string
} }

View File

@@ -2,6 +2,7 @@ package datasource
import ( import (
"io/ioutil" "io/ioutil"
"os"
) )
type localFile struct { type localFile struct {
@@ -12,10 +13,27 @@ func NewLocalFile(path string) *localFile {
return &localFile{path} return &localFile{path}
} }
func (self *localFile) Fetch() ([]byte, error) { func (f *localFile) IsAvailable() bool {
return ioutil.ReadFile(self.path) _, err := os.Stat(f.path)
return !os.IsNotExist(err)
} }
func (self *localFile) Type() string { func (f *localFile) AvailabilityChanges() bool {
return true
}
func (f *localFile) ConfigRoot() string {
return ""
}
func (f *localFile) FetchMetadata() ([]byte, error) {
return []byte{}, nil
}
func (f *localFile) FetchUserdata() ([]byte, error) {
return ioutil.ReadFile(f.path)
}
func (f *localFile) Type() string {
return "local-file" return "local-file"
} }

View File

@@ -1,20 +1,155 @@
package datasource package datasource
import "github.com/coreos/coreos-cloudinit/pkg" import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"strings"
type metadataService struct { "github.com/coreos/coreos-cloudinit/pkg"
url string )
// metadataService retrieves metadata from either an OpenStack[1] (2012-08-10)
// or EC2[2] (2009-04-04) compatible endpoint. It will first attempt to
// directly retrieve a JSON blob from the OpenStack endpoint. If that fails
// with a 404, it then attempts to retrieve metadata bit-by-bit from the EC2
// endpoint, and populates that into an equivalent JSON blob. metadataService
// also checks for userdata from EC2 and, if that fails with a 404, OpenStack.
//
// [1] http://docs.openstack.org/grizzly/openstack-compute/admin/content/metadata-service.html
// [2] http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html#instancedata-data-categories
const (
BaseUrl = "http://169.254.169.254/"
Ec2ApiVersion = "2009-04-04"
Ec2UserdataUrl = BaseUrl + Ec2ApiVersion + "/user-data"
Ec2MetadataUrl = BaseUrl + Ec2ApiVersion + "/meta-data"
OpenstackApiVersion = "openstack/2012-08-10"
OpenstackUserdataUrl = BaseUrl + OpenstackApiVersion + "/user_data"
)
type metadataService struct{}
type getter interface {
GetRetry(string) ([]byte, error)
} }
func NewMetadataService(url string) *metadataService { func NewMetadataService() *metadataService {
return &metadataService{url} return &metadataService{}
} }
func (ms *metadataService) Fetch() ([]byte, error) { func (ms *metadataService) IsAvailable() bool {
client := pkg.NewHttpClient() client := pkg.NewHttpClient()
return client.Get(ms.url) _, err := client.Get(BaseUrl)
return (err == nil)
}
func (ms *metadataService) AvailabilityChanges() bool {
return true
}
func (ms *metadataService) ConfigRoot() string {
return ""
}
func (ms *metadataService) FetchMetadata() ([]byte, error) {
return fetchMetadata(pkg.NewHttpClient())
}
func (ms *metadataService) FetchUserdata() ([]byte, error) {
client := pkg.NewHttpClient()
if data, err := client.GetRetry(Ec2UserdataUrl); err == nil {
return data, err
} else if _, ok := err.(pkg.ErrTimeout); ok {
return data, err
}
if data, err := client.GetRetry(OpenstackUserdataUrl); err == nil {
return data, err
} else if _, ok := err.(pkg.ErrNotFound); ok {
return []byte{}, nil
} else {
return data, err
}
} }
func (ms *metadataService) Type() string { func (ms *metadataService) Type() string {
return "metadata-service" return "metadata-service"
} }
func fetchMetadata(client getter) ([]byte, error) {
attrs := make(map[string]interface{})
if keynames, err := fetchAttributes(client, fmt.Sprintf("%s/public-keys", Ec2MetadataUrl)); err == nil {
keyIDs := make(map[string]string)
for _, keyname := range keynames {
tokens := strings.SplitN(keyname, "=", 2)
if len(tokens) != 2 {
return nil, fmt.Errorf("malformed public key: %q\n", keyname)
}
keyIDs[tokens[1]] = tokens[0]
}
keys := make(map[string]string)
for name, id := range keyIDs {
sshkey, err := fetchAttribute(client, fmt.Sprintf("%s/public-keys/%s/openssh-key", Ec2MetadataUrl, id))
if err != nil {
return nil, err
}
keys[name] = sshkey
fmt.Printf("Found SSH key for %q\n", name)
}
attrs["public_keys"] = keys
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
if hostname, err := fetchAttribute(client, fmt.Sprintf("%s/hostname", Ec2MetadataUrl)); err == nil {
attrs["hostname"] = hostname
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
if localAddr, err := fetchAttribute(client, fmt.Sprintf("%s/local-ipv4", Ec2MetadataUrl)); err == nil {
attrs["local-ipv4"] = localAddr
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
if publicAddr, err := fetchAttribute(client, fmt.Sprintf("%s/public-ipv4", Ec2MetadataUrl)); err == nil {
attrs["public-ipv4"] = publicAddr
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
if content_path, err := fetchAttribute(client, fmt.Sprintf("%s/network_config/content_path", Ec2MetadataUrl)); err == nil {
attrs["network_config"] = map[string]string{
"content_path": content_path,
}
} else if _, ok := err.(pkg.ErrNotFound); !ok {
return nil, err
}
return json.Marshal(attrs)
}
func fetchAttributes(client getter, url string) ([]string, error) {
resp, err := client.GetRetry(url)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(bytes.NewBuffer(resp))
data := make([]string, 0)
for scanner.Scan() {
data = append(data, scanner.Text())
}
return data, scanner.Err()
}
func fetchAttribute(client getter, url string) (string, error) {
if attrs, err := fetchAttributes(client, url); err == nil && len(attrs) > 0 {
return attrs[0], nil
} else {
return "", err
}
}

View File

@@ -0,0 +1,159 @@
package datasource
import (
"bytes"
"fmt"
"reflect"
"testing"
"github.com/coreos/coreos-cloudinit/pkg"
)
type TestHttpClient struct {
metadata map[string]string
err error
}
func (t *TestHttpClient) GetRetry(url string) ([]byte, error) {
if t.err != nil {
return nil, t.err
}
if val, ok := t.metadata[url]; ok {
return []byte(val), nil
} else {
return nil, pkg.ErrNotFound{fmt.Errorf("not found: %q", url)}
}
}
func TestFetchAttributes(t *testing.T) {
for _, s := range []struct {
metadata map[string]string
err error
tests []struct {
path string
val []string
}
}{
{
metadata: map[string]string{
"/": "a\nb\nc/",
"/c/": "d\ne/",
"/c/e/": "f",
"/a": "1",
"/b": "2",
"/c/d": "3",
"/c/e/f": "4",
},
tests: []struct {
path string
val []string
}{
{"/", []string{"a", "b", "c/"}},
{"/b", []string{"2"}},
{"/c/d", []string{"3"}},
{"/c/e/", []string{"f"}},
},
},
{
err: pkg.ErrNotFound{fmt.Errorf("test error")},
tests: []struct {
path string
val []string
}{
{"", nil},
},
},
} {
client := &TestHttpClient{s.metadata, s.err}
for _, tt := range s.tests {
attrs, err := fetchAttributes(client, tt.path)
if err != s.err {
t.Fatalf("bad error for %q (%q): want %q, got %q", tt.path, s.metadata, s.err, err)
}
if !reflect.DeepEqual(attrs, tt.val) {
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.metadata, tt.val, attrs)
}
}
}
}
func TestFetchAttribute(t *testing.T) {
for _, s := range []struct {
metadata map[string]string
err error
tests []struct {
path string
val string
}
}{
{
metadata: map[string]string{
"/": "a\nb\nc/",
"/c/": "d\ne/",
"/c/e/": "f",
"/a": "1",
"/b": "2",
"/c/d": "3",
"/c/e/f": "4",
},
tests: []struct {
path string
val string
}{
{"/a", "1"},
{"/b", "2"},
{"/c/d", "3"},
{"/c/e/f", "4"},
},
},
{
err: pkg.ErrNotFound{fmt.Errorf("test error")},
tests: []struct {
path string
val string
}{
{"", ""},
},
},
} {
client := &TestHttpClient{s.metadata, s.err}
for _, tt := range s.tests {
attr, err := fetchAttribute(client, tt.path)
if err != s.err {
t.Fatalf("bad error for %q (%q): want %q, got %q", tt.path, s.metadata, s.err, err)
}
if attr != tt.val {
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.metadata, tt.val, attr)
}
}
}
}
func TestFetchMetadata(t *testing.T) {
for _, tt := range []struct {
metadata map[string]string
err error
expect []byte
}{
{
metadata: map[string]string{
"http://169.254.169.254/2009-04-04/meta-data/hostname": "host",
"http://169.254.169.254/2009-04-04/meta-data/public-keys": "0=test1\n",
"http://169.254.169.254/2009-04-04/meta-data/public-keys/0": "openssh-key",
"http://169.254.169.254/2009-04-04/meta-data/public-keys/0/openssh-key": "key",
"http://169.254.169.254/2009-04-04/meta-data/network_config/content_path": "path",
},
expect: []byte(`{"hostname":"host","network_config":{"content_path":"path"},"public_keys":{"test1":"key"}}`),
},
{err: pkg.ErrTimeout{fmt.Errorf("test error")}},
} {
client := &TestHttpClient{tt.metadata, tt.err}
metadata, err := fetchMetadata(client)
if err != tt.err {
t.Fatalf("bad error (%q): want %q, got %q", tt.metadata, tt.err, err)
}
if !bytes.Equal(metadata, tt.expect) {
t.Fatalf("bad fetch (%q): want %q, got %q", tt.metadata, tt.expect, metadata)
}
}
}

View File

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

View File

@@ -77,7 +77,7 @@ func TestProcCmdlineAndFetchConfig(t *testing.T) {
p := NewProcCmdline() p := NewProcCmdline()
p.Location = file.Name() p.Location = file.Name()
cfg, err := p.Fetch() cfg, err := p.FetchUserdata()
if err != nil { if err != nil {
t.Errorf("Test produced error: %v", err) t.Errorf("Test produced error: %v", err)
} }

38
datasource/url.go Normal file
View File

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

View File

@@ -3,11 +3,13 @@ package initialize
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"path" "path"
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml" "github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
"github.com/coreos/coreos-cloudinit/network"
"github.com/coreos/coreos-cloudinit/system" "github.com/coreos/coreos-cloudinit/system"
) )
@@ -20,11 +22,9 @@ type CloudConfigFile interface {
} }
// CloudConfigUnit represents a CoreOS specific configuration option that can generate // CloudConfigUnit represents a CoreOS specific configuration option that can generate
// an associated system.Unit to be created/enabled appropriately // associated system.Units to be created/enabled appropriately
type CloudConfigUnit interface { type CloudConfigUnit interface {
// Unit should either return (*system.Unit, error), or (nil, nil) if nothing Units(root string) ([]system.Unit, error)
// needs to be done for this configuration option.
Unit(root string) (*system.Unit, error)
} }
// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML // CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
@@ -37,10 +37,11 @@ type CloudConfig struct {
Update UpdateConfig Update UpdateConfig
Units []system.Unit Units []system.Unit
} }
WriteFiles []system.File `yaml:"write_files"` WriteFiles []system.File `yaml:"write_files"`
Hostname string Hostname string
Users []system.User Users []system.User
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"` ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
NetworkConfigPath string
} }
type warner func(format string, v ...interface{}) type warner func(format string, v ...interface{})
@@ -215,27 +216,66 @@ func Apply(cfg CloudConfig, env *Environment) error {
} }
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} { for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
u, err := ccu.Unit(env.Root()) u, err := ccu.Units(env.Root())
if err != nil { if err != nil {
return err return err
} }
if u != nil { cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
cfg.Coreos.Units = append(cfg.Coreos.Units, *u) }
wroteEnvironment := false
for _, file := range cfg.WriteFiles {
fullPath, err := system.WriteFile(&file, env.Root())
if err != nil {
return err
}
if path.Clean(file.Path) == "/etc/environment" {
wroteEnvironment = true
}
log.Printf("Wrote file %s to filesystem", fullPath)
}
if !wroteEnvironment {
ef := env.DefaultEnvironmentFile()
if ef != nil {
err := system.WriteEnvFile(ef, env.Root())
if err != nil {
return err
}
log.Printf("Updated /etc/environment")
} }
} }
for _, file := range cfg.WriteFiles { if env.NetconfType() != "" {
file.Path = path.Join(env.Root(), file.Path) netconfBytes, err := ioutil.ReadFile(path.Join(env.ConfigRoot(), cfg.NetworkConfigPath))
if err := system.WriteFile(&file); err != nil { if err != nil {
return err
}
var interfaces []network.InterfaceGenerator
switch env.NetconfType() {
case "debian":
interfaces, err = network.ProcessDebianNetconf(string(netconfBytes))
default:
return fmt.Errorf("Unsupported network config format %q", env.NetconfType())
}
if err != nil {
return err
}
if err := system.WriteNetworkdConfigs(interfaces); err != nil {
return err
}
if err := system.RestartNetwork(interfaces); err != nil {
return err return err
} }
log.Printf("Wrote file %s to filesystem", file.Path)
} }
commands := make(map[string]string, 0) commands := make(map[string]string, 0)
reload := false reload := false
for _, unit := range cfg.Coreos.Units { for _, unit := range cfg.Coreos.Units {
dst := system.UnitDestination(&unit, env.Root()) dst := unit.Destination(env.Root())
if unit.Content != "" { if unit.Content != "" {
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
if err := system.PlaceUnit(&unit, dst); err != nil { if err := system.PlaceUnit(&unit, dst); err != nil {
@@ -247,7 +287,12 @@ func Apply(cfg CloudConfig, env *Environment) error {
if unit.Mask { if unit.Mask {
log.Printf("Masking unit file %s", unit.Name) log.Printf("Masking unit file %s", unit.Name)
if err := system.MaskUnit(unit.Name, env.Root()); err != nil { if err := system.MaskUnit(&unit, env.Root()); err != nil {
return err
}
} else if unit.Runtime {
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
if err := system.UnmaskUnit(&unit, env.Root()); err != nil {
return err return err
} }
} }

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package initialize
import ( import (
"errors" "errors"
"fmt" "fmt"
"sort"
"github.com/coreos/coreos-cloudinit/system" "github.com/coreos/coreos-cloudinit/system"
) )
@@ -19,18 +20,25 @@ func (ee EtcdEnvironment) String() (out string) {
} }
} }
var sorted sort.StringSlice
for k, _ := range norm {
sorted = append(sorted, k)
}
sorted.Sort()
out += "[Service]\n" out += "[Service]\n"
for key, val := range norm { for _, key := range sorted {
val := norm[key]
out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val) out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val)
} }
return return
} }
// Unit creates a Unit file drop-in for etcd, using any configured // Units creates a Unit file drop-in for etcd, using any configured
// options and adding a default MachineID if unset. // options and adding a default MachineID if unset.
func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) { func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
if ee == nil { if ee == nil {
return nil, nil return nil, nil
} }
@@ -45,10 +53,11 @@ func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) {
} }
} }
return &system.Unit{ etcd := system.Unit{
Name: "etcd.service", Name: "etcd.service",
Runtime: true, Runtime: true,
DropIn: true, DropIn: true,
Content: ee.String(), Content: ee.String(),
}, nil }
return []system.Unit{etcd}, nil
} }

View File

@@ -59,7 +59,7 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
} }
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) { func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
ec := EtcdEnvironment{ ee := EtcdEnvironment{
"name": "node001", "name": "node001",
"discovery": "http://disco.example.com/foobar", "discovery": "http://disco.example.com/foobar",
"peer-bind-addr": "127.0.0.1:7002", "peer-bind-addr": "127.0.0.1:7002",
@@ -70,17 +70,18 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
u, err := ec.Unit(dir) uu, err := ee.Units(dir)
if err != nil { if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err) t.Fatalf("Generating etcd unit failed: %v", err)
} }
if u == nil { if len(uu) != 1 {
t.Fatalf("Returned nil etcd unit unexpectedly") t.Fatalf("Expected 1 unit to be returned, got %d", len(uu))
} }
u := uu[0]
dst := system.UnitDestination(u, dir) dst := u.Destination(dir)
os.Stderr.WriteString("writing to " + dir + "\n") os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil { if err := system.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err) t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
} }
@@ -101,8 +102,8 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
} }
expect := `[Service] expect := `[Service]
Environment="ETCD_NAME=node001"
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar" Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_NAME=node001"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
` `
if string(contents) != expect { if string(contents) != expect {
@@ -124,17 +125,18 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
t.Fatalf("Failed writing out /etc/machine-id: %v", err) t.Fatalf("Failed writing out /etc/machine-id: %v", err)
} }
u, err := ee.Unit(dir) uu, err := ee.Units(dir)
if err != nil { if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err) t.Fatalf("Generating etcd unit failed: %v", err)
} }
if u == nil { if len(uu) == 0 {
t.Fatalf("Returned nil etcd unit unexpectedly") t.Fatalf("Returned empty etcd units unexpectedly")
} }
u := uu[0]
dst := system.UnitDestination(u, dir) dst := u.Destination(dir)
os.Stderr.WriteString("writing to " + dir + "\n") os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil { if err := system.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err) t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
} }
@@ -159,8 +161,8 @@ func TestEtcdEnvironmentWhenNil(t *testing.T) {
if ee != nil { if ee != nil {
t.Fatalf("EtcdEnvironment is not nil") t.Fatalf("EtcdEnvironment is not nil")
} }
u, err := ee.Unit("") uu, err := ee.Units("")
if u != nil || err != nil { if len(uu) != 0 || err != nil {
t.Fatalf("Unit returned a non-nil value for nil input") t.Fatalf("Units returned value for nil input")
} }
} }

View File

@@ -19,16 +19,17 @@ func (fe FleetEnvironment) String() (out string) {
return return
} }
// Unit generates a Unit file drop-in for fleet, if any fleet options were // Units generates a Unit file drop-in for fleet, if any fleet options were
// configured in cloud-config // configured in cloud-config
func (fe FleetEnvironment) Unit(root string) (*system.Unit, error) { func (fe FleetEnvironment) Units(root string) ([]system.Unit, error) {
if len(fe) < 1 { if len(fe) < 1 {
return nil, nil return nil, nil
} }
return &system.Unit{ fleet := system.Unit{
Name: "fleet.service", Name: "fleet.service",
Runtime: true, Runtime: true,
DropIn: true, DropIn: true,
Content: fe.String(), Content: fe.String(),
}, nil }
return []system.Unit{fleet}, nil
} }

View File

@@ -19,20 +19,21 @@ Environment="FLEET_PUBLIC_IP=12.34.56.78"
func TestFleetUnit(t *testing.T) { func TestFleetUnit(t *testing.T) {
cfg := make(FleetEnvironment, 0) cfg := make(FleetEnvironment, 0)
u, err := cfg.Unit("/") uu, err := cfg.Units("/")
if u != nil { if len(uu) != 0 {
t.Errorf("unexpectedly generated unit with empty FleetEnvironment") t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
} }
cfg["public-ip"] = "12.34.56.78" cfg["public-ip"] = "12.34.56.78"
u, err = cfg.Unit("/") uu, err = cfg.Units("/")
if err != nil { if err != nil {
t.Errorf("error generating fleet unit: %v", err) t.Errorf("error generating fleet unit: %v", err)
} }
if u == nil { if len(uu) != 1 {
t.Fatalf("unexpectedly got nil unit generating fleet unit!") t.Fatalf("expected 1 unit generated, got %d", len(uu))
} }
u := uu[0]
if !u.Runtime { if !u.Runtime {
t.Errorf("bad Runtime for generated fleet unit!") t.Errorf("bad Runtime for generated fleet unit!")
} }

View File

@@ -50,9 +50,7 @@ func TestEtcHostsWrittenToDisk(t *testing.T) {
t.Fatalf("manageEtcHosts returned nil file unexpectedly") t.Fatalf("manageEtcHosts returned nil file unexpectedly")
} }
f.Path = path.Join(dir, f.Path) if _, err := system.WriteFile(f, dir); err != nil {
if err := system.WriteFile(f); err != nil {
t.Fatalf("Error writing EtcHosts: %v", err) t.Fatalf("Error writing EtcHosts: %v", err)
} }

52
initialize/meta_data.go Normal file
View File

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

View File

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

View File

@@ -31,8 +31,7 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
t.Fatalf("OEMRelease returned nil file unexpectedly") t.Fatalf("OEMRelease returned nil file unexpectedly")
} }
f.Path = path.Join(dir, f.Path) if _, err := system.WriteFile(f, dir); err != nil {
if err := system.WriteFile(f); err != nil {
t.Fatalf("Writing of OEMRelease failed: %v", err) t.Fatalf("Writing of OEMRelease failed: %v", err)
} }

View File

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

View File

@@ -12,7 +12,8 @@ import (
) )
const ( const (
locksmithUnit = "locksmithd.service" locksmithUnit = "locksmithd.service"
updateEngineUnit = "update-engine.service"
) )
// updateOption represents a configurable update option, which, if set, will be // updateOption represents a configurable update option, which, if set, will be
@@ -36,7 +37,6 @@ var updateOptions = []*updateOption{
&updateOption{ &updateOption{
key: "group", key: "group",
prefix: "GROUP=", prefix: "GROUP=",
valid: []string{"master", "beta", "alpha", "stable"},
}, },
&updateOption{ &updateOption{
key: "server", key: "server",
@@ -126,24 +126,40 @@ func (uc UpdateConfig) File(root string) (*system.File, error) {
}, nil }, nil
} }
// GetUnit generates a locksmith system.Unit, if reboot-strategy was set in // Units generates units for the cloud-init initializer to act on:
// cloud-config, for the cloud-init initializer to act on appropriately // - a locksmith system.Unit, if "reboot-strategy" was set in cloud-config
func (uc UpdateConfig) Unit(root string) (*system.Unit, error) { // - an update_engine system.Unit, if "group" was set in cloud-config
strategy, ok := uc["reboot-strategy"] func (uc UpdateConfig) Units(root string) ([]system.Unit, error) {
if !ok { var units []system.Unit
return nil, nil if strategy, ok := uc["reboot-strategy"]; ok {
ls := &system.Unit{
Name: locksmithUnit,
Command: "restart",
Mask: false,
Runtime: true,
}
if strategy == "off" {
ls.Command = "stop"
ls.Mask = true
}
units = append(units, *ls)
} }
u := &system.Unit{ rue := false
Name: locksmithUnit, if _, ok := uc["group"]; ok {
Command: "restart", rue = true
Mask: false, }
if _, ok := uc["server"]; ok {
rue = true
}
if rue {
ue := system.Unit{
Name: updateEngineUnit,
Command: "restart",
}
units = append(units, ue)
} }
if strategy == "off" { return units, nil
u.Command = "stop"
u.Mask = true
}
return u, nil
} }

View File

@@ -38,12 +38,12 @@ func TestEmptyUpdateConfig(t *testing.T) {
if f != nil { if f != nil {
t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f) t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f)
} }
u, err := uc.Unit("") uu, err := uc.Units("")
if err != nil { if err != nil {
t.Error("unexpected error getting unit from empty UpdateConfig") t.Error("unexpected error getting unit from empty UpdateConfig")
} }
if u != nil { if len(uu) != 0 {
t.Errorf("getting unit from empty UpdateConfig should have returned nil, got %v", u) t.Errorf("getting unit from empty UpdateConfig should have returned zero units, got %d", len(uu))
} }
} }
@@ -106,6 +106,21 @@ SERVER=http://foo.com`
t.Errorf("File has incorrect contents, got %v, want %v", got, want) t.Errorf("File has incorrect contents, got %v, want %v", got, want)
} }
} }
uu, err := u.Units(dir)
if err != nil {
t.Errorf("unexpected error getting units from UpdateConfig: %v", err)
} else if len(uu) != 1 {
t.Errorf("unexpected number of files returned from UpdateConfig: want 1, got %d", len(uu))
} else {
unit := uu[0]
if unit.Name != "update-engine.service" {
t.Errorf("bad name for generated unit: want update-engine.service, got %s", unit.Name)
}
if unit.Command != "restart" {
t.Errorf("bad command for generated unit: want restart, got %s", unit.Command)
}
}
} }
func TestRebootStrategies(t *testing.T) { func TestRebootStrategies(t *testing.T) {
@@ -145,12 +160,13 @@ func TestRebootStrategies(t *testing.T) {
t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line) t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line)
} }
} }
u, err := uc.Unit(dir) uu, err := uc.Units(dir)
if err != nil { if err != nil {
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name) t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
} else if u == nil { } else if len(uu) != 1 {
t.Errorf("generated empty unit for reboot-strategy=%v", s.name) t.Errorf("unexpected number of units for reboot-strategy=%v: %d", s.name, len(uu))
} else { } else {
u := uu[0]
if u.Name != locksmithUnit { if u.Name != locksmithUnit {
t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name) t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name)
} }
@@ -189,8 +205,7 @@ func TestUpdateConfWrittenToDisk(t *testing.T) {
t.Fatal("Unexpectedly got nil updateconfig file") t.Fatal("Unexpectedly got nil updateconfig file")
} }
f.Path = path.Join(dir, f.Path) if _, err := system.WriteFile(f, dir); err != nil {
if err := system.WriteFile(f); err != nil {
t.Fatalf("Error writing update config: %v", err) t.Fatalf("Error writing update config: %v", err)
} }

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package initialize
import ( import (
"io/ioutil" "io/ioutil"
"path" "path"
"strings"
"github.com/coreos/coreos-cloudinit/system" "github.com/coreos/coreos-cloudinit/system"
) )
@@ -28,21 +29,23 @@ func PersistScriptInWorkspace(script system.Script, workspace string) (string, e
} }
tmp.Close() tmp.Close()
relpath := strings.TrimPrefix(tmp.Name(), workspace)
file := system.File{ file := system.File{
Path: tmp.Name(), Path: relpath,
RawFilePermissions: "0744", RawFilePermissions: "0744",
Content: string(script), Content: string(script),
} }
err = system.WriteFile(&file) return system.WriteFile(&file, workspace)
return file.Path, err
} }
func PersistUnitNameInWorkspace(name string, workspace string) error { func PersistUnitNameInWorkspace(name string, workspace string) error {
file := system.File{ file := system.File{
Path: path.Join(workspace, "scripts", "unit-name"), Path: path.Join("scripts", "unit-name"),
RawFilePermissions: "0644", RawFilePermissions: "0644",
Content: name, Content: name,
} }
return system.WriteFile(&file) _, err := system.WriteFile(&file, workspace)
return err
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

89
system/env_file.go Normal file
View File

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

385
system/env_file_test.go Normal file
View File

@@ -0,0 +1,385 @@
package system
import (
"io/ioutil"
"os"
"path"
"strings"
"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)
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)
}
}
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)
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)
}
}
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.Fatal("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.Fatal("Unable to stat file: %v", err)
}
if oldStat.ModTime() != newStat.ModTime() {
t.Fatal("File mtime changed.")
}
}
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)
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)
}
}
// 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.Fatal("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.Fatal("Unable to stat file: %v", err)
}
if oldStat.ModTime() != newStat.ModTime() {
t.Fatal("File mtime changed.")
}
}
// 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.Fatal("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.Fatal("Unable to stat file: %v", err)
}
if oldStat.ModTime() != newStat.ModTime() {
t.Fatal("File mtime changed.")
}
}
// 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"
fullPath := path.Join(dir, name)
ioutil.WriteFile(fullPath, []byte(base), 0000)
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

@@ -31,33 +31,55 @@ func (f *File) Permissions() (os.FileMode, error) {
return os.FileMode(perm), nil return os.FileMode(perm), nil
} }
func WriteFile(f *File) error { func WriteFile(f *File, root string) (string, error) {
if f.Encoding != "" { if f.Encoding != "" {
return fmt.Errorf("Unable to write file with encoding %s", f.Encoding) return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
} }
if err := os.MkdirAll(path.Dir(f.Path), os.FileMode(0755)); err != nil { fullpath := path.Join(root, f.Path)
return err dir := path.Dir(fullpath)
if err := EnsureDirectoryExists(dir); err != nil {
return "", err
} }
perm, err := f.Permissions() perm, err := f.Permissions()
if err != nil { if err != nil {
return err return "", err
} }
if err := ioutil.WriteFile(f.Path, []byte(f.Content), perm); err != nil { var tmp *os.File
return err // Create a temporary file in the same directory to ensure it's on the same filesystem
if tmp, err = ioutil.TempFile(dir, "cloudinit-temp"); err != nil {
return "", err
}
if err := ioutil.WriteFile(tmp.Name(), []byte(f.Content), perm); err != nil {
return "", err
}
if err := tmp.Close(); err != nil {
return "", err
}
// Ensure the permissions are as requested (since WriteFile can be affected by sticky bit)
if err := os.Chmod(tmp.Name(), perm); err != nil {
return "", err
} }
if f.Owner != "" { if f.Owner != "" {
// We shell out since we don't have a way to look up unix groups natively // We shell out since we don't have a way to look up unix groups natively
cmd := exec.Command("chown", f.Owner, f.Path) cmd := exec.Command("chown", f.Owner, tmp.Name())
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return err return "", err
} }
} }
return nil if err := os.Rename(tmp.Name(), fullpath); err != nil {
return "", err
}
return fullpath, nil
} }
func EnsureDirectoryExists(dir string) error { func EnsureDirectoryExists(dir string) error {

View File

@@ -4,7 +4,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"syscall"
"testing" "testing"
) )
@@ -13,18 +12,22 @@ func TestWriteFileUnencodedContent(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer syscall.Rmdir(dir) defer os.RemoveAll(dir)
fullPath := path.Join(dir, "tmp", "foo") fn := "foo"
fullPath := path.Join(dir, fn)
wf := File{ wf := File{
Path: fullPath, Path: fn,
Content: "bar", Content: "bar",
RawFilePermissions: "0644", RawFilePermissions: "0644",
} }
if err := WriteFile(&wf); err != nil { path, err := WriteFile(&wf, dir)
if err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err) t.Fatalf("Processing of WriteFile failed: %v", err)
} else if path != fullPath {
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
} }
fi, err := os.Stat(fullPath) fi, err := os.Stat(fullPath)
@@ -51,15 +54,15 @@ func TestWriteFileInvalidPermission(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer syscall.Rmdir(dir) defer os.RemoveAll(dir)
wf := File{ wf := File{
Path: path.Join(dir, "tmp", "foo"), Path: path.Join(dir, "tmp", "foo"),
Content: "bar", Content: "bar",
RawFilePermissions: "pants", RawFilePermissions: "pants",
} }
if err := WriteFile(&wf); err == nil { if _, err := WriteFile(&wf, dir); err == nil {
t.Fatalf("Expected error to be raised when writing file with invalid permission") t.Fatalf("Expected error to be raised when writing file with invalid permission")
} }
} }
@@ -69,17 +72,21 @@ func TestWriteFilePermissions(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer syscall.Rmdir(dir) defer os.RemoveAll(dir)
fullPath := path.Join(dir, "tmp", "foo") fn := "foo"
fullPath := path.Join(dir, fn)
wf := File{ wf := File{
Path: fullPath, Path: fn,
RawFilePermissions: "0755", RawFilePermissions: "0755",
} }
if err := WriteFile(&wf); err != nil { path, err := WriteFile(&wf, dir)
if err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err) t.Fatalf("Processing of WriteFile failed: %v", err)
} else if path != fullPath {
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
} }
fi, err := os.Stat(fullPath) fi, err := os.Stat(fullPath)
@@ -97,15 +104,15 @@ func TestWriteFileEncodedContent(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer syscall.Rmdir(dir) defer os.RemoveAll(dir)
wf := File{ wf := File{
Path: path.Join(dir, "tmp", "foo"), Path: path.Join(dir, "tmp", "foo"),
Content: "", Content: "",
Encoding: "base64", Encoding: "base64",
} }
if err := WriteFile(&wf); err == nil { if _, err := WriteFile(&wf, dir); err == nil {
t.Fatalf("Expected error to be raised when writing file with encoding") t.Fatalf("Expected error to be raised when writing file with encoding")
} }
} }

View File

@@ -36,7 +36,9 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
sysInterfaceMap := make(map[string]*net.Interface) sysInterfaceMap := make(map[string]*net.Interface)
if systemInterfaces, err := net.Interfaces(); err == nil { if systemInterfaces, err := net.Interfaces(); err == nil {
for _, iface := range systemInterfaces { for _, iface := range systemInterfaces {
sysInterfaceMap[iface.Name] = &iface // Need a copy of the interface so we can take the address
temp := iface
sysInterfaceMap[temp.Name] = &temp
} }
} else { } else {
return err return err
@@ -64,15 +66,15 @@ func restartNetworkd() error {
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error { func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
for _, iface := range interfaces { for _, iface := range interfaces {
filename := path.Join(runtimeNetworkPath, fmt.Sprintf("%s.netdev", iface.Name())) filename := path.Join(runtimeNetworkPath, fmt.Sprintf("%s.netdev", iface.Filename()))
if err := writeConfig(filename, iface.Netdev()); err != nil { if err := writeConfig(filename, iface.Netdev()); err != nil {
return err return err
} }
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.link", iface.Name())) filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.link", iface.Filename()))
if err := writeConfig(filename, iface.Link()); err != nil { if err := writeConfig(filename, iface.Link()); err != nil {
return err return err
} }
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.network", iface.Name())) filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.network", iface.Filename()))
if err := writeConfig(filename, iface.Network()); err != nil { if err := writeConfig(filename, iface.Network()); err != nil {
return err return err
} }

View File

@@ -51,10 +51,10 @@ func (u *Unit) Group() (group string) {
type Script []byte type Script []byte
// UnitDestination builds the appropriate absolute file path for // Destination builds the appropriate absolute file path for
// the given Unit. The root argument indicates the effective base // the Unit. The root argument indicates the effective base
// directory of the system (similar to a chroot). // directory of the system (similar to a chroot).
func UnitDestination(u *Unit, root string) string { func (u *Unit) Destination(root string) string {
dir := "etc" dir := "etc"
if u.Runtime { if u.Runtime {
dir = "run" dir = "run"
@@ -78,12 +78,12 @@ func PlaceUnit(u *Unit, dst string) error {
} }
file := File{ file := File{
Path: dst, Path: filepath.Base(dst),
Content: u.Content, Content: u.Content,
RawFilePermissions: "0644", RawFilePermissions: "0644",
} }
err := WriteFile(&file) _, err := WriteFile(&file, dir)
if err != nil { if err != nil {
return err return err
} }
@@ -179,12 +179,12 @@ func MachineID(root string) string {
return id return id
} }
// MaskUnit masks a Unit by the given name by symlinking its unit file (in // MaskUnit masks the given Unit by symlinking its unit file to
// /etc/systemd/system) to /dev/null, analogous to `systemctl mask` // /dev/null, analogous to `systemctl mask`.
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit // N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
// file* in /etc/systemd/system, to ensure that the mask will succeed. // file at the location*, to ensure that the mask will succeed.
func MaskUnit(unit string, root string) error { func MaskUnit(unit *Unit, root string) error {
masked := path.Join(root, "etc", "systemd", "system", unit) masked := unit.Destination(root)
if _, err := os.Stat(masked); os.IsNotExist(err) { if _, err := os.Stat(masked); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil { if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
return err return err
@@ -194,3 +194,38 @@ func MaskUnit(unit string, root string) error {
} }
return os.Symlink("/dev/null", masked) return os.Symlink("/dev/null", masked)
} }
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
// associated with the given Unit is empty or appears to be a symlink to
// /dev/null, it is removed.
func UnmaskUnit(unit *Unit, root string) error {
masked := unit.Destination(root)
ne, err := nullOrEmpty(masked)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return err
}
if !ne {
log.Printf("%s is not null or empty, refusing to unmask", masked)
return nil
}
return os.Remove(masked)
}
// nullOrEmpty checks whether a given path appears to be an empty regular file
// or a symlink to /dev/null
func nullOrEmpty(path string) (bool, error) {
fi, err := os.Stat(path)
if err != nil {
return false, err
}
m := fi.Mode()
if m.IsRegular() && fi.Size() <= 0 {
return true, nil
}
if m&os.ModeCharDevice > 0 {
return true, nil
}
return false, nil
}

View File

@@ -25,10 +25,10 @@ Address=10.209.171.177/19
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
dst := UnitDestination(&u, dir) dst := u.Destination(dir)
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network") expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
if dst != expectDst { if dst != expectDst {
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst) t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
if err := PlaceUnit(&u, dst); err != nil { if err := PlaceUnit(&u, dst); err != nil {
@@ -69,18 +69,18 @@ func TestUnitDestination(t *testing.T) {
DropIn: false, DropIn: false,
} }
dst := UnitDestination(&u, dir) dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service") expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
if dst != expectDst { if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst) t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
u.DropIn = true u.DropIn = true
dst = UnitDestination(&u, dir) dst = u.Destination(dir)
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn) expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
if dst != expectDst { if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst) t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
} }
@@ -100,10 +100,10 @@ Where=/media/state
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
dst := UnitDestination(&u, dir) dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount") expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
if dst != expectDst { if dst != expectDst {
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst) t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
} }
if err := PlaceUnit(&u, dst); err != nil { if err := PlaceUnit(&u, dst); err != nil {
@@ -156,7 +156,8 @@ func TestMaskUnit(t *testing.T) {
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
// Ensure mask works with units that do not currently exist // Ensure mask works with units that do not currently exist
if err := MaskUnit("foo.service", dir); err != nil { uf := &Unit{Name: "foo.service"}
if err := MaskUnit(uf, dir); err != nil {
t.Fatalf("Unable to mask new unit: %v", err) t.Fatalf("Unable to mask new unit: %v", err)
} }
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service") fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
@@ -169,11 +170,12 @@ func TestMaskUnit(t *testing.T) {
} }
// Ensure mask works with unit files that already exist // Ensure mask works with unit files that already exist
ub := &Unit{Name: "bar.service"}
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service") barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
if _, err := os.Create(barPath); err != nil { if _, err := os.Create(barPath); err != nil {
t.Fatalf("Error creating new unit file: %v", err) t.Fatalf("Error creating new unit file: %v", err)
} }
if err := MaskUnit("bar.service", dir); err != nil { if err := MaskUnit(ub, dir); err != nil {
t.Fatalf("Unable to mask existing unit: %v", err) t.Fatalf("Unable to mask existing unit: %v", err)
} }
barTgt, err := os.Readlink(barPath) barTgt, err := os.Readlink(barPath)
@@ -184,3 +186,94 @@ func TestMaskUnit(t *testing.T) {
t.Fatalf("unit not masked, got unit target", barTgt) t.Fatalf("unit not masked, got unit target", barTgt)
} }
} }
func TestUnmaskUnit(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
nilUnit := &Unit{Name: "null.service"}
if err := UnmaskUnit(nilUnit, dir); 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 := UnmaskUnit(uf, dir); 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 := UnmaskUnit(ub, dir); err != nil {
t.Errorf("unmask of unit returned unexpected error: %v", err)
}
if _, err := os.Stat(dst); !os.IsNotExist(err) {
t.Errorf("expected %s to not exist after unmask, but got err: %s", err)
}
}
func TestNullOrEmpty(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
non := path.Join(dir, "does_not_exist")
ne, err := nullOrEmpty(non)
if !os.IsNotExist(err) {
t.Errorf("nullOrEmpty on nonexistent file returned bad error: %v", err)
}
if ne {
t.Errorf("nullOrEmpty returned true unxpectedly")
}
regEmpty := path.Join(dir, "regular_empty_file")
_, err = os.Create(regEmpty)
if err != nil {
t.Fatalf("Unable to create tempfile: %v", err)
}
gotNe, gotErr := nullOrEmpty(regEmpty)
if !gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of regular empty file returned %t, %v - want true, nil", gotNe, gotErr)
}
reg := path.Join(dir, "regular_file")
if err := ioutil.WriteFile(reg, []byte("asdf"), 700); err != nil {
t.Fatalf("Unable to create tempfile: %v", err)
}
gotNe, gotErr = nullOrEmpty(reg)
if gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of regular file returned %t, %v - want false, nil", gotNe, gotErr)
}
null := path.Join(dir, "null")
if err := os.Symlink(os.DevNull, null); err != nil {
t.Fatalf("Unable to create /dev/null link: %s", err)
}
gotNe, gotErr = nullOrEmpty(null)
if !gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of null symlink returned %t, %v - want true, nil", gotNe, gotErr)
}
}

2
test
View File

@@ -18,7 +18,7 @@ declare -a TESTPKGS=(initialize system datasource pkg network)
if [ -z "$PKG" ]; then if [ -z "$PKG" ]; then
GOFMTPATH="$TESTPKGS coreos-cloudinit.go" GOFMTPATH="$TESTPKGS coreos-cloudinit.go"
# prepend repo path to each package # prepend repo path to each package
TESTPKGS=${TESTPKGS[@]/#/${REPO_PATH}/} TESTPKGS="${TESTPKGS[@]/#/${REPO_PATH}/} ./"
else else
GOFMTPATH="$TESTPKGS" GOFMTPATH="$TESTPKGS"
# strip out slashes and dots from PKG=./foo/ # strip out slashes and dots from PKG=./foo/

View File

@@ -3,9 +3,9 @@
ACTION!="add|change", GOTO="coreos_configdrive_end" ACTION!="add|change", GOTO="coreos_configdrive_end"
# A normal config drive. Block device formatted with iso9660 or fat # A normal config drive. Block device formatted with iso9660 or fat
SUBSYSTEM=="block", ENV{ID_FS_TYPE}=="iso9660|vfat", ENV{ID_FS_LABEL}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="configdrive-block.service" SUBSYSTEM=="block", ENV{ID_FS_TYPE}=="iso9660|vfat", ENV{ID_FS_LABEL}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="media-configdrive.mount"
# Addtionally support virtfs from QEMU # Addtionally support virtfs from QEMU
SUBSYSTEM=="virtio", DRIVER=="9pnet_virtio", ATTR{mount_tag}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="configdrive-virtfs.service" SUBSYSTEM=="virtio", DRIVER=="9pnet_virtio", ATTR{mount_tag}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="media-configvirtfs.mount"
LABEL="coreos_configdrive_end" LABEL="coreos_configdrive_end"

View File

@@ -1,11 +0,0 @@
[Unit]
Description=Mount config drive
Conflicts=configdrive-virtfs.service umount.target
ConditionPathIsMountPoint=!/media/configdrive
# Only mount config drive block devices automatically in virtual machines
ConditionVirtualization=vm
[Service]
Type=oneshot
RemainAfterExit=no
ExecStart=/bin/mount -t auto -o ro,x-mount.mkdir LABEL=config-2 /media/configdrive

View File

@@ -1,14 +0,0 @@
[Unit]
Description=Mount config drive from virtfs
Conflicts=configdrive-block.service umount.target
ConditionPathIsMountPoint=!/media/configdrive
ConditionVirtualization=vm
# Support old style setup for now
Wants=addon-run@media-configdrive.service addon-config@media-configdrive.service
Before=addon-run@media-configdrive.service addon-config@media-configdrive.service
[Service]
Type=oneshot
RemainAfterExit=no
ExecStart=/bin/mount -t 9p -o trans=virtio,version=9p2000.L,x-mount.mkdir config-2 /media/configdrive

View File

@@ -0,0 +1,13 @@
[Unit]
Wants=user-configdrive.service
Before=user-configdrive.service
# Only mount config drive block devices automatically in virtual machines
# or any host that has it explicitly enabled and not explicitly disabled.
ConditionVirtualization=|vm
ConditionKernelCommandLine=|coreos.configdrive=1
ConditionKernelCommandLine=!coreos.configdrive=0
[Mount]
What=LABEL=config-2
Where=/media/configdrive
Options=ro

View File

@@ -0,0 +1,18 @@
[Unit]
Wants=user-configvirtfs.service
Before=user-configvirtfs.service
# Only mount config drive block devices automatically in virtual machines
# or any host that has it explicitly enabled and not explicitly disabled.
ConditionVirtualization=|vm
ConditionKernelCommandLine=|coreos.configdrive=1
ConditionKernelCommandLine=!coreos.configdrive=0
# Support old style setup for now
Wants=addon-run@media-configvirtfs.service addon-config@media-configvirtfs.service
Before=addon-run@media-configvirtfs.service addon-config@media-configvirtfs.service
[Mount]
What=config-2
Where=/media/configvirtfs
Options=ro,trans=virtio,version=9p2000.L
Type=9p

View File

@@ -1,5 +1,10 @@
[Unit] [Unit]
Description=Watch for a cloud-config at /media/configdrive Description=Watch for a cloud-config at /media/configdrive
# Note: This unit is essentially just here as a fall-back mechanism to
# trigger cloudinit if it isn't triggered explicitly by other means
# such as by a Wants= in the mount unit. This ensures we handle the
# case where /media/configdrive is provided to a CoreOS container.
[Path] [Path]
DirectoryNotEmpty=/media/configdrive DirectoryNotEmpty=/media/configdrive

View File

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

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Load cloud-config from /media/configvirtfs
Requires=coreos-setup-environment.service
After=coreos-setup-environment.service
Before=user-config.target
[Service]
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=-/etc/environment
ExecStart=/usr/bin/coreos-cloudinit --from-configdrive=/media/configvirtfs