Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a398ce82f7 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
*.swp
|
||||
bin/
|
||||
coverage/
|
||||
gopath/
|
||||
pkg/
|
||||
|
@@ -1,8 +0,0 @@
|
||||
language: go
|
||||
go: 1.2
|
||||
|
||||
install:
|
||||
- go get code.google.com/p/go.tools/cmd/cover
|
||||
|
||||
script:
|
||||
- ./test
|
@@ -13,7 +13,7 @@ If no **id** field is provided, coreos-cloudinit will ignore this section.
|
||||
|
||||
For example, the following cloud-config document...
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
coreos:
|
||||
oem:
|
||||
@@ -26,7 +26,7 @@ coreos:
|
||||
|
||||
...would be rendered to the following `/etc/oem-release`:
|
||||
|
||||
```yaml
|
||||
```
|
||||
ID=rackspace
|
||||
NAME="Rackspace Cloud Servers"
|
||||
VERSION_ID=168.0.0
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Using Cloud-Config
|
||||
|
||||
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime. Your cloud-config is processed during each boot.
|
||||
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime.
|
||||
|
||||
## Configuration File
|
||||
|
||||
@@ -16,7 +16,7 @@ We've designed our implementation to allow the same cloud-config file to work ac
|
||||
|
||||
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
|
||||
|
||||
A cloud-config file should contain `#cloud-config`, followed by an associative array which has zero or more of the following keys:
|
||||
A cloud-config file should contain an associative array which has zero or more of the following keys:
|
||||
|
||||
- `coreos`
|
||||
- `ssh_authorized_keys`
|
||||
@@ -40,9 +40,9 @@ CoreOS tries to conform to each platform's native method to provide user data. E
|
||||
#### etcd
|
||||
|
||||
The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
|
||||
If the platform environment supports the templating feature of coreos-cloudinit it is possible to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
|
||||
We can use the templating feature of coreos-cloudinit to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
@@ -57,7 +57,7 @@ coreos:
|
||||
|
||||
...will generate a systemd unit drop-in like this:
|
||||
|
||||
```yaml
|
||||
```
|
||||
[Service]
|
||||
Environment="ETCD_NAME=node001"
|
||||
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
|
||||
@@ -68,55 +68,19 @@ Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
|
||||
For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
|
||||
Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
|
||||
|
||||
_Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, and Vagrant._
|
||||
|
||||
[etcd-config]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md
|
||||
|
||||
#### fleet
|
||||
|
||||
The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allow for the configuration of fleet through environment variables. For example, the following cloud-config document...
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
fleet:
|
||||
public-ip: $public_ipv4
|
||||
metadata: region=us-west
|
||||
```
|
||||
|
||||
...will generate a systemd unit drop-in like this:
|
||||
|
||||
```yaml
|
||||
[Service]
|
||||
Environment="FLEET_PUBLIC_IP=203.0.113.29"
|
||||
Environment="FLEET_METADATA=region=us-west"
|
||||
```
|
||||
|
||||
For more information on fleet configuration, see the [fleet documentation][fleet-config].
|
||||
|
||||
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/configuration.md
|
||||
|
||||
#### update
|
||||
|
||||
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
|
||||
|
||||
These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field.
|
||||
The `reboot-strategy` parameter also affects the behaviour of [locksmith](https://github.com/coreos/locksmith).
|
||||
|
||||
- **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed.
|
||||
- _reboot_: Reboot immediately after an update is applied.
|
||||
- _etcd-lock_: Reboot after first taking a distributed lock in etcd, this guarantees that only one host will reboot concurrently and that the cluster will remain available during the update.
|
||||
- _best-effort_ - If etcd is running, "etcd-lock", otherwise simply "reboot".
|
||||
- _off_ - Disable rebooting after updates are applied (not recommended).
|
||||
- **server**: is the omaha endpoint URL which will be queried for updates.
|
||||
- **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable")
|
||||
|
||||
*Note: cloudinit will only manipulate the locksmith unit file in the systemd runtime directory (`/run/systemd/system/locksmithd.service`). If any manual modifications are made to an overriding unit configuration file (e.g. `/etc/systemd/system/locksmithd.service`), cloudinit will no longer be able to control the locksmith service unit.*
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
coreos:
|
||||
update:
|
||||
@@ -125,16 +89,13 @@ coreos:
|
||||
|
||||
#### units
|
||||
|
||||
The `coreos.units.*` parameters define a list of arbitrary systemd units to start after booting. This feature is intended to help you start essential services required to mount storage and configure networking in order to join the CoreOS cluster. It is not intended to be a Chef/Puppet replacement.
|
||||
|
||||
Each item is an object with the following fields:
|
||||
The `coreos.units.*` parameters define a list of arbitrary systemd units to start. Each item is an object with the following fields:
|
||||
|
||||
- **name**: String representing unit's name. Required.
|
||||
- **runtime**: Boolean indicating whether or not to persist the unit across reboots. This is analogous to the `--runtime` argument to `systemctl enable`. Default value is false.
|
||||
- **runtime**: Boolean indicating whether or not to persist the unit across reboots. This is analagous to the `--runtime` argument to `systemd enable`. Default value is false.
|
||||
- **enable**: Boolean indicating whether or not to handle the [Install] section of the unit file. This is similar to running `systemctl enable <name>`. Default value is false.
|
||||
- **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already.
|
||||
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. Default value is restart.
|
||||
- **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. Default value is false.
|
||||
|
||||
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
|
||||
|
||||
@@ -142,7 +103,7 @@ Each item is an object with the following fields:
|
||||
|
||||
Write a unit to disk, automatically starting it.
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
@@ -159,12 +120,15 @@ coreos:
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/docker start -a redis_server
|
||||
ExecStop=/usr/bin/docker stop -t 2 redis_server
|
||||
|
||||
[Install]
|
||||
WantedBy=local.target
|
||||
```
|
||||
|
||||
Start the built-in `etcd` and `fleet` services:
|
||||
Start the builtin `etcd` and `fleet` services:
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
```
|
||||
# cloud-config
|
||||
|
||||
coreos:
|
||||
units:
|
||||
@@ -181,7 +145,7 @@ The `ssh_authorized_keys` parameter adds public SSH keys which will be authorize
|
||||
The keys will be named "coreos-cloudinit" by default.
|
||||
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
ssh_authorized_keys:
|
||||
@@ -193,7 +157,7 @@ ssh_authorized_keys:
|
||||
The `hostname` parameter defines the system's hostname.
|
||||
This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
hostname: coreos1
|
||||
@@ -207,7 +171,7 @@ All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the use
|
||||
- **name**: Required. Login name of user
|
||||
- **gecos**: GECOS comment of user
|
||||
- **passwd**: Hash of the password to use for this user
|
||||
- **homedir**: User's home directory. Defaults to /home/\<name\>
|
||||
- **homedir**: User's home directory. Defaults to /home/<name>
|
||||
- **no-create-home**: Boolean. Skip home directory creation.
|
||||
- **primary-group**: Default group for the user. Defaults to a new group created named after the user.
|
||||
- **groups**: Add user to these additional groups
|
||||
@@ -226,7 +190,7 @@ The following fields are not yet implemented:
|
||||
- **selinux-user**: Corresponding SELinux user
|
||||
- **ssh-import-id**: Import SSH keys by ID from Launchpad.
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
@@ -265,7 +229,7 @@ Using a higher number of rounds will help create more secure passwords, but give
|
||||
|
||||
Using the `coreos-ssh-import-github` field, we can import public SSH keys from a GitHub user to use as authorized keys to a server.
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
@@ -278,17 +242,17 @@ users:
|
||||
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
|
||||
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-url: https://github-enterprise.example.com/api/v3/users/elroy/keys?access_token=<TOKEN>
|
||||
coreos-ssh-import-url: https://token:<OAUTH-TOKEN>@github-enterprise.example.com/users/elroy/keys
|
||||
```
|
||||
|
||||
You can also specify any URL whose response matches the JSON format for public keys:
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
@@ -308,7 +272,7 @@ The `write-file` parameter defines a list of files to create on the local filesy
|
||||
Explicitly not implemented is the **encoding** attribute.
|
||||
The **content** field must represent exactly what should be written to disk.
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
write_files:
|
||||
- path: /etc/fleet/fleet.conf
|
||||
@@ -325,7 +289,7 @@ Currently, the only supported value is "localhost" which will cause your system'
|
||||
to resolve to "127.0.0.1". This is helpful when the host does not have DNS
|
||||
infrastructure in place to resolve its own hostname, for example, when using Vagrant.
|
||||
|
||||
```yaml
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
manage_etc_hosts: localhost
|
||||
|
@@ -14,21 +14,17 @@ The image should be a single FAT or ISO9660 file system with the label
|
||||
|
||||
For example, to wrap up a config named `user_data` in a config drive image:
|
||||
|
||||
```sh
|
||||
mkdir -p /tmp/new-drive/openstack/latest
|
||||
cp user_data /tmp/new-drive/openstack/latest/user_data
|
||||
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
|
||||
rm -r /tmp/new-drive
|
||||
```
|
||||
mkdir -p /tmp/new-drive/openstack/latest
|
||||
cp user_data /tmp/new-drive/openstack/latest/user_data
|
||||
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
|
||||
rm -r /tmp/new-drive
|
||||
|
||||
## QEMU virtfs
|
||||
|
||||
One exception to the above, when using QEMU it is possible to skip creating an
|
||||
image and use a plain directory containing the same contents:
|
||||
|
||||
```sh
|
||||
qemu-system-x86_64 \
|
||||
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
|
||||
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
|
||||
[usual qemu options here...]
|
||||
```
|
||||
qemu-system-x86_64 \
|
||||
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
|
||||
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
|
||||
[usual qemu options here...]
|
||||
|
@@ -1,27 +0,0 @@
|
||||
#Debian Interfaces#
|
||||
**WARNING**: This option is EXPERIMENTAL and may change or be removed at any
|
||||
point.
|
||||
There is basic support for converting from a Debian network configuration to
|
||||
networkd unit files. The -convert-netconf=debian option is used to activate
|
||||
this feature.
|
||||
|
||||
#convert-netconf#
|
||||
Default: ""
|
||||
Read the network config provided in cloud-drive and translate it from the
|
||||
specified format into networkd unit files (requires the -from-configdrive
|
||||
flag). Currently only supports "debian" which provides support for a small
|
||||
subset of the [Debian network configuration]
|
||||
(https://wiki.debian.org/NetworkConfiguration). These options include:
|
||||
|
||||
- interface config methods
|
||||
- static
|
||||
- address/netmask
|
||||
- gateway
|
||||
- hwaddress
|
||||
- dns-nameservers
|
||||
- dhcp
|
||||
- hwaddress
|
||||
- manual
|
||||
- loopback
|
||||
- vlan_raw_device
|
||||
- bond-slaves
|
@@ -1,4 +1,4 @@
|
||||
# coreos-cloudinit [](https://travis-ci.org/coreos/coreos-cloudinit)
|
||||
# coreos-cloudinit
|
||||
|
||||
coreos-cloudinit enables a user to customize CoreOS machines by providing either a cloud-config document or an executable script through user-data.
|
||||
|
||||
|
7
build
7
build
@@ -3,12 +3,7 @@
|
||||
ORG_PATH="github.com/coreos"
|
||||
REPO_PATH="${ORG_PATH}/coreos-cloudinit"
|
||||
|
||||
if [ ! -h gopath/src/${REPO_PATH} ]; then
|
||||
mkdir -p gopath/src/${ORG_PATH}
|
||||
ln -s ../../../.. gopath/src/${REPO_PATH} || exit 255
|
||||
fi
|
||||
|
||||
export GOBIN=${PWD}/bin
|
||||
export GOPATH=${PWD}/gopath
|
||||
export GOPATH=${PWD}
|
||||
|
||||
go build -o bin/coreos-cloudinit ${REPO_PATH}
|
||||
|
@@ -3,289 +3,107 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/configdrive"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/file"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/ec2"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/proc_cmdline"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/url"
|
||||
"github.com/coreos/coreos-cloudinit/initialize"
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const (
|
||||
version = "0.9.2"
|
||||
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
|
||||
ec2MetadataService string
|
||||
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, "[DEPRECATED - Use -from-ec2-metadata] Download data from metadata service")
|
||||
flag.StringVar(&sources.ec2MetadataService, "from-ec2-metadata", "", "Download data from the provided metadata service")
|
||||
flag.StringVar(&sources.url, "from-url", "", "Download user-data from provided url")
|
||||
flag.BoolVar(&sources.procCmdLine, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=<url>', using the cloud-config served by an HTTP GET to <url>", proc_cmdline.ProcCmdlineLocation, proc_cmdline.ProcCmdlineCloudConfigFlag))
|
||||
flag.StringVar(&convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files (requires the -from-configdrive flag)")
|
||||
flag.StringVar(&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")
|
||||
}
|
||||
const version = "0.6.1"
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
var printVersion bool
|
||||
flag.BoolVar(&printVersion, "version", false, "Print the version and exit")
|
||||
|
||||
die := func() {
|
||||
if ignoreFailure {
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
var ignoreFailure bool
|
||||
flag.BoolVar(&ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")
|
||||
|
||||
var file string
|
||||
flag.StringVar(&file, "from-file", "", "Read user-data from provided file")
|
||||
|
||||
var url string
|
||||
flag.StringVar(&url, "from-url", "", "Download user-data from provided url")
|
||||
|
||||
var useProcCmdline bool
|
||||
flag.BoolVar(&useProcCmdline, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=<url>', using the cloud-config served by an HTTP GET to <url>", datasource.ProcCmdlineLocation, datasource.ProcCmdlineCloudConfigFlag))
|
||||
|
||||
var workspace string
|
||||
flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
|
||||
|
||||
var sshKeyName string
|
||||
flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if printVersion == true {
|
||||
fmt.Printf("coreos-cloudinit version %s\n", version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if convertNetconf != "" && sources.configDrive == "" {
|
||||
fmt.Println("-convert-netconf flag requires -from-configdrive")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch convertNetconf {
|
||||
case "":
|
||||
case "debian":
|
||||
default:
|
||||
fmt.Printf("Invalid option to -convert-netconf: '%s'. Supported options: 'debian'\n", convertNetconf)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dss := getDatasources()
|
||||
if len(dss) == 0 {
|
||||
fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --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())
|
||||
userdataBytes, err := ds.FetchUserdata()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed fetching user-data from datasource: %v\n", err)
|
||||
die()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// Apply environment to user-data
|
||||
env := initialize.NewEnvironment("/", ds.ConfigRoot(), workspace, convertNetconf, sshKeyName, subs)
|
||||
userdata := env.Apply(string(userdataBytes))
|
||||
|
||||
var ccm, ccu *initialize.CloudConfig
|
||||
var script *system.Script
|
||||
if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil {
|
||||
fmt.Printf("Failed to parse meta-data: %v\n", err)
|
||||
die()
|
||||
}
|
||||
if ud, err := initialize.ParseUserData(userdata); err != nil {
|
||||
fmt.Printf("Failed to parse user-data: %v\n", err)
|
||||
die()
|
||||
var ds datasource.Datasource
|
||||
if file != "" {
|
||||
ds = datasource.NewLocalFile(file)
|
||||
} else if url != "" {
|
||||
ds = datasource.NewMetadataService(url)
|
||||
} else if useProcCmdline {
|
||||
ds = datasource.NewProcCmdline()
|
||||
} else {
|
||||
switch t := ud.(type) {
|
||||
case *initialize.CloudConfig:
|
||||
ccu = t
|
||||
case system.Script:
|
||||
script = &t
|
||||
}
|
||||
fmt.Println("Provide one of --from-file, --from-url or --from-proc-cmdline")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var cc *initialize.CloudConfig
|
||||
if ccm != nil && ccu != nil {
|
||||
fmt.Println("Merging cloud-config from meta-data and user-data")
|
||||
merged := mergeCloudConfig(*ccm, *ccu)
|
||||
cc = &merged
|
||||
} else if ccm != nil && ccu == nil {
|
||||
fmt.Println("Processing cloud-config from meta-data")
|
||||
cc = ccm
|
||||
} else if ccm == nil && ccu != nil {
|
||||
fmt.Println("Processing cloud-config from user-data")
|
||||
cc = ccu
|
||||
} else {
|
||||
fmt.Println("No cloud-config data to handle.")
|
||||
}
|
||||
|
||||
if cc != nil {
|
||||
if err = initialize.Apply(*cc, env); err != nil {
|
||||
fmt.Printf("Failed to apply cloud-config: %v\n", err)
|
||||
die()
|
||||
}
|
||||
}
|
||||
|
||||
if script != nil {
|
||||
if err = runScript(*script, env); err != nil {
|
||||
fmt.Printf("Failed to run script: %v\n", err)
|
||||
die()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mergeCloudConfig merges certain options from mdcc (a CloudConfig derived from
|
||||
// meta-data) onto udcc (a CloudConfig derived from user-data), if they are
|
||||
// not already set on udcc (i.e. user-data always takes precedence)
|
||||
// NB: This needs to be kept in sync with ParseMetadata so that it tracks all
|
||||
// elements of a CloudConfig which that function can populate.
|
||||
func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudConfig) {
|
||||
if mdcc.Hostname != "" {
|
||||
if udcc.Hostname != "" {
|
||||
fmt.Printf("Warning: user-data hostname (%s) overrides metadata hostname (%s)\n", udcc.Hostname, mdcc.Hostname)
|
||||
} else {
|
||||
udcc.Hostname = mdcc.Hostname
|
||||
}
|
||||
|
||||
}
|
||||
for _, key := range mdcc.SSHAuthorizedKeys {
|
||||
udcc.SSHAuthorizedKeys = append(udcc.SSHAuthorizedKeys, key)
|
||||
}
|
||||
if mdcc.NetworkConfigPath != "" {
|
||||
if udcc.NetworkConfigPath != "" {
|
||||
fmt.Printf("Warning: user-data NetworkConfigPath %s overrides metadata NetworkConfigPath %s\n", udcc.NetworkConfigPath, mdcc.NetworkConfigPath)
|
||||
} else {
|
||||
udcc.NetworkConfigPath = mdcc.NetworkConfigPath
|
||||
}
|
||||
}
|
||||
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, file.NewDatasource(sources.file))
|
||||
}
|
||||
if sources.url != "" {
|
||||
dss = append(dss, url.NewDatasource(sources.url))
|
||||
}
|
||||
if sources.configDrive != "" {
|
||||
dss = append(dss, configdrive.NewDatasource(sources.configDrive))
|
||||
}
|
||||
if sources.metadataService {
|
||||
dss = append(dss, ec2.NewDatasource(ec2.DefaultAddress))
|
||||
}
|
||||
if sources.ec2MetadataService != "" {
|
||||
dss = append(dss, ec2.NewDatasource(sources.ec2MetadataService))
|
||||
}
|
||||
if sources.procCmdLine {
|
||||
dss = append(dss, proc_cmdline.NewDatasource())
|
||||
}
|
||||
return dss
|
||||
}
|
||||
|
||||
// selectDatasource attempts to choose a valid Datasource to use based on its
|
||||
// current availability. The first Datasource to report to be available is
|
||||
// returned. Datasources will be retried if possible if they are not
|
||||
// immediately available. If all Datasources are permanently unavailable or
|
||||
// datasourceTimeout is reached before one becomes available, nil is returned.
|
||||
func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
||||
ds := make(chan datasource.Datasource)
|
||||
stop := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, s := range sources {
|
||||
wg.Add(1)
|
||||
go func(s datasource.Datasource) {
|
||||
defer wg.Done()
|
||||
|
||||
duration := datasourceInterval
|
||||
for {
|
||||
fmt.Printf("Checking availability of %q\n", s.Type())
|
||||
if s.IsAvailable() {
|
||||
ds <- s
|
||||
return
|
||||
} else if !s.AvailabilityChanges() {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-time.After(duration):
|
||||
duration = pkg.ExpBackoff(duration, datasourceMaxInterval)
|
||||
}
|
||||
}
|
||||
}(s)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
var s datasource.Datasource
|
||||
select {
|
||||
case s = <-ds:
|
||||
case <-done:
|
||||
case <-time.After(datasourceTimeout):
|
||||
}
|
||||
|
||||
close(stop)
|
||||
return s
|
||||
}
|
||||
|
||||
// TODO(jonboulle): this should probably be refactored and moved into a different module
|
||||
func runScript(script system.Script, env *initialize.Environment) error {
|
||||
err := initialize.PrepWorkspace(env.Workspace())
|
||||
log.Printf("Fetching user-data from datasource of type %q", ds.Type())
|
||||
userdataBytes, err := ds.Fetch()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed preparing workspace: %v\n", err)
|
||||
return err
|
||||
log.Printf("Failed fetching user-data from datasource: %v", err)
|
||||
if ignoreFailure {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
path, err := initialize.PersistScriptInWorkspace(script, env.Workspace())
|
||||
if err == nil {
|
||||
var name string
|
||||
name, err = system.ExecuteScript(path)
|
||||
initialize.PersistUnitNameInWorkspace(name, env.Workspace())
|
||||
|
||||
if len(userdataBytes) == 0 {
|
||||
log.Printf("No user data to handle, exiting.")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
env := initialize.NewEnvironment("/", workspace)
|
||||
|
||||
userdata := string(userdataBytes)
|
||||
userdata = env.Apply(userdata)
|
||||
|
||||
parsed, err := initialize.ParseUserData(userdata)
|
||||
if err != nil {
|
||||
log.Printf("Failed parsing user-data: %v", err)
|
||||
if ignoreFailure {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
err = initialize.PrepWorkspace(env.Workspace())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed preparing workspace: %v", err)
|
||||
}
|
||||
|
||||
switch t := parsed.(type) {
|
||||
case initialize.CloudConfig:
|
||||
err = initialize.Apply(t, env)
|
||||
case system.Script:
|
||||
var path string
|
||||
path, err = initialize.PersistScriptInWorkspace(t, env.Workspace())
|
||||
if err == nil {
|
||||
var name string
|
||||
name, err = system.ExecuteScript(path)
|
||||
initialize.PersistUnitNameInWorkspace(name, workspace)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed resolving user-data: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@@ -1,110 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
package configdrive
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
ec2ApiVersion = "2009-04-04"
|
||||
openstackApiVersion = "latest"
|
||||
)
|
||||
|
||||
type configDrive struct {
|
||||
root string
|
||||
readFile func(filename string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *configDrive {
|
||||
return &configDrive{root, ioutil.ReadFile}
|
||||
}
|
||||
|
||||
func (cd *configDrive) IsAvailable() bool {
|
||||
_, err := os.Stat(cd.root)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func (cd *configDrive) AvailabilityChanges() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (cd *configDrive) ConfigRoot() string {
|
||||
return cd.openstackRoot()
|
||||
}
|
||||
|
||||
// FetchMetadata attempts to retrieve metadata from ec2/2009-04-04/meta-data.json.
|
||||
func (cd *configDrive) FetchMetadata() ([]byte, error) {
|
||||
return cd.tryReadFile(path.Join(cd.ec2Root(), "meta-data.json"))
|
||||
}
|
||||
|
||||
// FetchUserdata attempts to retrieve the userdata from ec2/2009-04-04/user-data.
|
||||
// If no data is found, it will attempt to read from openstack/latest/user_data.
|
||||
func (cd *configDrive) FetchUserdata() ([]byte, error) {
|
||||
bytes, err := cd.tryReadFile(path.Join(cd.ec2Root(), "user-data"))
|
||||
if bytes == nil && err == nil {
|
||||
bytes, err = cd.tryReadFile(path.Join(cd.openstackRoot(), "user_data"))
|
||||
}
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
func (cd *configDrive) Type() string {
|
||||
return "cloud-drive"
|
||||
}
|
||||
|
||||
func (cd *configDrive) ec2Root() string {
|
||||
return path.Join(cd.root, "ec2", ec2ApiVersion)
|
||||
}
|
||||
|
||||
func (cd *configDrive) openstackRoot() string {
|
||||
return path.Join(cd.root, "openstack", openstackApiVersion)
|
||||
}
|
||||
|
||||
func (cd *configDrive) tryReadFile(filename string) ([]byte, error) {
|
||||
data, err := cd.readFile(filename)
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
}
|
||||
return data, err
|
||||
}
|
@@ -1,135 +0,0 @@
|
||||
package configdrive
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockFilesystem []string
|
||||
|
||||
func (m mockFilesystem) readFile(filename string) ([]byte, error) {
|
||||
for _, file := range m {
|
||||
if file == filename {
|
||||
return []byte(filename), nil
|
||||
}
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func TestCDFetchMetadata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
filename string
|
||||
files mockFilesystem
|
||||
}{
|
||||
{
|
||||
"/",
|
||||
"",
|
||||
mockFilesystem{},
|
||||
},
|
||||
{
|
||||
"/",
|
||||
"/ec2/2009-04-04/meta-data.json",
|
||||
mockFilesystem([]string{"/ec2/2009-04-04/meta-data.json"}),
|
||||
},
|
||||
{
|
||||
"/media/configdrive",
|
||||
"/media/configdrive/ec2/2009-04-04/meta-data.json",
|
||||
mockFilesystem([]string{"/media/configdrive/ec2/2009-04-04/meta-data.json"}),
|
||||
},
|
||||
} {
|
||||
cd := configDrive{tt.root, tt.files.readFile}
|
||||
filename, err := cd.FetchMetadata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %q: want %q, got %q", tt, nil, err)
|
||||
}
|
||||
if string(filename) != tt.filename {
|
||||
t.Fatalf("bad path for %q: want %q, got %q", tt, tt.filename, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDFetchUserdata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
filename string
|
||||
files mockFilesystem
|
||||
}{
|
||||
{
|
||||
"/",
|
||||
"",
|
||||
mockFilesystem{},
|
||||
},
|
||||
{
|
||||
"/",
|
||||
"/ec2/2009-04-04/user-data",
|
||||
mockFilesystem([]string{"/ec2/2009-04-04/user-data"}),
|
||||
},
|
||||
{
|
||||
"/",
|
||||
"/openstack/latest/user_data",
|
||||
mockFilesystem([]string{"/openstack/latest/user_data"}),
|
||||
},
|
||||
{
|
||||
"/",
|
||||
"/ec2/2009-04-04/user-data",
|
||||
mockFilesystem([]string{"/openstack/latest/user_data", "/ec2/2009-04-04/user-data"}),
|
||||
},
|
||||
{
|
||||
"/media/configdrive",
|
||||
"/media/configdrive/ec2/2009-04-04/user-data",
|
||||
mockFilesystem([]string{"/media/configdrive/ec2/2009-04-04/user-data"}),
|
||||
},
|
||||
} {
|
||||
cd := configDrive{tt.root, tt.files.readFile}
|
||||
filename, err := cd.FetchUserdata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %q: want %q, got %q", tt, nil, err)
|
||||
}
|
||||
if string(filename) != tt.filename {
|
||||
t.Fatalf("bad path for %q: want %q, got %q", tt, tt.filename, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDConfigRoot(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
configRoot string
|
||||
}{
|
||||
{
|
||||
"/",
|
||||
"/openstack/latest",
|
||||
},
|
||||
{
|
||||
"/media/configdrive",
|
||||
"/media/configdrive/openstack/latest",
|
||||
},
|
||||
} {
|
||||
cd := configDrive{tt.root, nil}
|
||||
if configRoot := cd.ConfigRoot(); configRoot != tt.configRoot {
|
||||
t.Fatalf("bad config root for %q: want %q, got %q", tt, tt.configRoot, configRoot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDatasource(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
expectRoot string
|
||||
}{
|
||||
{
|
||||
root: "",
|
||||
expectRoot: "",
|
||||
},
|
||||
{
|
||||
root: "/media/configdrive",
|
||||
expectRoot: "/media/configdrive",
|
||||
},
|
||||
} {
|
||||
service := NewDatasource(tt.root)
|
||||
if service.root != tt.expectRoot {
|
||||
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,15 +1,31 @@
|
||||
package datasource
|
||||
|
||||
const (
|
||||
Ec2ApiVersion = "2009-04-04"
|
||||
OpenstackApiVersion = "2012-08-10"
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Datasource interface {
|
||||
IsAvailable() bool
|
||||
AvailabilityChanges() bool
|
||||
ConfigRoot() string
|
||||
FetchMetadata() ([]byte, error)
|
||||
FetchUserdata() ([]byte, error)
|
||||
Type() string
|
||||
Fetch() ([]byte, error)
|
||||
Type() string
|
||||
}
|
||||
|
||||
func fetchURL(url string) ([]byte, error) {
|
||||
client := http.Client{}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode / 100 != 2 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
21
datasource/file.go
Normal file
21
datasource/file.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type localFile struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func NewLocalFile(path string) *localFile {
|
||||
return &localFile{path}
|
||||
}
|
||||
|
||||
func (self *localFile) Fetch() ([]byte, error) {
|
||||
return ioutil.ReadFile(self.path)
|
||||
}
|
||||
|
||||
func (self *localFile) Type() string {
|
||||
return "local-file"
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
type localFile struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func NewDatasource(path string) *localFile {
|
||||
return &localFile{path}
|
||||
}
|
||||
|
||||
func (f *localFile) IsAvailable() bool {
|
||||
_, err := os.Stat(f.path)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func (f *localFile) AvailabilityChanges() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *localFile) ConfigRoot() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *localFile) FetchMetadata() ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (f *localFile) FetchUserdata() ([]byte, error) {
|
||||
return ioutil.ReadFile(f.path)
|
||||
}
|
||||
|
||||
func (f *localFile) Type() string {
|
||||
return "local-file"
|
||||
}
|
@@ -1,141 +0,0 @@
|
||||
package ec2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAddress = "http://169.254.169.254/"
|
||||
apiVersion = "2009-04-04"
|
||||
userdataUrl = apiVersion + "/user-data"
|
||||
metadataUrl = apiVersion + "/meta-data"
|
||||
)
|
||||
|
||||
type metadataService struct {
|
||||
root string
|
||||
client pkg.Getter
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *metadataService {
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root += "/"
|
||||
}
|
||||
return &metadataService{root, pkg.NewHttpClient()}
|
||||
}
|
||||
|
||||
func (ms metadataService) IsAvailable() bool {
|
||||
_, err := ms.client.Get(ms.root + apiVersion)
|
||||
return (err == nil)
|
||||
}
|
||||
|
||||
func (ms metadataService) AvailabilityChanges() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ms metadataService) ConfigRoot() string {
|
||||
return ms.root
|
||||
}
|
||||
|
||||
func (ms metadataService) FetchMetadata() ([]byte, error) {
|
||||
attrs := make(map[string]interface{})
|
||||
if keynames, err := fetchAttributes(ms.client, fmt.Sprintf("%s/public-keys", ms.metadataUrl())); err == nil {
|
||||
keyIDs := make(map[string]string)
|
||||
for _, keyname := range keynames {
|
||||
tokens := strings.SplitN(keyname, "=", 2)
|
||||
if len(tokens) != 2 {
|
||||
return nil, fmt.Errorf("malformed public key: %q", keyname)
|
||||
}
|
||||
keyIDs[tokens[1]] = tokens[0]
|
||||
}
|
||||
|
||||
keys := make(map[string]string)
|
||||
for name, id := range keyIDs {
|
||||
sshkey, err := fetchAttribute(ms.client, fmt.Sprintf("%s/public-keys/%s/openssh-key", ms.metadataUrl(), id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys[name] = sshkey
|
||||
fmt.Printf("Found SSH key for %q\n", name)
|
||||
}
|
||||
attrs["public_keys"] = keys
|
||||
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hostname, err := fetchAttribute(ms.client, fmt.Sprintf("%s/hostname", ms.metadataUrl())); err == nil {
|
||||
attrs["hostname"] = hostname
|
||||
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if localAddr, err := fetchAttribute(ms.client, fmt.Sprintf("%s/local-ipv4", ms.metadataUrl())); err == nil {
|
||||
attrs["local-ipv4"] = localAddr
|
||||
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if publicAddr, err := fetchAttribute(ms.client, fmt.Sprintf("%s/public-ipv4", ms.metadataUrl())); err == nil {
|
||||
attrs["public-ipv4"] = publicAddr
|
||||
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if content_path, err := fetchAttribute(ms.client, fmt.Sprintf("%s/network_config/content_path", ms.metadataUrl())); err == nil {
|
||||
attrs["network_config"] = map[string]string{
|
||||
"content_path": content_path,
|
||||
}
|
||||
} else if _, ok := err.(pkg.ErrNotFound); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(attrs)
|
||||
}
|
||||
|
||||
func (ms metadataService) FetchUserdata() ([]byte, error) {
|
||||
if data, err := ms.client.GetRetry(ms.userdataUrl()); err == nil {
|
||||
return data, err
|
||||
} else if _, ok := err.(pkg.ErrNotFound); ok {
|
||||
return []byte{}, nil
|
||||
} else {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
func (ms metadataService) Type() string {
|
||||
return "ec2-metadata-service"
|
||||
}
|
||||
|
||||
func (ms metadataService) metadataUrl() string {
|
||||
return (ms.root + metadataUrl)
|
||||
}
|
||||
|
||||
func (ms metadataService) userdataUrl() string {
|
||||
return (ms.root + userdataUrl)
|
||||
}
|
||||
|
||||
func fetchAttributes(client pkg.Getter, url string) ([]string, error) {
|
||||
resp, err := client.GetRetry(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scanner := bufio.NewScanner(bytes.NewBuffer(resp))
|
||||
data := make([]string, 0)
|
||||
for scanner.Scan() {
|
||||
data = append(data, scanner.Text())
|
||||
}
|
||||
return data, scanner.Err()
|
||||
}
|
||||
|
||||
func fetchAttribute(client pkg.Getter, url string) (string, error) {
|
||||
if attrs, err := fetchAttributes(client, url); err == nil && len(attrs) > 0 {
|
||||
return attrs[0], nil
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
@@ -1,324 +0,0 @@
|
||||
package ec2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
type testHttpClient struct {
|
||||
resources map[string]string
|
||||
err error
|
||||
}
|
||||
|
||||
func (t *testHttpClient) GetRetry(url string) ([]byte, error) {
|
||||
if t.err != nil {
|
||||
return nil, t.err
|
||||
}
|
||||
if val, ok := t.resources[url]; ok {
|
||||
return []byte(val), nil
|
||||
} else {
|
||||
return nil, pkg.ErrNotFound{fmt.Errorf("not found: %q", url)}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *testHttpClient) Get(url string) ([]byte, error) {
|
||||
return t.GetRetry(url)
|
||||
}
|
||||
|
||||
func TestAvailabilityChanges(t *testing.T) {
|
||||
want := true
|
||||
if ac := (metadataService{}).AvailabilityChanges(); ac != want {
|
||||
t.Fatalf("bad AvailabilityChanges: want %q, got %q", want, ac)
|
||||
}
|
||||
}
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
want := "ec2-metadata-service"
|
||||
if kind := (metadataService{}).Type(); kind != want {
|
||||
t.Fatalf("bad type: want %q, got %q", want, kind)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAvailable(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
resources map[string]string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
resources: map[string]string{
|
||||
"/2009-04-04": "",
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
resources: map[string]string{},
|
||||
expect: false,
|
||||
},
|
||||
} {
|
||||
service := &metadataService{tt.root, &testHttpClient{tt.resources, nil}}
|
||||
if a := service.IsAvailable(); a != tt.expect {
|
||||
t.Fatalf("bad isAvailable (%q): want %q, got %q", tt.resources, tt.expect, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchUserdata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
resources map[string]string
|
||||
userdata []byte
|
||||
clientErr error
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
resources: map[string]string{
|
||||
"/2009-04-04/user-data": "hello",
|
||||
},
|
||||
userdata: []byte("hello"),
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
clientErr: pkg.ErrNotFound{fmt.Errorf("test not found error")},
|
||||
userdata: []byte{},
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
clientErr: pkg.ErrTimeout{fmt.Errorf("test timeout error")},
|
||||
expectErr: pkg.ErrTimeout{fmt.Errorf("test timeout error")},
|
||||
},
|
||||
} {
|
||||
service := &metadataService{tt.root, &testHttpClient{tt.resources, tt.clientErr}}
|
||||
data, err := service.FetchUserdata()
|
||||
if Error(err) != Error(tt.expectErr) {
|
||||
t.Fatalf("bad error (%q): want %q, got %q", tt.resources, tt.expectErr, err)
|
||||
}
|
||||
if !bytes.Equal(data, tt.userdata) {
|
||||
t.Fatalf("bad userdata (%q): want %q, got %q", tt.resources, tt.userdata, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrls(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
expectRoot string
|
||||
userdata string
|
||||
metadata string
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
expectRoot: "/",
|
||||
userdata: "/2009-04-04/user-data",
|
||||
metadata: "/2009-04-04/meta-data",
|
||||
},
|
||||
{
|
||||
root: "http://169.254.169.254/",
|
||||
expectRoot: "http://169.254.169.254/",
|
||||
userdata: "http://169.254.169.254/2009-04-04/user-data",
|
||||
metadata: "http://169.254.169.254/2009-04-04/meta-data",
|
||||
},
|
||||
} {
|
||||
service := &metadataService{tt.root, nil}
|
||||
if url := service.userdataUrl(); url != tt.userdata {
|
||||
t.Fatalf("bad url (%q): want %q, got %q", tt.root, tt.userdata, url)
|
||||
}
|
||||
if url := service.metadataUrl(); url != tt.metadata {
|
||||
t.Fatalf("bad url (%q): want %q, got %q", tt.root, tt.metadata, url)
|
||||
}
|
||||
if url := service.ConfigRoot(); url != tt.expectRoot {
|
||||
t.Fatalf("bad url (%q): want %q, got %q", tt.root, tt.expectRoot, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchAttributes(t *testing.T) {
|
||||
for _, s := range []struct {
|
||||
resources map[string]string
|
||||
err error
|
||||
tests []struct {
|
||||
path string
|
||||
val []string
|
||||
}
|
||||
}{
|
||||
{
|
||||
resources: map[string]string{
|
||||
"/": "a\nb\nc/",
|
||||
"/c/": "d\ne/",
|
||||
"/c/e/": "f",
|
||||
"/a": "1",
|
||||
"/b": "2",
|
||||
"/c/d": "3",
|
||||
"/c/e/f": "4",
|
||||
},
|
||||
tests: []struct {
|
||||
path string
|
||||
val []string
|
||||
}{
|
||||
{"/", []string{"a", "b", "c/"}},
|
||||
{"/b", []string{"2"}},
|
||||
{"/c/d", []string{"3"}},
|
||||
{"/c/e/", []string{"f"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
err: pkg.ErrNotFound{fmt.Errorf("test error")},
|
||||
tests: []struct {
|
||||
path string
|
||||
val []string
|
||||
}{
|
||||
{"", nil},
|
||||
},
|
||||
},
|
||||
} {
|
||||
client := &testHttpClient{s.resources, s.err}
|
||||
for _, tt := range s.tests {
|
||||
attrs, err := fetchAttributes(client, tt.path)
|
||||
if err != s.err {
|
||||
t.Fatalf("bad error for %q (%q): want %q, got %q", tt.path, s.resources, s.err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(attrs, tt.val) {
|
||||
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.resources, tt.val, attrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchAttribute(t *testing.T) {
|
||||
for _, s := range []struct {
|
||||
resources map[string]string
|
||||
err error
|
||||
tests []struct {
|
||||
path string
|
||||
val string
|
||||
}
|
||||
}{
|
||||
{
|
||||
resources: map[string]string{
|
||||
"/": "a\nb\nc/",
|
||||
"/c/": "d\ne/",
|
||||
"/c/e/": "f",
|
||||
"/a": "1",
|
||||
"/b": "2",
|
||||
"/c/d": "3",
|
||||
"/c/e/f": "4",
|
||||
},
|
||||
tests: []struct {
|
||||
path string
|
||||
val string
|
||||
}{
|
||||
{"/a", "1"},
|
||||
{"/b", "2"},
|
||||
{"/c/d", "3"},
|
||||
{"/c/e/f", "4"},
|
||||
},
|
||||
},
|
||||
{
|
||||
err: pkg.ErrNotFound{fmt.Errorf("test error")},
|
||||
tests: []struct {
|
||||
path string
|
||||
val string
|
||||
}{
|
||||
{"", ""},
|
||||
},
|
||||
},
|
||||
} {
|
||||
client := &testHttpClient{s.resources, s.err}
|
||||
for _, tt := range s.tests {
|
||||
attr, err := fetchAttribute(client, tt.path)
|
||||
if err != s.err {
|
||||
t.Fatalf("bad error for %q (%q): want %q, got %q", tt.path, s.resources, s.err, err)
|
||||
}
|
||||
if attr != tt.val {
|
||||
t.Fatalf("bad fetch for %q (%q): want %q, got %q", tt.path, s.resources, tt.val, attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchMetadata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
resources map[string]string
|
||||
expect []byte
|
||||
clientErr error
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
resources: map[string]string{
|
||||
"/2009-04-04/meta-data/public-keys": "bad\n",
|
||||
},
|
||||
expectErr: fmt.Errorf("malformed public key: \"bad\""),
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
resources: map[string]string{
|
||||
"/2009-04-04/meta-data/hostname": "host",
|
||||
"/2009-04-04/meta-data/local-ipv4": "1.2.3.4",
|
||||
"/2009-04-04/meta-data/public-ipv4": "5.6.7.8",
|
||||
"/2009-04-04/meta-data/public-keys": "0=test1\n",
|
||||
"/2009-04-04/meta-data/public-keys/0": "openssh-key",
|
||||
"/2009-04-04/meta-data/public-keys/0/openssh-key": "key",
|
||||
"/2009-04-04/meta-data/network_config/content_path": "path",
|
||||
},
|
||||
expect: []byte(`{"hostname":"host","local-ipv4":"1.2.3.4","network_config":{"content_path":"path"},"public-ipv4":"5.6.7.8","public_keys":{"test1":"key"}}`),
|
||||
},
|
||||
{
|
||||
clientErr: pkg.ErrTimeout{fmt.Errorf("test error")},
|
||||
expectErr: pkg.ErrTimeout{fmt.Errorf("test error")},
|
||||
},
|
||||
} {
|
||||
service := &metadataService{tt.root, &testHttpClient{tt.resources, tt.clientErr}}
|
||||
metadata, err := service.FetchMetadata()
|
||||
if Error(err) != Error(tt.expectErr) {
|
||||
t.Fatalf("bad error (%q): want %q, got %q", tt.resources, tt.expectErr, err)
|
||||
}
|
||||
if !bytes.Equal(metadata, tt.expect) {
|
||||
t.Fatalf("bad fetch (%q): want %q, got %q", tt.resources, tt.expect, metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDatasource(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
expectRoot string
|
||||
}{
|
||||
{
|
||||
root: "",
|
||||
expectRoot: "/",
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
expectRoot: "/",
|
||||
},
|
||||
{
|
||||
root: "http://169.254.169.254",
|
||||
expectRoot: "http://169.254.169.254/",
|
||||
},
|
||||
{
|
||||
root: "http://169.254.169.254/",
|
||||
expectRoot: "http://169.254.169.254/",
|
||||
},
|
||||
} {
|
||||
service := NewDatasource(tt.root)
|
||||
if service.root != tt.expectRoot {
|
||||
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Error(err error) string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
17
datasource/metadata_service.go
Normal file
17
datasource/metadata_service.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package datasource
|
||||
|
||||
type metadataService struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func NewMetadataService(url string) *metadataService {
|
||||
return &metadataService{url}
|
||||
}
|
||||
|
||||
func (ms *metadataService) Fetch() ([]byte, error) {
|
||||
return fetchURL(ms.url)
|
||||
}
|
||||
|
||||
func (ms *metadataService) Type() string {
|
||||
return "metadata-service"
|
||||
}
|
66
datasource/proc_cmdline.go
Normal file
66
datasource/proc_cmdline.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ProcCmdlineLocation = "/proc/cmdline"
|
||||
ProcCmdlineCloudConfigFlag = "cloud-config-url"
|
||||
)
|
||||
|
||||
type procCmdline struct{}
|
||||
|
||||
func NewProcCmdline() *procCmdline {
|
||||
return &procCmdline{}
|
||||
}
|
||||
|
||||
func (self *procCmdline) Fetch() ([]byte, error) {
|
||||
cmdline, err := ioutil.ReadFile(ProcCmdlineLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url, err := findCloudConfigURL(string(cmdline))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := fetchURL(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (self *procCmdline) Type() string {
|
||||
return "proc-cmdline"
|
||||
}
|
||||
|
||||
func findCloudConfigURL(input string) (url string, err error) {
|
||||
err = errors.New("cloud-config-url not found")
|
||||
for _, token := range strings.Split(input, " ") {
|
||||
parts := strings.SplitN(token, "=", 2)
|
||||
|
||||
key := parts[0]
|
||||
key = strings.Replace(key, "_", "-", -1)
|
||||
|
||||
if key != "cloud-config-url" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(parts) != 2 {
|
||||
log.Printf("Found cloud-config-url in /proc/cmdline with no value, ignoring.")
|
||||
continue
|
||||
}
|
||||
|
||||
url = parts[1]
|
||||
err = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@@ -1,95 +0,0 @@
|
||||
package proc_cmdline
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
)
|
||||
|
||||
const (
|
||||
ProcCmdlineLocation = "/proc/cmdline"
|
||||
ProcCmdlineCloudConfigFlag = "cloud-config-url"
|
||||
)
|
||||
|
||||
type procCmdline struct {
|
||||
Location string
|
||||
}
|
||||
|
||||
func NewDatasource() *procCmdline {
|
||||
return &procCmdline{Location: ProcCmdlineLocation}
|
||||
}
|
||||
|
||||
func (c *procCmdline) IsAvailable() bool {
|
||||
contents, err := ioutil.ReadFile(c.Location)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
cmdline := strings.TrimSpace(string(contents))
|
||||
_, err = findCloudConfigURL(cmdline)
|
||||
return (err == nil)
|
||||
}
|
||||
|
||||
func (c *procCmdline) AvailabilityChanges() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *procCmdline) ConfigRoot() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *procCmdline) FetchMetadata() ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (c *procCmdline) FetchUserdata() ([]byte, error) {
|
||||
contents, err := ioutil.ReadFile(c.Location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmdline := strings.TrimSpace(string(contents))
|
||||
url, err := findCloudConfigURL(cmdline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := pkg.NewHttpClient()
|
||||
cfg, err := client.GetRetry(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c *procCmdline) Type() string {
|
||||
return "proc-cmdline"
|
||||
}
|
||||
|
||||
func findCloudConfigURL(input string) (url string, err error) {
|
||||
err = errors.New("cloud-config-url not found")
|
||||
for _, token := range strings.Split(input, " ") {
|
||||
parts := strings.SplitN(token, "=", 2)
|
||||
|
||||
key := parts[0]
|
||||
key = strings.Replace(key, "_", "-", -1)
|
||||
|
||||
if key != "cloud-config-url" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(parts) != 2 {
|
||||
log.Printf("Found cloud-config-url in /proc/cmdline with no value, ignoring.")
|
||||
continue
|
||||
}
|
||||
|
||||
url = parts[1]
|
||||
err = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@@ -1,88 +0,0 @@
|
||||
package proc_cmdline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCmdlineCloudConfigFound(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
"cloud-config-url=example.com",
|
||||
"example.com",
|
||||
},
|
||||
{
|
||||
"cloud_config_url=example.com",
|
||||
"example.com",
|
||||
},
|
||||
{
|
||||
"cloud-config-url cloud-config-url=example.com",
|
||||
"example.com",
|
||||
},
|
||||
{
|
||||
"cloud-config-url= cloud-config-url=example.com",
|
||||
"example.com",
|
||||
},
|
||||
{
|
||||
"cloud-config-url=one.example.com cloud-config-url=two.example.com",
|
||||
"two.example.com",
|
||||
},
|
||||
{
|
||||
"foo=bar cloud-config-url=example.com ping=pong",
|
||||
"example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
output, err := findCloudConfigURL(tt.input)
|
||||
if output != tt.expect {
|
||||
t.Errorf("Test case %d failed: %s != %s", i, output, tt.expect)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Test case %d produced error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcCmdlineAndFetchConfig(t *testing.T) {
|
||||
|
||||
var (
|
||||
ProcCmdlineTmpl = "foo=bar cloud-config-url=%s/config\n"
|
||||
CloudConfigContent = "#cloud-config\n"
|
||||
)
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" && r.RequestURI == "/config" {
|
||||
fmt.Fprint(w, CloudConfigContent)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
file, err := ioutil.TempFile(os.TempDir(), "test_proc_cmdline")
|
||||
defer os.Remove(file.Name())
|
||||
if err != nil {
|
||||
t.Errorf("Test produced error: %v", err)
|
||||
}
|
||||
_, err = file.Write([]byte(fmt.Sprintf(ProcCmdlineTmpl, ts.URL)))
|
||||
if err != nil {
|
||||
t.Errorf("Test produced error: %v", err)
|
||||
}
|
||||
|
||||
p := NewDatasource()
|
||||
p.Location = file.Name()
|
||||
cfg, err := p.FetchUserdata()
|
||||
if err != nil {
|
||||
t.Errorf("Test produced error: %v", err)
|
||||
}
|
||||
|
||||
if string(cfg) != CloudConfigContent {
|
||||
t.Errorf("Test failed, response body: %s != %s", cfg, CloudConfigContent)
|
||||
}
|
||||
}
|
47
datasource/proc_cmdline_test.go
Normal file
47
datasource/proc_cmdline_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCmdlineCloudConfigFound(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
"cloud-config-url=example.com",
|
||||
"example.com",
|
||||
},
|
||||
{
|
||||
"cloud_config_url=example.com",
|
||||
"example.com",
|
||||
},
|
||||
{
|
||||
"cloud-config-url cloud-config-url=example.com",
|
||||
"example.com",
|
||||
},
|
||||
{
|
||||
"cloud-config-url= cloud-config-url=example.com",
|
||||
"example.com",
|
||||
},
|
||||
{
|
||||
"cloud-config-url=one.example.com cloud-config-url=two.example.com",
|
||||
"two.example.com",
|
||||
},
|
||||
{
|
||||
"foo=bar cloud-config-url=example.com ping=pong",
|
||||
"example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
output, err := findCloudConfigURL(tt.input)
|
||||
if output != tt.expect {
|
||||
t.Errorf("Test case %d failed: %s != %s", i, output, tt.expect)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Test case %d produced error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
package url
|
||||
|
||||
import "github.com/coreos/coreos-cloudinit/pkg"
|
||||
|
||||
type remoteFile struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func NewDatasource(url string) *remoteFile {
|
||||
return &remoteFile{url}
|
||||
}
|
||||
|
||||
func (f *remoteFile) IsAvailable() bool {
|
||||
client := pkg.NewHttpClient()
|
||||
_, err := client.Get(f.url)
|
||||
return (err == nil)
|
||||
}
|
||||
|
||||
func (f *remoteFile) AvailabilityChanges() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *remoteFile) ConfigRoot() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *remoteFile) FetchMetadata() ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (f *remoteFile) FetchUserdata() ([]byte, error) {
|
||||
client := pkg.NewHttpClient()
|
||||
return client.GetRetry(f.url)
|
||||
}
|
||||
|
||||
func (f *remoteFile) Type() string {
|
||||
return "url"
|
||||
}
|
@@ -1,144 +1,33 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/network"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
// CloudConfigFile represents a CoreOS specific configuration option that can generate
|
||||
// an associated system.File to be written to disk
|
||||
type CloudConfigFile interface {
|
||||
// File should either return (*system.File, error), or (nil, nil) if nothing
|
||||
// needs to be done for this configuration option.
|
||||
File(root string) (*system.File, error)
|
||||
}
|
||||
|
||||
// CloudConfigUnit represents a CoreOS specific configuration option that can generate
|
||||
// associated system.Units to be created/enabled appropriately
|
||||
type CloudConfigUnit interface {
|
||||
Units(root string) ([]system.Unit, error)
|
||||
}
|
||||
|
||||
// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
|
||||
type CloudConfig struct {
|
||||
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
|
||||
Coreos struct {
|
||||
Etcd EtcdEnvironment
|
||||
Fleet FleetEnvironment
|
||||
OEM OEMRelease
|
||||
Update UpdateConfig
|
||||
Update map[string]string
|
||||
Units []system.Unit
|
||||
OEM OEMRelease
|
||||
}
|
||||
WriteFiles []system.File `yaml:"write_files"`
|
||||
Hostname string
|
||||
Users []system.User
|
||||
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
|
||||
NetworkConfigPath string
|
||||
WriteFiles []system.File `yaml:"write_files"`
|
||||
Hostname string
|
||||
Users []system.User
|
||||
ManageEtcHosts string `yaml:"manage_etc_hosts"`
|
||||
}
|
||||
|
||||
type warner func(format string, v ...interface{})
|
||||
|
||||
// warnOnUnrecognizedKeys parses the contents of a cloud-config file and calls
|
||||
// warn(msg, key) for every unrecognized key (i.e. those not present in CloudConfig)
|
||||
func warnOnUnrecognizedKeys(contents string, warn warner) {
|
||||
// Generate a map of all understood cloud config options
|
||||
var cc map[string]interface{}
|
||||
b, _ := goyaml.Marshal(&CloudConfig{})
|
||||
goyaml.Unmarshal(b, &cc)
|
||||
|
||||
// Now unmarshal the entire provided contents
|
||||
var c map[string]interface{}
|
||||
goyaml.Unmarshal([]byte(contents), &c)
|
||||
|
||||
// Check that every key in the contents exists in the cloud config
|
||||
for k, _ := range c {
|
||||
if _, ok := cc[k]; !ok {
|
||||
warn("Warning: unrecognized key %q in provided cloud config - ignoring section", k)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unrecognized coreos options, if any are set
|
||||
if coreos, ok := c["coreos"]; ok {
|
||||
if set, ok := coreos.(map[interface{}]interface{}); ok {
|
||||
known := cc["coreos"].(map[interface{}]interface{})
|
||||
for k, _ := range set {
|
||||
if key, ok := k.(string); ok {
|
||||
if _, ok := known[key]; !ok {
|
||||
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", key)
|
||||
}
|
||||
} else {
|
||||
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any badly-specified users, if any are set
|
||||
if users, ok := c["users"]; ok {
|
||||
var known map[string]interface{}
|
||||
b, _ := goyaml.Marshal(&system.User{})
|
||||
goyaml.Unmarshal(b, &known)
|
||||
|
||||
if set, ok := users.([]interface{}); ok {
|
||||
for _, u := range set {
|
||||
if user, ok := u.(map[interface{}]interface{}); ok {
|
||||
for k, _ := range user {
|
||||
if key, ok := k.(string); ok {
|
||||
if _, ok := known[key]; !ok {
|
||||
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", key)
|
||||
}
|
||||
} else {
|
||||
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any badly-specified files, if any are set
|
||||
if files, ok := c["write_files"]; ok {
|
||||
var known map[string]interface{}
|
||||
b, _ := goyaml.Marshal(&system.File{})
|
||||
goyaml.Unmarshal(b, &known)
|
||||
|
||||
if set, ok := files.([]interface{}); ok {
|
||||
for _, f := range set {
|
||||
if file, ok := f.(map[interface{}]interface{}); ok {
|
||||
for k, _ := range file {
|
||||
if key, ok := k.(string); ok {
|
||||
if _, ok := known[key]; !ok {
|
||||
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", key)
|
||||
}
|
||||
} else {
|
||||
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewCloudConfig instantiates a new CloudConfig from the given contents (a
|
||||
// string of YAML), returning any error encountered. It will ignore unknown
|
||||
// fields but log encountering them.
|
||||
func NewCloudConfig(contents string) (*CloudConfig, error) {
|
||||
var cfg CloudConfig
|
||||
err := goyaml.Unmarshal([]byte(contents), &cfg)
|
||||
if err != nil {
|
||||
return &cfg, err
|
||||
}
|
||||
warnOnUnrecognizedKeys(contents, log.Printf)
|
||||
return &cfg, nil
|
||||
return &cfg, err
|
||||
}
|
||||
|
||||
func (cc CloudConfig) String() string {
|
||||
@@ -153,9 +42,6 @@ func (cc CloudConfig) String() string {
|
||||
return stringified
|
||||
}
|
||||
|
||||
// Apply renders a CloudConfig to an Environment. This can involve things like
|
||||
// configuring the hostname, adding new users, writing various configuration
|
||||
// files to disk, and manipulating systemd services.
|
||||
func Apply(cfg CloudConfig, env *Environment) error {
|
||||
if cfg.Hostname != "" {
|
||||
if err := system.SetHostname(cfg.Hostname); err != nil {
|
||||
@@ -164,45 +50,54 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
log.Printf("Set hostname to %s", cfg.Hostname)
|
||||
}
|
||||
|
||||
for _, user := range cfg.Users {
|
||||
if user.Name == "" {
|
||||
log.Printf("User object has no 'name' field, skipping")
|
||||
continue
|
||||
if cfg.Coreos.OEM.ID != "" {
|
||||
if err := WriteOEMRelease(&cfg.Coreos.OEM, env.Root()); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Wrote /etc/oem-release to filesystem")
|
||||
}
|
||||
|
||||
if system.UserExists(&user) {
|
||||
log.Printf("User '%s' exists, ignoring creation-time fields", user.Name)
|
||||
if user.PasswordHash != "" {
|
||||
log.Printf("Setting '%s' user's password", user.Name)
|
||||
if err := system.SetUserPassword(user.Name, user.PasswordHash); err != nil {
|
||||
log.Printf("Failed setting '%s' user's password: %v", user.Name, err)
|
||||
if len(cfg.Users) > 0 {
|
||||
for _, user := range cfg.Users {
|
||||
if user.Name == "" {
|
||||
log.Printf("User object has no 'name' field, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
if system.UserExists(&user) {
|
||||
log.Printf("User '%s' exists, ignoring creation-time fields", user.Name)
|
||||
if user.PasswordHash != "" {
|
||||
log.Printf("Setting '%s' user's password", user.Name)
|
||||
if err := system.SetUserPassword(user.Name, user.PasswordHash); err != nil {
|
||||
log.Printf("Failed setting '%s' user's password: %v", user.Name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("Creating user '%s'", user.Name)
|
||||
if err := system.CreateUser(&user); err != nil {
|
||||
log.Printf("Failed creating user '%s': %v", user.Name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("Creating user '%s'", user.Name)
|
||||
if err := system.CreateUser(&user); err != nil {
|
||||
log.Printf("Failed creating user '%s': %v", user.Name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(user.SSHAuthorizedKeys) > 0 {
|
||||
log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name)
|
||||
if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil {
|
||||
return err
|
||||
if len(user.SSHAuthorizedKeys) > 0 {
|
||||
log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name)
|
||||
if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if user.SSHImportGithubUser != "" {
|
||||
log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name)
|
||||
if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil {
|
||||
return err
|
||||
if user.SSHImportGithubUser != "" {
|
||||
log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name)
|
||||
if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if user.SSHImportURL != "" {
|
||||
log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL)
|
||||
if err := SSHImportKeysFromURL(user.Name, user.SSHImportURL); err != nil {
|
||||
return err
|
||||
if user.SSHImportURL != "" {
|
||||
log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL)
|
||||
if err := SSHImportKeysFromURL(user.Name, user.SSHImportURL); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,140 +111,86 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, ccf := range []CloudConfigFile{cfg.Coreos.OEM, cfg.Coreos.Update, cfg.ManageEtcHosts} {
|
||||
f, err := ccf.File(env.Root())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f != nil {
|
||||
cfg.WriteFiles = append(cfg.WriteFiles, *f)
|
||||
if len(cfg.WriteFiles) > 0 {
|
||||
for _, file := range cfg.WriteFiles {
|
||||
file.Path = path.Join(env.Root(), file.Path)
|
||||
if err := system.WriteFile(&file); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Wrote file %s to filesystem", file.Path)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
|
||||
u, err := ccu.Units(env.Root())
|
||||
if err != nil {
|
||||
return err
|
||||
if len(cfg.Coreos.Etcd) > 0 {
|
||||
if err := WriteEtcdEnvironment(cfg.Coreos.Etcd, env.Root()); err != nil {
|
||||
log.Fatalf("Failed to write etcd config to filesystem: %v", err)
|
||||
}
|
||||
cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
|
||||
|
||||
log.Printf("Wrote etcd config file to filesystem")
|
||||
}
|
||||
|
||||
wroteEnvironment := false
|
||||
for _, file := range cfg.WriteFiles {
|
||||
fullPath, err := system.WriteFile(&file, env.Root())
|
||||
if err != nil {
|
||||
return err
|
||||
if s, ok := cfg.Coreos.Update["reboot-strategy"]; ok {
|
||||
if err := WriteLocksmithConfig(s, env.Root()); err != nil {
|
||||
log.Fatalf("Failed to write locksmith config to filesystem: %v", err)
|
||||
}
|
||||
if path.Clean(file.Path) == "/etc/environment" {
|
||||
wroteEnvironment = true
|
||||
}
|
||||
log.Printf("Wrote file %s to filesystem", fullPath)
|
||||
log.Printf("Wrote locksmith config file to filesystem")
|
||||
}
|
||||
|
||||
if !wroteEnvironment {
|
||||
ef := env.DefaultEnvironmentFile()
|
||||
if ef != nil {
|
||||
err := system.WriteEnvFile(ef, env.Root())
|
||||
if len(cfg.Coreos.Units) > 0 {
|
||||
commands := make(map[string]string, 0)
|
||||
for _, unit := range cfg.Coreos.Units {
|
||||
dst := system.UnitDestination(&unit, env.Root())
|
||||
if unit.Content != "" {
|
||||
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
|
||||
if err := system.PlaceUnit(&unit, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Placed unit %s at %s", unit.Name, dst)
|
||||
}
|
||||
|
||||
if unit.Enable {
|
||||
if unit.Group() != "network" {
|
||||
log.Printf("Enabling unit file %s", dst)
|
||||
if err := system.EnableUnitFile(dst, unit.Runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Enabled unit %s", unit.Name)
|
||||
} else {
|
||||
log.Printf("Skipping enable for network-like unit %s", unit.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if unit.Group() == "network" {
|
||||
commands["systemd-networkd.service"] = "restart"
|
||||
} else {
|
||||
if unit.Command != "" {
|
||||
commands[unit.Name] = unit.Command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := system.DaemonReload(); err != nil {
|
||||
log.Fatalf("Failed systemd daemon-reload: %v", err)
|
||||
}
|
||||
|
||||
for unit, command := range commands {
|
||||
log.Printf("Calling unit command '%s %s'", command, unit)
|
||||
res, err := system.RunUnitCommand(command, unit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Updated /etc/environment")
|
||||
log.Printf("Result of '%s %s': %s", command, unit, res)
|
||||
}
|
||||
}
|
||||
|
||||
if env.NetconfType() != "" {
|
||||
netconfBytes, err := ioutil.ReadFile(path.Join(env.ConfigRoot(), cfg.NetworkConfigPath))
|
||||
if err != nil {
|
||||
return err
|
||||
if cfg.ManageEtcHosts != "" {
|
||||
|
||||
if err := WriteEtcHosts(cfg.ManageEtcHosts, env.Root()); err != nil {
|
||||
log.Fatalf("Failed to write /etc/hosts to filesystem: %v", 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())
|
||||
}
|
||||
log.Printf("Wrote /etc/hosts file to filesystem")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := system.WriteNetworkdConfigs(interfaces); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := system.RestartNetwork(interfaces); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
um := system.NewUnitManager(env.Root())
|
||||
return processUnits(cfg.Coreos.Units, env.Root(), um)
|
||||
|
||||
}
|
||||
|
||||
// processUnits takes a set of Units and applies them to the given root using
|
||||
// the given UnitManager. This can involve things like writing unit files to
|
||||
// disk, masking/unmasking units, or invoking systemd
|
||||
// commands against units. It returns any error encountered.
|
||||
func processUnits(units []system.Unit, root string, um system.UnitManager) error {
|
||||
commands := make(map[string]string, 0)
|
||||
reload := false
|
||||
for _, unit := range units {
|
||||
dst := unit.Destination(root)
|
||||
if unit.Content != "" {
|
||||
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
|
||||
if err := um.PlaceUnit(&unit, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Placed unit %s at %s", unit.Name, dst)
|
||||
reload = true
|
||||
}
|
||||
|
||||
if unit.Mask {
|
||||
log.Printf("Masking unit file %s", unit.Name)
|
||||
if err := um.MaskUnit(&unit); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if unit.Runtime {
|
||||
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
|
||||
if err := um.UnmaskUnit(&unit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if unit.Enable {
|
||||
if unit.Group() != "network" {
|
||||
log.Printf("Enabling unit file %s", unit.Name)
|
||||
if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Enabled unit %s", unit.Name)
|
||||
} else {
|
||||
log.Printf("Skipping enable for network-like unit %s", unit.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if unit.Group() == "network" {
|
||||
commands["systemd-networkd.service"] = "restart"
|
||||
} else if unit.Command != "" {
|
||||
commands[unit.Name] = unit.Command
|
||||
}
|
||||
}
|
||||
|
||||
if reload {
|
||||
if err := um.DaemonReload(); err != nil {
|
||||
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
for unit, command := range commands {
|
||||
log.Printf("Calling unit command '%s %s'", command, unit)
|
||||
res, err := um.RunUnitCommand(command, unit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Result of '%s %s': %s", command, unit, res)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@@ -1,105 +1,10 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestCloudConfigInvalidKeys(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("panic while instantiating CloudConfig with nil keys: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, tt := range []struct {
|
||||
contents string
|
||||
}{
|
||||
{"coreos:"},
|
||||
{"ssh_authorized_keys:"},
|
||||
{"ssh_authorized_keys:\n -"},
|
||||
{"ssh_authorized_keys:\n - 0:"},
|
||||
{"write_files:"},
|
||||
{"write_files:\n -"},
|
||||
{"write_files:\n - 0:"},
|
||||
{"users:"},
|
||||
{"users:\n -"},
|
||||
{"users:\n - 0:"},
|
||||
} {
|
||||
_, err := NewCloudConfig(tt.contents)
|
||||
if err != nil {
|
||||
t.Fatalf("error instantiating CloudConfig with invalid keys: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUnknownKeys(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
etcd:
|
||||
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
||||
coreos_unknown:
|
||||
foo: "bar"
|
||||
section_unknown:
|
||||
dunno:
|
||||
something
|
||||
bare_unknown:
|
||||
bar
|
||||
write_files:
|
||||
- content: fun
|
||||
path: /var/party
|
||||
file_unknown: nofun
|
||||
users:
|
||||
- name: fry
|
||||
passwd: somehash
|
||||
user_unknown: philip
|
||||
hostname:
|
||||
foo
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("error instantiating CloudConfig with unknown keys: %v", err)
|
||||
}
|
||||
if cfg.Hostname != "foo" {
|
||||
t.Fatalf("hostname not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.Coreos.Etcd) < 1 {
|
||||
t.Fatalf("etcd section not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.WriteFiles) < 1 || cfg.WriteFiles[0].Content != "fun" || cfg.WriteFiles[0].Path != "/var/party" {
|
||||
t.Fatalf("write_files section not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.Users) < 1 || cfg.Users[0].Name != "fry" || cfg.Users[0].PasswordHash != "somehash" {
|
||||
t.Fatalf("users section not correctly set when invalid keys are present")
|
||||
}
|
||||
|
||||
var warnings string
|
||||
catchWarn := func(f string, v ...interface{}) {
|
||||
warnings += fmt.Sprintf(f, v...)
|
||||
}
|
||||
|
||||
warnOnUnrecognizedKeys(contents, catchWarn)
|
||||
|
||||
if !strings.Contains(warnings, "coreos_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized coreos option coreos_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "bare_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized key bare_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "section_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized key section_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "user_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized user key user_unknown")
|
||||
}
|
||||
if !strings.Contains(warnings, "file_unknown") {
|
||||
t.Errorf("warnings did not catch unrecognized file key file_unknown")
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the parsing of a cloud config file "generally works"
|
||||
func TestCloudConfigEmpty(t *testing.T) {
|
||||
cfg, err := NewCloudConfig("")
|
||||
@@ -239,7 +144,7 @@ ssh_authorized_keys:
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
t.Fatalf("Encountered unexpected error :%v", err)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
@@ -257,26 +162,6 @@ func TestCloudConfigSerializationHeader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropInIgnored asserts that users are unable to set DropIn=True on units
|
||||
func TestDropInIgnored(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
units:
|
||||
- name: test
|
||||
dropin: true
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil || len(cfg.Coreos.Units) != 1 {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
if len(cfg.Coreos.Units) != 1 || cfg.Coreos.Units[0].Name != "test" {
|
||||
t.Fatalf("Expected 1 unit, but got %d: %v", len(cfg.Coreos.Units), cfg.Coreos.Units)
|
||||
}
|
||||
if cfg.Coreos.Units[0].DropIn {
|
||||
t.Errorf("dropin option on unit in cloud-config was not ignored!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUsers(t *testing.T) {
|
||||
contents := `
|
||||
users:
|
||||
@@ -362,109 +247,3 @@ users:
|
||||
t.Errorf("Failed to parse no-log-init field")
|
||||
}
|
||||
}
|
||||
|
||||
type TestUnitManager struct {
|
||||
placed []string
|
||||
enabled []string
|
||||
masked []string
|
||||
unmasked []string
|
||||
commands map[string]string
|
||||
reload bool
|
||||
}
|
||||
|
||||
func (tum *TestUnitManager) PlaceUnit(unit *system.Unit, dst string) error {
|
||||
tum.placed = append(tum.placed, unit.Name)
|
||||
return nil
|
||||
}
|
||||
func (tum *TestUnitManager) EnableUnitFile(unit string, runtime bool) error {
|
||||
tum.enabled = append(tum.enabled, unit)
|
||||
return nil
|
||||
}
|
||||
func (tum *TestUnitManager) RunUnitCommand(command, unit string) (string, error) {
|
||||
tum.commands = make(map[string]string)
|
||||
tum.commands[unit] = command
|
||||
return "", nil
|
||||
}
|
||||
func (tum *TestUnitManager) DaemonReload() error {
|
||||
tum.reload = true
|
||||
return nil
|
||||
}
|
||||
func (tum *TestUnitManager) MaskUnit(unit *system.Unit) error {
|
||||
tum.masked = append(tum.masked, unit.Name)
|
||||
return nil
|
||||
}
|
||||
func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error {
|
||||
tum.unmasked = append(tum.unmasked, unit.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestProcessUnits(t *testing.T) {
|
||||
tum := &TestUnitManager{}
|
||||
units := []system.Unit{
|
||||
system.Unit{
|
||||
Name: "foo",
|
||||
Mask: true,
|
||||
},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.masked) != 1 || tum.masked[0] != "foo" {
|
||||
t.Errorf("expected foo to be masked, but found %v", tum.masked)
|
||||
}
|
||||
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{
|
||||
Name: "bar.network",
|
||||
},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if _, ok := tum.commands["systemd-networkd.service"]; !ok {
|
||||
t.Errorf("expected systemd-networkd.service to be reloaded!")
|
||||
}
|
||||
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{
|
||||
Name: "baz.service",
|
||||
Content: "[Service]\nExecStart=/bin/true",
|
||||
},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.placed) != 1 || tum.placed[0] != "baz.service" {
|
||||
t.Fatalf("expected baz.service to be written, but got %v", tum.placed)
|
||||
}
|
||||
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{
|
||||
Name: "locksmithd.service",
|
||||
Runtime: true,
|
||||
},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.unmasked) != 1 || tum.unmasked[0] != "locksmithd.service" {
|
||||
t.Fatalf("expected locksmithd.service to be unmasked, but got %v", tum.unmasked)
|
||||
}
|
||||
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{
|
||||
Name: "woof",
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.enabled) != 1 || tum.enabled[0] != "woof" {
|
||||
t.Fatalf("expected woof to be enabled, but got %v", tum.enabled)
|
||||
}
|
||||
}
|
||||
|
@@ -4,98 +4,44 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const DefaultSSHKeyName = "coreos-cloudinit"
|
||||
|
||||
type Environment struct {
|
||||
root string
|
||||
configRoot string
|
||||
workspace string
|
||||
netconfType string
|
||||
sshKeyName string
|
||||
substitutions map[string]string
|
||||
}
|
||||
|
||||
// TODO(jonboulle): this is getting unwieldy, should be able to simplify the interface somehow
|
||||
func NewEnvironment(root, configRoot, workspace, netconfType, sshKeyName string, substitutions map[string]string) *Environment {
|
||||
if substitutions == nil {
|
||||
substitutions = make(map[string]string)
|
||||
}
|
||||
// If certain values are not in the supplied substitution, fall back to retrieving them from the environment
|
||||
for k, v := range map[string]string{
|
||||
func NewEnvironment(root, workspace string) *Environment {
|
||||
substitutions := map[string]string{
|
||||
"$public_ipv4": os.Getenv("COREOS_PUBLIC_IPV4"),
|
||||
"$private_ipv4": os.Getenv("COREOS_PRIVATE_IPV4"),
|
||||
} {
|
||||
if _, ok := substitutions[k]; !ok {
|
||||
substitutions[k] = v
|
||||
}
|
||||
}
|
||||
return &Environment{root, configRoot, workspace, netconfType, sshKeyName, substitutions}
|
||||
return &Environment{root, workspace, DefaultSSHKeyName, substitutions}
|
||||
}
|
||||
|
||||
func (e *Environment) Workspace() string {
|
||||
return path.Join(e.root, e.workspace)
|
||||
func (self *Environment) Workspace() string {
|
||||
return path.Join(self.root, self.workspace)
|
||||
}
|
||||
|
||||
func (e *Environment) Root() string {
|
||||
return e.root
|
||||
func (self *Environment) Root() string {
|
||||
return self.root
|
||||
}
|
||||
|
||||
func (e *Environment) ConfigRoot() string {
|
||||
return e.configRoot
|
||||
func (self *Environment) SSHKeyName() string {
|
||||
return self.sshKeyName
|
||||
}
|
||||
|
||||
func (e *Environment) NetconfType() string {
|
||||
return e.netconfType
|
||||
func (self *Environment) SetSSHKeyName(name string) {
|
||||
self.sshKeyName = name
|
||||
}
|
||||
|
||||
func (e *Environment) SSHKeyName() string {
|
||||
return e.sshKeyName
|
||||
}
|
||||
|
||||
func (e *Environment) SetSSHKeyName(name string) {
|
||||
e.sshKeyName = name
|
||||
}
|
||||
|
||||
func (e *Environment) Apply(data string) string {
|
||||
for key, val := range e.substitutions {
|
||||
func (self *Environment) Apply(data string) string {
|
||||
for key, val := range self.substitutions {
|
||||
data = strings.Replace(data, key, val, -1)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (e *Environment) DefaultEnvironmentFile() *system.EnvFile {
|
||||
ef := system.EnvFile{
|
||||
File: &system.File{
|
||||
Path: "/etc/environment",
|
||||
},
|
||||
Vars: map[string]string{},
|
||||
}
|
||||
if ip, ok := e.substitutions["$public_ipv4"]; ok && len(ip) > 0 {
|
||||
ef.Vars["COREOS_PUBLIC_IPV4"] = ip
|
||||
}
|
||||
if ip, ok := e.substitutions["$private_ipv4"]; ok && len(ip) > 0 {
|
||||
ef.Vars["COREOS_PRIVATE_IPV4"] = ip
|
||||
}
|
||||
if len(ef.Vars) == 0 {
|
||||
return nil
|
||||
} else {
|
||||
return &ef
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeSvcEnv standardizes the keys of the map (environment variables for a service)
|
||||
// by replacing any dashes with underscores and ensuring they are entirely upper case.
|
||||
// For example, "some-env" --> "SOME_ENV"
|
||||
func normalizeSvcEnv(m map[string]string) map[string]string {
|
||||
out := make(map[string]string, len(m))
|
||||
for key, val := range m {
|
||||
key = strings.ToUpper(key)
|
||||
key = strings.Replace(key, "-", "_", -1)
|
||||
out[key] = val
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
@@ -1,106 +1,27 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestEnvironmentApply(t *testing.T) {
|
||||
os.Setenv("COREOS_PUBLIC_IPV4", "1.2.3.4")
|
||||
os.Setenv("COREOS_PRIVATE_IPV4", "5.6.7.8")
|
||||
for _, tt := range []struct {
|
||||
subs map[string]string
|
||||
input string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
// Substituting both values directly should always take precedence
|
||||
// over environment variables
|
||||
map[string]string{
|
||||
"$public_ipv4": "192.0.2.3",
|
||||
"$private_ipv4": "192.0.2.203",
|
||||
},
|
||||
`[Service]
|
||||
os.Setenv("COREOS_PUBLIC_IPV4", "192.0.2.3")
|
||||
os.Setenv("COREOS_PRIVATE_IPV4", "192.0.2.203")
|
||||
env := NewEnvironment("./", "./")
|
||||
input := `[Service]
|
||||
ExecStart=/usr/bin/echo "$public_ipv4"
|
||||
ExecStop=/usr/bin/echo $private_ipv4
|
||||
ExecStop=/usr/bin/echo $unknown`,
|
||||
`[Service]
|
||||
ExecStop=/usr/bin/echo $unknown
|
||||
`
|
||||
expected := `[Service]
|
||||
ExecStart=/usr/bin/echo "192.0.2.3"
|
||||
ExecStop=/usr/bin/echo 192.0.2.203
|
||||
ExecStop=/usr/bin/echo $unknown`,
|
||||
},
|
||||
{
|
||||
// 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",
|
||||
},
|
||||
} {
|
||||
ExecStop=/usr/bin/echo $unknown
|
||||
`
|
||||
|
||||
env := NewEnvironment("./", "./", "./", "", "", tt.subs)
|
||||
got := env.Apply(tt.input)
|
||||
if got != tt.out {
|
||||
t.Fatalf("Environment incorrectly applied.\ngot:\n%s\nwant:\n%s", got, tt.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentFile(t *testing.T) {
|
||||
subs := map[string]string{
|
||||
"$public_ipv4": "1.2.3.4",
|
||||
"$private_ipv4": "5.6.7.8",
|
||||
}
|
||||
expect := "COREOS_PRIVATE_IPV4=5.6.7.8\nCOREOS_PUBLIC_IPV4=1.2.3.4\n"
|
||||
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
env := NewEnvironment("./", "./", "./", "", "", subs)
|
||||
ef := env.DefaultEnvironmentFile()
|
||||
err = system.WriteEnvFile(ef, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteEnvFile failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "environment")
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != expect {
|
||||
t.Fatalf("File has incorrect contents: %q", contents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentFileNil(t *testing.T) {
|
||||
subs := map[string]string{
|
||||
"$public_ipv4": "",
|
||||
"$private_ipv4": "",
|
||||
}
|
||||
|
||||
env := NewEnvironment("./", "./", "./", "", "", subs)
|
||||
ef := env.DefaultEnvironmentFile()
|
||||
if ef != nil {
|
||||
t.Fatalf("Environment file not nil: %v", ef)
|
||||
output := env.Apply(input)
|
||||
if output != expected {
|
||||
t.Fatalf("Environment incorrectly applied.\nOutput:\n%s\nExpected:\n%s", output, expected)
|
||||
}
|
||||
}
|
||||
|
@@ -3,15 +3,26 @@ package initialize
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
type EtcdEnvironment map[string]string
|
||||
|
||||
func (ee EtcdEnvironment) String() (out string) {
|
||||
norm := normalizeSvcEnv(ee)
|
||||
func (ec EtcdEnvironment) normalized() map[string]string {
|
||||
out := make(map[string]string, len(ec))
|
||||
for key, val := range ec {
|
||||
key = strings.ToUpper(key)
|
||||
key = strings.Replace(key, "-", "_", -1)
|
||||
out[key] = val
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (ec EtcdEnvironment) String() (out string) {
|
||||
norm := ec.normalized()
|
||||
|
||||
if val, ok := norm["DISCOVERY_URL"]; ok {
|
||||
delete(norm, "DISCOVERY_URL")
|
||||
@@ -20,44 +31,32 @@ func (ee EtcdEnvironment) String() (out string) {
|
||||
}
|
||||
}
|
||||
|
||||
var sorted sort.StringSlice
|
||||
for k, _ := range norm {
|
||||
sorted = append(sorted, k)
|
||||
}
|
||||
sorted.Sort()
|
||||
|
||||
out += "[Service]\n"
|
||||
|
||||
for _, key := range sorted {
|
||||
val := norm[key]
|
||||
for key, val := range norm {
|
||||
out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Units creates a Unit file drop-in for etcd, using any configured
|
||||
// options and adding a default MachineID if unset.
|
||||
func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
|
||||
if len(ee) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if _, ok := ee["name"]; !ok {
|
||||
// Write an EtcdEnvironment to the appropriate path on disk for etcd.service
|
||||
func WriteEtcdEnvironment(env EtcdEnvironment, root string) error {
|
||||
if _, ok := env["name"]; !ok {
|
||||
if machineID := system.MachineID(root); machineID != "" {
|
||||
ee["name"] = machineID
|
||||
env["name"] = machineID
|
||||
} else if hostname, err := system.Hostname(); err == nil {
|
||||
ee["name"] = hostname
|
||||
env["name"] = hostname
|
||||
} else {
|
||||
return nil, errors.New("Unable to determine default etcd name")
|
||||
return errors.New("Unable to determine default etcd name")
|
||||
}
|
||||
}
|
||||
|
||||
etcd := system.Unit{
|
||||
Name: "etcd.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: ee.String(),
|
||||
file := system.File{
|
||||
Path: path.Join(root, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: env.String(),
|
||||
}
|
||||
return []system.Unit{etcd}, nil
|
||||
|
||||
return system.WriteFile(&file)
|
||||
}
|
||||
|
@@ -3,10 +3,9 @@ package initialize
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestEtcdEnvironment(t *testing.T) {
|
||||
@@ -59,7 +58,7 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
}
|
||||
|
||||
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
||||
ee := EtcdEnvironment{
|
||||
ec := EtcdEnvironment{
|
||||
"name": "node001",
|
||||
"discovery": "http://disco.example.com/foobar",
|
||||
"peer-bind-addr": "127.0.0.1:7002",
|
||||
@@ -70,21 +69,8 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := system.NewUnitManager(dir)
|
||||
|
||||
uu, err := ee.Units(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Generating etcd unit failed: %v", err)
|
||||
}
|
||||
if len(uu) != 1 {
|
||||
t.Fatalf("Expected 1 unit to be returned, got %d", len(uu))
|
||||
}
|
||||
u := uu[0]
|
||||
|
||||
dst := u.Destination(dir)
|
||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
||||
if err := WriteEtcdEnvironment(ec, dir); err != nil {
|
||||
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
|
||||
@@ -104,8 +90,8 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
||||
}
|
||||
|
||||
expect := `[Service]
|
||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||
Environment="ETCD_NAME=node001"
|
||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
`
|
||||
if string(contents) != expect {
|
||||
@@ -113,46 +99,22 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdEnvironmentEmptyNoOp(t *testing.T) {
|
||||
ee := EtcdEnvironment{}
|
||||
uu, err := ee.Units("")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if len(uu) > 0 {
|
||||
t.Fatalf("Generated etcd units unexpectedly: %v")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
|
||||
ee := EtcdEnvironment{"foo": "bar"}
|
||||
ec := EtcdEnvironment{}
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := system.NewUnitManager(dir)
|
||||
|
||||
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
|
||||
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed writing out /etc/machine-id: %v", err)
|
||||
}
|
||||
|
||||
uu, err := ee.Units(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Generating etcd unit failed: %v", err)
|
||||
}
|
||||
if len(uu) == 0 {
|
||||
t.Fatalf("Returned empty etcd units unexpectedly")
|
||||
}
|
||||
u := uu[0]
|
||||
|
||||
dst := u.Destination(dir)
|
||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
||||
if err := WriteEtcdEnvironment(ec, dir); err != nil {
|
||||
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
|
||||
@@ -163,7 +125,6 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
|
||||
}
|
||||
|
||||
expect := `[Service]
|
||||
Environment="ETCD_FOO=bar"
|
||||
Environment="ETCD_NAME=node007"
|
||||
`
|
||||
if string(contents) != expect {
|
||||
@@ -171,14 +132,7 @@ Environment="ETCD_NAME=node007"
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdEnvironmentWhenNil(t *testing.T) {
|
||||
// EtcdEnvironment will be a nil map if it wasn't in the yaml
|
||||
var ee EtcdEnvironment
|
||||
if ee != nil {
|
||||
t.Fatalf("EtcdEnvironment is not nil")
|
||||
}
|
||||
uu, err := ee.Units("")
|
||||
if len(uu) != 0 || err != nil {
|
||||
t.Fatalf("Units returned value for nil input")
|
||||
}
|
||||
func rmdir(path string) error {
|
||||
cmd := exec.Command("rm", "-rf", path)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
@@ -1,35 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
type FleetEnvironment map[string]string
|
||||
|
||||
func (fe FleetEnvironment) String() (out string) {
|
||||
norm := normalizeSvcEnv(fe)
|
||||
out += "[Service]\n"
|
||||
|
||||
for key, val := range norm {
|
||||
out += fmt.Sprintf("Environment=\"FLEET_%s=%s\"\n", key, val)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Units generates a Unit file drop-in for fleet, if any fleet options were
|
||||
// configured in cloud-config
|
||||
func (fe FleetEnvironment) Units(root string) ([]system.Unit, error) {
|
||||
if len(fe) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
fleet := system.Unit{
|
||||
Name: "fleet.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: fe.String(),
|
||||
}
|
||||
return []system.Unit{fleet}, nil
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFleetEnvironment(t *testing.T) {
|
||||
cfg := make(FleetEnvironment, 0)
|
||||
cfg["public-ip"] = "12.34.56.78"
|
||||
|
||||
env := cfg.String()
|
||||
|
||||
expect := `[Service]
|
||||
Environment="FLEET_PUBLIC_IP=12.34.56.78"
|
||||
`
|
||||
|
||||
if env != expect {
|
||||
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetUnit(t *testing.T) {
|
||||
cfg := make(FleetEnvironment, 0)
|
||||
uu, err := cfg.Units("/")
|
||||
if len(uu) != 0 {
|
||||
t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
|
||||
}
|
||||
|
||||
cfg["public-ip"] = "12.34.56.78"
|
||||
|
||||
uu, err = cfg.Units("/")
|
||||
if err != nil {
|
||||
t.Errorf("error generating fleet unit: %v", err)
|
||||
}
|
||||
if len(uu) != 1 {
|
||||
t.Fatalf("expected 1 unit generated, got %d", len(uu))
|
||||
}
|
||||
u := uu[0]
|
||||
if !u.Runtime {
|
||||
t.Errorf("bad Runtime for generated fleet unit!")
|
||||
}
|
||||
if !u.DropIn {
|
||||
t.Errorf("bad DropIn for generated fleet unit!")
|
||||
}
|
||||
}
|
85
initialize/locksmith.go
Normal file
85
initialize/locksmith.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const locksmithUnit = "locksmithd.service"
|
||||
|
||||
// addStrategy creates an `/etc/coreos/update.conf` file with the requested
|
||||
// strategy via rewriting the file on disk or by starting from
|
||||
// `/usr/share/coreos/update.conf`.
|
||||
func addStrategy(strategy string, root string) error {
|
||||
etcUpdate := path.Join(root, "etc", "coreos", "update.conf")
|
||||
usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf")
|
||||
|
||||
// Ensure /etc/coreos/ exists before attempting to write a file in it
|
||||
os.MkdirAll(path.Join(root, "etc", "coreos"), 0755)
|
||||
|
||||
tmp, err := ioutil.TempFile(path.Join(root, "etc", "coreos"), ".update.conf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tmp.Chmod(0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf, err := os.Open(etcUpdate)
|
||||
if os.IsNotExist(err) {
|
||||
conf, err = os.Open(usrUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(conf)
|
||||
|
||||
sawStrat := false
|
||||
stratLine := "REBOOT_STRATEGY="+strategy
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "REBOOT_STRATEGY=") {
|
||||
line = stratLine
|
||||
sawStrat = true
|
||||
}
|
||||
fmt.Fprintln(tmp, line)
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !sawStrat {
|
||||
fmt.Fprintln(tmp, stratLine)
|
||||
}
|
||||
|
||||
return os.Rename(tmp.Name(), etcUpdate)
|
||||
}
|
||||
|
||||
// WriteLocksmithConfig updates the `update.conf` file with a REBOOT_STRATEGY for locksmith.
|
||||
func WriteLocksmithConfig(strategy string, root string) error {
|
||||
cmd := "restart"
|
||||
if strategy == "off" {
|
||||
err := system.MaskUnit(locksmithUnit, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd = "stop"
|
||||
} else {
|
||||
return addStrategy(strategy, root)
|
||||
}
|
||||
if err := system.DaemonReload(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := system.RunUnitCommand(cmd, locksmithUnit); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
91
initialize/locksmith_test.go
Normal file
91
initialize/locksmith_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
base = `SERVER=https://example.com
|
||||
GROUP=thegroupc`
|
||||
|
||||
configured = base + `
|
||||
REBOOT_STRATEGY=awesome
|
||||
`
|
||||
|
||||
expected = base + `
|
||||
REBOOT_STRATEGY=etcd-lock
|
||||
`
|
||||
)
|
||||
|
||||
func setupFixtures(dir string) {
|
||||
os.MkdirAll(path.Join(dir, "usr", "share", "coreos"), 0755)
|
||||
os.MkdirAll(path.Join(dir, "run", "systemd", "system"), 0755)
|
||||
|
||||
ioutil.WriteFile(path.Join(dir, "usr", "share", "coreos", "update.conf"), []byte(base), 0644)
|
||||
}
|
||||
|
||||
func TestLocksmithEnvironmentWrittenToDisk(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
setupFixtures(dir)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
if i == 1 {
|
||||
err = ioutil.WriteFile(path.Join(dir, "etc", "coreos", "update.conf"), []byte(configured), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := WriteLocksmithConfig("etcd-lock", dir); err != nil {
|
||||
t.Fatalf("Processing of LocksmithEnvironment failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "coreos", "update.conf")
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode() != os.FileMode(0644) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != expected {
|
||||
t.Fatalf("File has incorrect contents, got %v, wanted %v", string(contents), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestLocksmithEnvironmentMasked(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
setupFixtures(dir)
|
||||
|
||||
if err := WriteLocksmithConfig("off", dir); err != nil {
|
||||
t.Fatalf("Processing of LocksmithEnvironment failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "systemd", "system", "locksmithd.service")
|
||||
target, err := os.Readlink(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read link %v", err)
|
||||
}
|
||||
if target != "/dev/null" {
|
||||
t.Fatalf("Locksmith not masked, unit target %v", target)
|
||||
}
|
||||
}
|
@@ -11,10 +11,8 @@ import (
|
||||
|
||||
const DefaultIpv4Address = "127.0.0.1"
|
||||
|
||||
type EtcHosts string
|
||||
|
||||
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
|
||||
if eh != "localhost" {
|
||||
func generateEtcHosts(option string) (out string, err error) {
|
||||
if option != "localhost" {
|
||||
return "", errors.New("Invalid option to manage_etc_hosts")
|
||||
}
|
||||
|
||||
@@ -28,19 +26,19 @@ func (eh EtcHosts) generateEtcHosts() (out string, err error) {
|
||||
|
||||
}
|
||||
|
||||
func (eh EtcHosts) File(root string) (*system.File, error) {
|
||||
if eh == "" {
|
||||
return nil, nil
|
||||
}
|
||||
// Write an /etc/hosts file
|
||||
func WriteEtcHosts(option string, root string) error {
|
||||
|
||||
etcHosts, err := eh.generateEtcHosts()
|
||||
etcHosts, err := generateEtcHosts(option)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return &system.File{
|
||||
Path: path.Join("etc", "hosts"),
|
||||
file := system.File{
|
||||
Path: path.Join(root, "etc", "hosts"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: etcHosts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return system.WriteFile(&file)
|
||||
}
|
||||
|
@@ -6,8 +6,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestCloudConfigManageEtcHosts(t *testing.T) {
|
||||
@@ -27,9 +25,14 @@ manage_etc_hosts: localhost
|
||||
}
|
||||
|
||||
func TestManageEtcHostsInvalidValue(t *testing.T) {
|
||||
eh := EtcHosts("invalid")
|
||||
if f, err := eh.File(""); err == nil || f != nil {
|
||||
t.Fatalf("EtcHosts File succeeded with invalid value!")
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer rmdir(dir)
|
||||
|
||||
if err := WriteEtcHosts("invalid", dir); err == nil {
|
||||
t.Fatalf("WriteEtcHosts succeeded with invalid value: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,20 +41,10 @@ func TestEtcHostsWrittenToDisk(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
defer rmdir(dir)
|
||||
|
||||
eh := EtcHosts("localhost")
|
||||
|
||||
f, err := eh.File(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error calling File on EtcHosts: %v", err)
|
||||
}
|
||||
if f == nil {
|
||||
t.Fatalf("manageEtcHosts returned nil file unexpectedly")
|
||||
}
|
||||
|
||||
if _, err := system.WriteFile(f, dir); err != nil {
|
||||
t.Fatalf("Error writing EtcHosts: %v", err)
|
||||
if err := WriteEtcHosts("localhost", dir); err != nil {
|
||||
t.Fatalf("WriteEtcHosts failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "hosts")
|
||||
|
@@ -1,52 +0,0 @@
|
||||
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
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,7 +16,7 @@ type OEMRelease struct {
|
||||
BugReportURL string `yaml:"bug-report-url"`
|
||||
}
|
||||
|
||||
func (oem OEMRelease) String() string {
|
||||
func (oem *OEMRelease) String() string {
|
||||
fields := []string{
|
||||
fmt.Sprintf("ID=%s", oem.ID),
|
||||
fmt.Sprintf("VERSION_ID=%s", oem.VersionID),
|
||||
@@ -28,14 +28,12 @@ func (oem OEMRelease) String() string {
|
||||
return strings.Join(fields, "\n") + "\n"
|
||||
}
|
||||
|
||||
func (oem OEMRelease) File(root string) (*system.File, error) {
|
||||
if oem.ID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &system.File{
|
||||
Path: path.Join("etc", "oem-release"),
|
||||
func WriteOEMRelease(oem *OEMRelease, root string) error {
|
||||
file := system.File{
|
||||
Path: path.Join(root, "etc", "oem-release"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: oem.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return system.WriteFile(&file)
|
||||
}
|
||||
|
@@ -5,8 +5,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func TestOEMReleaseWrittenToDisk(t *testing.T) {
|
||||
@@ -23,16 +21,8 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
f, err := oem.File(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Processing of OEMRelease failed: %v", err)
|
||||
}
|
||||
if f == nil {
|
||||
t.Fatalf("OEMRelease returned nil file unexpectedly")
|
||||
}
|
||||
|
||||
if _, err := system.WriteFile(f, dir); err != nil {
|
||||
t.Fatalf("Writing of OEMRelease failed: %v", err)
|
||||
if err := WriteOEMRelease(&oem, dir); err != nil {
|
||||
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "oem-release")
|
||||
|
@@ -3,8 +3,9 @@ package initialize
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
@@ -24,19 +25,22 @@ func SSHImportKeysFromURL(system_user string, url string) error {
|
||||
}
|
||||
|
||||
func fetchUserKeys(url string) ([]string, error) {
|
||||
client := pkg.NewHttpClient()
|
||||
data, err := client.GetRetry(url)
|
||||
res, err := http.Get(url)
|
||||
defer res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var userKeys []UserKey
|
||||
err = json.Unmarshal(data, &userKeys)
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data []UserKey
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]string, 0)
|
||||
for _, key := range userKeys {
|
||||
for _, key := range data {
|
||||
keys = append(keys, key.Key)
|
||||
}
|
||||
return keys, err
|
||||
|
@@ -1,165 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const (
|
||||
locksmithUnit = "locksmithd.service"
|
||||
updateEngineUnit = "update-engine.service"
|
||||
)
|
||||
|
||||
// updateOption represents a configurable update option, which, if set, will be
|
||||
// written into update.conf, replacing any existing value for the option
|
||||
type updateOption struct {
|
||||
key string // key used to configure this option in cloud-config
|
||||
valid []string // valid values for the option
|
||||
prefix string // prefix for the option in the update.conf file
|
||||
value string // used to store the new value in update.conf (including prefix)
|
||||
seen bool // whether the option has been seen in any existing update.conf
|
||||
}
|
||||
|
||||
// updateOptions defines the update options understood by cloud-config.
|
||||
// The keys represent the string used in cloud-config to configure the option.
|
||||
var updateOptions = []*updateOption{
|
||||
&updateOption{
|
||||
key: "reboot-strategy",
|
||||
prefix: "REBOOT_STRATEGY=",
|
||||
valid: []string{"best-effort", "etcd-lock", "reboot", "off"},
|
||||
},
|
||||
&updateOption{
|
||||
key: "group",
|
||||
prefix: "GROUP=",
|
||||
},
|
||||
&updateOption{
|
||||
key: "server",
|
||||
prefix: "SERVER=",
|
||||
},
|
||||
}
|
||||
|
||||
// isValid checks whether a supplied value is valid for this option
|
||||
func (uo updateOption) isValid(val string) bool {
|
||||
if len(uo.valid) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, v := range uo.valid {
|
||||
if val == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type UpdateConfig map[string]string
|
||||
|
||||
// File generates an `/etc/coreos/update.conf` file (if any update
|
||||
// configuration options are set in cloud-config) by either rewriting the
|
||||
// existing file on disk, or starting from `/usr/share/coreos/update.conf`
|
||||
func (uc UpdateConfig) File(root string) (*system.File, error) {
|
||||
if len(uc) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var out string
|
||||
|
||||
// Generate the list of possible substitutions to be performed based on the options that are configured
|
||||
subs := make([]*updateOption, 0)
|
||||
for _, uo := range updateOptions {
|
||||
val, ok := uc[uo.key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !uo.isValid(val) {
|
||||
return nil, errors.New(fmt.Sprintf("invalid value %v for option %v (valid options: %v)", val, uo.key, uo.valid))
|
||||
}
|
||||
uo.value = uo.prefix + val
|
||||
subs = append(subs, uo)
|
||||
}
|
||||
|
||||
etcUpdate := path.Join(root, "etc", "coreos", "update.conf")
|
||||
usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf")
|
||||
|
||||
conf, err := os.Open(etcUpdate)
|
||||
if os.IsNotExist(err) {
|
||||
conf, err = os.Open(usrUpdate)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(conf)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
for _, s := range subs {
|
||||
if strings.HasPrefix(line, s.prefix) {
|
||||
line = s.value
|
||||
s.seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
out += line
|
||||
out += "\n"
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range subs {
|
||||
if !s.seen {
|
||||
out += s.value
|
||||
out += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
return &system.File{
|
||||
Path: path.Join("etc", "coreos", "update.conf"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: out,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Units generates units for the cloud-init initializer to act on:
|
||||
// - a locksmith system.Unit, if "reboot-strategy" was set in cloud-config
|
||||
// - an update_engine system.Unit, if "group" was set in cloud-config
|
||||
func (uc UpdateConfig) Units(root string) ([]system.Unit, error) {
|
||||
var units []system.Unit
|
||||
if strategy, ok := uc["reboot-strategy"]; ok {
|
||||
ls := &system.Unit{
|
||||
Name: locksmithUnit,
|
||||
Command: "restart",
|
||||
Mask: false,
|
||||
Runtime: true,
|
||||
}
|
||||
|
||||
if strategy == "off" {
|
||||
ls.Command = "stop"
|
||||
ls.Mask = true
|
||||
}
|
||||
units = append(units, *ls)
|
||||
}
|
||||
|
||||
rue := false
|
||||
if _, ok := uc["group"]; ok {
|
||||
rue = true
|
||||
}
|
||||
if _, ok := uc["server"]; ok {
|
||||
rue = true
|
||||
}
|
||||
if rue {
|
||||
ue := system.Unit{
|
||||
Name: updateEngineUnit,
|
||||
Command: "restart",
|
||||
}
|
||||
units = append(units, ue)
|
||||
}
|
||||
|
||||
return units, nil
|
||||
}
|
@@ -1,232 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const (
|
||||
base = `SERVER=https://example.com
|
||||
GROUP=thegroupc`
|
||||
configured = base + `
|
||||
REBOOT_STRATEGY=awesome
|
||||
`
|
||||
expected = base + `
|
||||
REBOOT_STRATEGY=etcd-lock
|
||||
`
|
||||
)
|
||||
|
||||
func setupFixtures(dir string) {
|
||||
os.MkdirAll(path.Join(dir, "usr", "share", "coreos"), 0755)
|
||||
os.MkdirAll(path.Join(dir, "run", "systemd", "system"), 0755)
|
||||
|
||||
ioutil.WriteFile(path.Join(dir, "usr", "share", "coreos", "update.conf"), []byte(base), 0644)
|
||||
}
|
||||
|
||||
func TestEmptyUpdateConfig(t *testing.T) {
|
||||
uc := &UpdateConfig{}
|
||||
f, err := uc.File("")
|
||||
if err != nil {
|
||||
t.Error("unexpected error getting file from empty UpdateConfig")
|
||||
}
|
||||
if f != nil {
|
||||
t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f)
|
||||
}
|
||||
uu, err := uc.Units("")
|
||||
if err != nil {
|
||||
t.Error("unexpected error getting unit from empty UpdateConfig")
|
||||
}
|
||||
if len(uu) != 0 {
|
||||
t.Errorf("getting unit from empty UpdateConfig should have returned zero units, got %d", len(uu))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidUpdateOptions(t *testing.T) {
|
||||
uon := &updateOption{
|
||||
key: "numbers",
|
||||
prefix: "numero_",
|
||||
valid: []string{"one", "two"},
|
||||
}
|
||||
uoa := &updateOption{
|
||||
key: "any_will_do",
|
||||
prefix: "any_",
|
||||
}
|
||||
|
||||
if !uon.isValid("one") {
|
||||
t.Error("update option did not accept valid option \"one\"")
|
||||
}
|
||||
if uon.isValid("three") {
|
||||
t.Error("update option accepted invalid option \"three\"")
|
||||
}
|
||||
for _, s := range []string{"one", "asdf", "foobarbaz"} {
|
||||
if !uoa.isValid(s) {
|
||||
t.Errorf("update option with no \"valid\" field did not accept %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
uc := &UpdateConfig{"reboot-strategy": "wizzlewazzle"}
|
||||
f, err := uc.File("")
|
||||
if err == nil {
|
||||
t.Errorf("File did not give an error on invalid UpdateOption")
|
||||
}
|
||||
if f != nil {
|
||||
t.Errorf("File did not return a nil file on invalid UpdateOption")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerGroupOptions(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
setupFixtures(dir)
|
||||
u := &UpdateConfig{"group": "master", "server": "http://foo.com"}
|
||||
|
||||
want := `
|
||||
GROUP=master
|
||||
SERVER=http://foo.com`
|
||||
|
||||
f, err := u.File(dir)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error getting file from UpdateConfig: %v", err)
|
||||
} else if f == nil {
|
||||
t.Error("unexpectedly got empty file from UpdateConfig")
|
||||
} else {
|
||||
out := strings.Split(f.Content, "\n")
|
||||
sort.Strings(out)
|
||||
got := strings.Join(out, "\n")
|
||||
if got != want {
|
||||
t.Errorf("File has incorrect contents, got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
uu, err := u.Units(dir)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error getting units from UpdateConfig: %v", err)
|
||||
} else if len(uu) != 1 {
|
||||
t.Errorf("unexpected number of files returned from UpdateConfig: want 1, got %d", len(uu))
|
||||
} else {
|
||||
unit := uu[0]
|
||||
if unit.Name != "update-engine.service" {
|
||||
t.Errorf("bad name for generated unit: want update-engine.service, got %s", unit.Name)
|
||||
}
|
||||
if unit.Command != "restart" {
|
||||
t.Errorf("bad command for generated unit: want restart, got %s", unit.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebootStrategies(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
setupFixtures(dir)
|
||||
strategies := []struct {
|
||||
name string
|
||||
line string
|
||||
uMask bool
|
||||
uCommand string
|
||||
}{
|
||||
{"best-effort", "REBOOT_STRATEGY=best-effort", false, "restart"},
|
||||
{"etcd-lock", "REBOOT_STRATEGY=etcd-lock", false, "restart"},
|
||||
{"reboot", "REBOOT_STRATEGY=reboot", false, "restart"},
|
||||
{"off", "REBOOT_STRATEGY=off", true, "stop"},
|
||||
}
|
||||
for _, s := range strategies {
|
||||
uc := &UpdateConfig{"reboot-strategy": s.name}
|
||||
f, err := uc.File(dir)
|
||||
if err != nil {
|
||||
t.Errorf("update failed to generate file for reboot-strategy=%v: %v", s.name, err)
|
||||
} else if f == nil {
|
||||
t.Errorf("generated empty file for reboot-strategy=%v", s.name)
|
||||
} else {
|
||||
seen := false
|
||||
for _, line := range strings.Split(f.Content, "\n") {
|
||||
if line == s.line {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line)
|
||||
}
|
||||
}
|
||||
uu, err := uc.Units(dir)
|
||||
if err != nil {
|
||||
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
|
||||
} else if len(uu) != 1 {
|
||||
t.Errorf("unexpected number of units for reboot-strategy=%v: %d", s.name, len(uu))
|
||||
} else {
|
||||
u := uu[0]
|
||||
if u.Name != locksmithUnit {
|
||||
t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name)
|
||||
}
|
||||
if u.Mask != s.uMask {
|
||||
t.Errorf("unit generated for reboot strategy=%v had bad mask: %t", s.name, u.Mask)
|
||||
}
|
||||
if u.Command != s.uCommand {
|
||||
t.Errorf("unit generated for reboot strategy=%v had bad command: %v", s.name, u.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestUpdateConfWrittenToDisk(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
setupFixtures(dir)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
if i == 1 {
|
||||
err = ioutil.WriteFile(path.Join(dir, "etc", "coreos", "update.conf"), []byte(configured), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
uc := &UpdateConfig{"reboot-strategy": "etcd-lock"}
|
||||
|
||||
f, err := uc.File(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Processing UpdateConfig failed: %v", err)
|
||||
} else if f == nil {
|
||||
t.Fatal("Unexpectedly got nil updateconfig file")
|
||||
}
|
||||
|
||||
if _, err := system.WriteFile(f, dir); err != nil {
|
||||
t.Fatalf("Error writing update config: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "coreos", "update.conf")
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode() != os.FileMode(0644) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != expected {
|
||||
t.Fatalf("File has incorrect contents, got %v, wanted %v", string(contents), expected)
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,9 +9,6 @@ import (
|
||||
)
|
||||
|
||||
func ParseUserData(contents string) (interface{}, error) {
|
||||
if len(contents) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
header := strings.SplitN(contents, "\n", 2)[0]
|
||||
|
||||
// Explicitly trim the header so we can handle user-data from
|
||||
@@ -22,9 +19,14 @@ func ParseUserData(contents string) (interface{}, error) {
|
||||
if strings.HasPrefix(header, "#!") {
|
||||
log.Printf("Parsing user-data as script")
|
||||
return system.Script(contents), nil
|
||||
|
||||
} else if header == "#cloud-config" {
|
||||
log.Printf("Parsing user-data as cloud-config")
|
||||
return NewCloudConfig(contents)
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
return *cfg, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@ func TestParseConfigCRLF(t *testing.T) {
|
||||
t.Fatalf("Failed parsing config: %v", err)
|
||||
}
|
||||
|
||||
cfg := ud.(*CloudConfig)
|
||||
cfg := ud.(CloudConfig)
|
||||
|
||||
if cfg.Hostname != "foo" {
|
||||
t.Error("Failed parsing hostname from config")
|
||||
@@ -47,12 +47,3 @@ func TestParseConfigCRLF(t *testing.T) {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConfigEmpty(t *testing.T) {
|
||||
i, e := ParseUserData(``)
|
||||
if i != nil {
|
||||
t.Error("ParseUserData of empty string returned non-nil unexpectedly")
|
||||
} else if e != nil {
|
||||
t.Error("ParseUserData of empty string returned error unexpectedly")
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ package initialize
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
@@ -29,23 +28,21 @@ func PersistScriptInWorkspace(script system.Script, workspace string) (string, e
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
relpath := strings.TrimPrefix(tmp.Name(), workspace)
|
||||
|
||||
file := system.File{
|
||||
Path: relpath,
|
||||
Path: tmp.Name(),
|
||||
RawFilePermissions: "0744",
|
||||
Content: string(script),
|
||||
Content: string(script),
|
||||
}
|
||||
|
||||
return system.WriteFile(&file, workspace)
|
||||
err = system.WriteFile(&file)
|
||||
return file.Path, err
|
||||
}
|
||||
|
||||
func PersistUnitNameInWorkspace(name string, workspace string) error {
|
||||
file := system.File{
|
||||
Path: path.Join("scripts", "unit-name"),
|
||||
Path: path.Join(workspace, "scripts", "unit-name"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: name,
|
||||
Content: name,
|
||||
}
|
||||
_, err := system.WriteFile(&file, workspace)
|
||||
return err
|
||||
return system.WriteFile(&file)
|
||||
}
|
||||
|
@@ -1,293 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type InterfaceGenerator interface {
|
||||
Name() string
|
||||
Filename() string
|
||||
Netdev() string
|
||||
Link() string
|
||||
Network() string
|
||||
Type() string
|
||||
ModprobeParams() string
|
||||
}
|
||||
|
||||
type networkInterface interface {
|
||||
InterfaceGenerator
|
||||
Children() []networkInterface
|
||||
setConfigDepth(int)
|
||||
}
|
||||
|
||||
type logicalInterface struct {
|
||||
name string
|
||||
config configMethod
|
||||
children []networkInterface
|
||||
configDepth int
|
||||
}
|
||||
|
||||
func (i *logicalInterface) Network() string {
|
||||
config := fmt.Sprintf("[Match]\nName=%s\n\n[Network]\n", i.name)
|
||||
|
||||
for _, child := range i.children {
|
||||
switch iface := child.(type) {
|
||||
case *vlanInterface:
|
||||
config += fmt.Sprintf("VLAN=%s\n", iface.name)
|
||||
case *bondInterface:
|
||||
config += fmt.Sprintf("Bond=%s\n", iface.name)
|
||||
}
|
||||
}
|
||||
|
||||
switch conf := i.config.(type) {
|
||||
case configMethodStatic:
|
||||
for _, nameserver := range conf.nameservers {
|
||||
config += fmt.Sprintf("DNS=%s\n", nameserver)
|
||||
}
|
||||
if conf.address.IP != nil {
|
||||
config += fmt.Sprintf("\n[Address]\nAddress=%s\n", conf.address.String())
|
||||
}
|
||||
for _, route := range conf.routes {
|
||||
config += fmt.Sprintf("\n[Route]\nDestination=%s\nGateway=%s\n", route.destination.String(), route.gateway)
|
||||
}
|
||||
case configMethodDHCP:
|
||||
config += "DHCP=true\n"
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (i *logicalInterface) Link() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (i *logicalInterface) Filename() string {
|
||||
return fmt.Sprintf("%02x-%s", i.configDepth, i.name)
|
||||
}
|
||||
|
||||
func (i *logicalInterface) Children() []networkInterface {
|
||||
return i.children
|
||||
}
|
||||
|
||||
func (i *logicalInterface) ModprobeParams() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (i *logicalInterface) setConfigDepth(depth int) {
|
||||
i.configDepth = depth
|
||||
}
|
||||
|
||||
type physicalInterface struct {
|
||||
logicalInterface
|
||||
}
|
||||
|
||||
func (p *physicalInterface) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *physicalInterface) Netdev() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *physicalInterface) Type() string {
|
||||
return "physical"
|
||||
}
|
||||
|
||||
type bondInterface struct {
|
||||
logicalInterface
|
||||
slaves []string
|
||||
options map[string]string
|
||||
}
|
||||
|
||||
func (b *bondInterface) Name() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
func (b *bondInterface) Netdev() string {
|
||||
return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name)
|
||||
}
|
||||
|
||||
func (b *bondInterface) Type() string {
|
||||
return "bond"
|
||||
}
|
||||
|
||||
func (b *bondInterface) ModprobeParams() string {
|
||||
params := ""
|
||||
for name, val := range b.options {
|
||||
params += fmt.Sprintf("%s=%s ", name, val)
|
||||
}
|
||||
params = strings.TrimSuffix(params, " ")
|
||||
return params
|
||||
}
|
||||
|
||||
type vlanInterface struct {
|
||||
logicalInterface
|
||||
id int
|
||||
rawDevice string
|
||||
}
|
||||
|
||||
func (v *vlanInterface) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func (v *vlanInterface) Netdev() string {
|
||||
config := fmt.Sprintf("[NetDev]\nKind=vlan\nName=%s\n", v.name)
|
||||
switch c := v.config.(type) {
|
||||
case configMethodStatic:
|
||||
if c.hwaddress != nil {
|
||||
config += fmt.Sprintf("MACAddress=%s\n", c.hwaddress)
|
||||
}
|
||||
case configMethodDHCP:
|
||||
if c.hwaddress != nil {
|
||||
config += fmt.Sprintf("MACAddress=%s\n", c.hwaddress)
|
||||
}
|
||||
}
|
||||
config += fmt.Sprintf("\n[VLAN]\nId=%d\n", v.id)
|
||||
return config
|
||||
}
|
||||
|
||||
func (v *vlanInterface) Type() string {
|
||||
return "vlan"
|
||||
}
|
||||
|
||||
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
|
||||
interfaceMap := createInterfaces(stanzas)
|
||||
linkAncestors(interfaceMap)
|
||||
markConfigDepths(interfaceMap)
|
||||
|
||||
interfaces := make([]InterfaceGenerator, 0, len(interfaceMap))
|
||||
for _, iface := range interfaceMap {
|
||||
interfaces = append(interfaces, iface)
|
||||
}
|
||||
|
||||
return interfaces
|
||||
}
|
||||
|
||||
func createInterfaces(stanzas []*stanzaInterface) map[string]networkInterface {
|
||||
interfaceMap := make(map[string]networkInterface)
|
||||
for _, iface := range stanzas {
|
||||
switch iface.kind {
|
||||
case interfaceBond:
|
||||
bondOptions := make(map[string]string)
|
||||
for _, k := range []string{"mode", "miimon", "lacp-rate"} {
|
||||
if v, ok := iface.options["bond-"+k]; ok && len(v) > 0 {
|
||||
bondOptions[k] = v[0]
|
||||
}
|
||||
}
|
||||
interfaceMap[iface.name] = &bondInterface{
|
||||
logicalInterface{
|
||||
name: iface.name,
|
||||
config: iface.configMethod,
|
||||
children: []networkInterface{},
|
||||
},
|
||||
iface.options["bond-slaves"],
|
||||
bondOptions,
|
||||
}
|
||||
for _, slave := range iface.options["bond-slaves"] {
|
||||
if _, ok := interfaceMap[slave]; !ok {
|
||||
interfaceMap[slave] = &physicalInterface{
|
||||
logicalInterface{
|
||||
name: slave,
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case interfacePhysical:
|
||||
if _, ok := iface.configMethod.(configMethodLoopback); ok {
|
||||
continue
|
||||
}
|
||||
interfaceMap[iface.name] = &physicalInterface{
|
||||
logicalInterface{
|
||||
name: iface.name,
|
||||
config: iface.configMethod,
|
||||
children: []networkInterface{},
|
||||
},
|
||||
}
|
||||
|
||||
case interfaceVLAN:
|
||||
var rawDevice string
|
||||
id, _ := strconv.Atoi(iface.options["id"][0])
|
||||
if device := iface.options["raw_device"]; len(device) == 1 {
|
||||
rawDevice = device[0]
|
||||
if _, ok := interfaceMap[rawDevice]; !ok {
|
||||
interfaceMap[rawDevice] = &physicalInterface{
|
||||
logicalInterface{
|
||||
name: rawDevice,
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
interfaceMap[iface.name] = &vlanInterface{
|
||||
logicalInterface{
|
||||
name: iface.name,
|
||||
config: iface.configMethod,
|
||||
children: []networkInterface{},
|
||||
},
|
||||
id,
|
||||
rawDevice,
|
||||
}
|
||||
}
|
||||
}
|
||||
return interfaceMap
|
||||
}
|
||||
|
||||
func linkAncestors(interfaceMap map[string]networkInterface) {
|
||||
for _, iface := range interfaceMap {
|
||||
switch i := iface.(type) {
|
||||
case *vlanInterface:
|
||||
if parent, ok := interfaceMap[i.rawDevice]; ok {
|
||||
switch p := parent.(type) {
|
||||
case *physicalInterface:
|
||||
p.children = append(p.children, iface)
|
||||
case *bondInterface:
|
||||
p.children = append(p.children, iface)
|
||||
}
|
||||
}
|
||||
case *bondInterface:
|
||||
for _, slave := range i.slaves {
|
||||
if parent, ok := interfaceMap[slave]; ok {
|
||||
switch p := parent.(type) {
|
||||
case *physicalInterface:
|
||||
p.children = append(p.children, iface)
|
||||
case *bondInterface:
|
||||
p.children = append(p.children, iface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markConfigDepths(interfaceMap map[string]networkInterface) {
|
||||
rootInterfaceMap := make(map[string]networkInterface)
|
||||
for k, v := range interfaceMap {
|
||||
rootInterfaceMap[k] = v
|
||||
}
|
||||
|
||||
for _, iface := range interfaceMap {
|
||||
for _, child := range iface.Children() {
|
||||
delete(rootInterfaceMap, child.Name())
|
||||
}
|
||||
}
|
||||
for _, iface := range rootInterfaceMap {
|
||||
setDepth(iface)
|
||||
}
|
||||
}
|
||||
|
||||
func setDepth(iface networkInterface) int {
|
||||
maxDepth := 0
|
||||
for _, child := range iface.Children() {
|
||||
if depth := setDepth(child); depth > maxDepth {
|
||||
maxDepth = depth
|
||||
}
|
||||
}
|
||||
iface.setConfigDepth(maxDepth)
|
||||
return (maxDepth + 1)
|
||||
}
|
@@ -1,492 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPhysicalInterfaceName(t *testing.T) {
|
||||
p := physicalInterface{logicalInterface{name: "testname"}}
|
||||
if p.Name() != "testname" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhysicalInterfaceNetdev(t *testing.T) {
|
||||
p := physicalInterface{}
|
||||
if p.Netdev() != "" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhysicalInterfaceLink(t *testing.T) {
|
||||
p := physicalInterface{}
|
||||
if p.Link() != "" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhysicalInterfaceNetwork(t *testing.T) {
|
||||
p := physicalInterface{logicalInterface{
|
||||
name: "testname",
|
||||
children: []networkInterface{
|
||||
&bondInterface{
|
||||
logicalInterface{
|
||||
name: "testbond1",
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
&vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testvlan1",
|
||||
},
|
||||
1,
|
||||
"",
|
||||
},
|
||||
&vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testvlan2",
|
||||
},
|
||||
1,
|
||||
"",
|
||||
},
|
||||
},
|
||||
}}
|
||||
network := `[Match]
|
||||
Name=testname
|
||||
|
||||
[Network]
|
||||
Bond=testbond1
|
||||
VLAN=testvlan1
|
||||
VLAN=testvlan2
|
||||
`
|
||||
if p.Network() != network {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondInterfaceName(t *testing.T) {
|
||||
b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
|
||||
if b.Name() != "testname" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondInterfaceNetdev(t *testing.T) {
|
||||
b := bondInterface{logicalInterface{name: "testname"}, nil, nil}
|
||||
netdev := `[NetDev]
|
||||
Kind=bond
|
||||
Name=testname
|
||||
`
|
||||
if b.Netdev() != netdev {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondInterfaceLink(t *testing.T) {
|
||||
b := bondInterface{}
|
||||
if b.Link() != "" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondInterfaceNetwork(t *testing.T) {
|
||||
b := bondInterface{
|
||||
logicalInterface{
|
||||
name: "testname",
|
||||
config: configMethodDHCP{},
|
||||
children: []networkInterface{
|
||||
&bondInterface{
|
||||
logicalInterface{
|
||||
name: "testbond1",
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
&vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testvlan1",
|
||||
},
|
||||
1,
|
||||
"",
|
||||
},
|
||||
&vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testvlan2",
|
||||
},
|
||||
1,
|
||||
"",
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
}
|
||||
network := `[Match]
|
||||
Name=testname
|
||||
|
||||
[Network]
|
||||
Bond=testbond1
|
||||
VLAN=testvlan1
|
||||
VLAN=testvlan2
|
||||
DHCP=true
|
||||
`
|
||||
if b.Network() != network {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestVLANInterfaceName(t *testing.T) {
|
||||
v := vlanInterface{logicalInterface{name: "testname"}, 1, ""}
|
||||
if v.Name() != "testname" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestVLANInterfaceNetdev(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
i vlanInterface
|
||||
l string
|
||||
}{
|
||||
{
|
||||
vlanInterface{logicalInterface{name: "testname"}, 1, ""},
|
||||
"[NetDev]\nKind=vlan\nName=testname\n\n[VLAN]\nId=1\n",
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVLANInterfaceLink(t *testing.T) {
|
||||
v := vlanInterface{}
|
||||
if v.Link() != "" {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestVLANInterfaceNetwork(t *testing.T) {
|
||||
v := vlanInterface{
|
||||
logicalInterface{
|
||||
name: "testname",
|
||||
config: configMethodStatic{
|
||||
address: net.IPNet{
|
||||
IP: []byte{192, 168, 1, 100},
|
||||
Mask: []byte{255, 255, 255, 0},
|
||||
},
|
||||
nameservers: []net.IP{
|
||||
[]byte{8, 8, 8, 8},
|
||||
},
|
||||
routes: []route{
|
||||
route{
|
||||
destination: net.IPNet{
|
||||
IP: []byte{0, 0, 0, 0},
|
||||
Mask: []byte{0, 0, 0, 0},
|
||||
},
|
||||
gateway: []byte{1, 2, 3, 4},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
0,
|
||||
"",
|
||||
}
|
||||
network := `[Match]
|
||||
Name=testname
|
||||
|
||||
[Network]
|
||||
DNS=8.8.8.8
|
||||
|
||||
[Address]
|
||||
Address=192.168.1.100/24
|
||||
|
||||
[Route]
|
||||
Destination=0.0.0.0/0
|
||||
Gateway=1.2.3.4
|
||||
`
|
||||
if v.Network() != network {
|
||||
t.Log(v.Network())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
i InterfaceGenerator
|
||||
t string
|
||||
}{
|
||||
{
|
||||
i: &physicalInterface{},
|
||||
t: "physical",
|
||||
},
|
||||
{
|
||||
i: &vlanInterface{},
|
||||
t: "vlan",
|
||||
},
|
||||
{
|
||||
i: &bondInterface{},
|
||||
t: "bond",
|
||||
},
|
||||
} {
|
||||
if tp := tt.i.Type(); tp != tt.t {
|
||||
t.Fatalf("bad type (%q): got %s, want %s", tt.i, tp, tt.t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModprobeParams(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
i InterfaceGenerator
|
||||
p string
|
||||
}{
|
||||
{
|
||||
i: &physicalInterface{},
|
||||
p: "",
|
||||
},
|
||||
{
|
||||
i: &vlanInterface{},
|
||||
p: "",
|
||||
},
|
||||
{
|
||||
i: &bondInterface{
|
||||
logicalInterface{},
|
||||
nil,
|
||||
map[string]string{
|
||||
"a": "1",
|
||||
"b": "2",
|
||||
},
|
||||
},
|
||||
p: "a=1 b=2",
|
||||
},
|
||||
} {
|
||||
if p := tt.i.ModprobeParams(); p != tt.p {
|
||||
t.Fatalf("bad params (%q): got %s, want %s", tt.i, p, tt.p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildInterfacesLo(t *testing.T) {
|
||||
stanzas := []*stanzaInterface{
|
||||
&stanzaInterface{
|
||||
name: "lo",
|
||||
kind: interfacePhysical,
|
||||
auto: false,
|
||||
configMethod: configMethodLoopback{},
|
||||
options: map[string][]string{},
|
||||
},
|
||||
}
|
||||
interfaces := buildInterfaces(stanzas)
|
||||
if len(interfaces) != 0 {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildInterfacesBlindBond(t *testing.T) {
|
||||
stanzas := []*stanzaInterface{
|
||||
{
|
||||
name: "bond0",
|
||||
kind: interfaceBond,
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{
|
||||
"bond-slaves": []string{"eth0"},
|
||||
},
|
||||
},
|
||||
}
|
||||
interfaces := buildInterfaces(stanzas)
|
||||
bond0 := &bondInterface{
|
||||
logicalInterface{
|
||||
name: "bond0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 0,
|
||||
},
|
||||
[]string{"eth0"},
|
||||
map[string]string{},
|
||||
}
|
||||
eth0 := &physicalInterface{
|
||||
logicalInterface{
|
||||
name: "eth0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{bond0},
|
||||
configDepth: 1,
|
||||
},
|
||||
}
|
||||
expect := []InterfaceGenerator{bond0, eth0}
|
||||
if !reflect.DeepEqual(interfaces, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildInterfacesBlindVLAN(t *testing.T) {
|
||||
stanzas := []*stanzaInterface{
|
||||
{
|
||||
name: "vlan0",
|
||||
kind: interfaceVLAN,
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{
|
||||
"id": []string{"0"},
|
||||
"raw_device": []string{"eth0"},
|
||||
},
|
||||
},
|
||||
}
|
||||
interfaces := buildInterfaces(stanzas)
|
||||
vlan0 := &vlanInterface{
|
||||
logicalInterface{
|
||||
name: "vlan0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 0,
|
||||
},
|
||||
0,
|
||||
"eth0",
|
||||
}
|
||||
eth0 := &physicalInterface{
|
||||
logicalInterface{
|
||||
name: "eth0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{vlan0},
|
||||
configDepth: 1,
|
||||
},
|
||||
}
|
||||
expect := []InterfaceGenerator{eth0, vlan0}
|
||||
if !reflect.DeepEqual(interfaces, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildInterfaces(t *testing.T) {
|
||||
stanzas := []*stanzaInterface{
|
||||
&stanzaInterface{
|
||||
name: "eth0",
|
||||
kind: interfacePhysical,
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{},
|
||||
},
|
||||
&stanzaInterface{
|
||||
name: "bond0",
|
||||
kind: interfaceBond,
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{
|
||||
"bond-slaves": []string{"eth0"},
|
||||
"bond-mode": []string{"4"},
|
||||
"bond-miimon": []string{"100"},
|
||||
},
|
||||
},
|
||||
&stanzaInterface{
|
||||
name: "bond1",
|
||||
kind: interfaceBond,
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{
|
||||
"bond-slaves": []string{"bond0"},
|
||||
},
|
||||
},
|
||||
&stanzaInterface{
|
||||
name: "vlan0",
|
||||
kind: interfaceVLAN,
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{
|
||||
"id": []string{"0"},
|
||||
"raw_device": []string{"eth0"},
|
||||
},
|
||||
},
|
||||
&stanzaInterface{
|
||||
name: "vlan1",
|
||||
kind: interfaceVLAN,
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{
|
||||
"id": []string{"1"},
|
||||
"raw_device": []string{"bond0"},
|
||||
},
|
||||
},
|
||||
}
|
||||
interfaces := buildInterfaces(stanzas)
|
||||
vlan1 := &vlanInterface{
|
||||
logicalInterface{
|
||||
name: "vlan1",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 0,
|
||||
},
|
||||
1,
|
||||
"bond0",
|
||||
}
|
||||
vlan0 := &vlanInterface{
|
||||
logicalInterface{
|
||||
name: "vlan0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 0,
|
||||
},
|
||||
0,
|
||||
"eth0",
|
||||
}
|
||||
bond1 := &bondInterface{
|
||||
logicalInterface{
|
||||
name: "bond1",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{},
|
||||
configDepth: 0,
|
||||
},
|
||||
[]string{"bond0"},
|
||||
map[string]string{},
|
||||
}
|
||||
bond0 := &bondInterface{
|
||||
logicalInterface{
|
||||
name: "bond0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{bond1, vlan1},
|
||||
configDepth: 1,
|
||||
},
|
||||
[]string{"eth0"},
|
||||
map[string]string{
|
||||
"mode": "4",
|
||||
"miimon": "100",
|
||||
},
|
||||
}
|
||||
eth0 := &physicalInterface{
|
||||
logicalInterface{
|
||||
name: "eth0",
|
||||
config: configMethodManual{},
|
||||
children: []networkInterface{bond0, vlan0},
|
||||
configDepth: 2,
|
||||
},
|
||||
}
|
||||
expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1}
|
||||
if !reflect.DeepEqual(interfaces, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilename(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
i logicalInterface
|
||||
f string
|
||||
}{
|
||||
{logicalInterface{name: "iface", configDepth: 0}, "00-iface"},
|
||||
{logicalInterface{name: "iface", configDepth: 9}, "09-iface"},
|
||||
{logicalInterface{name: "iface", configDepth: 10}, "0a-iface"},
|
||||
{logicalInterface{name: "iface", configDepth: 53}, "35-iface"},
|
||||
} {
|
||||
if tt.i.Filename() != tt.f {
|
||||
t.Fatalf("bad filename (%q): got %q, want %q", tt.i, tt.i.Filename(), tt.f)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ProcessDebianNetconf(config string) ([]InterfaceGenerator, error) {
|
||||
lines := formatConfig(config)
|
||||
stanzas, err := parseStanzas(lines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
interfaces := make([]*stanzaInterface, 0, len(stanzas))
|
||||
for _, stanza := range stanzas {
|
||||
switch s := stanza.(type) {
|
||||
case *stanzaInterface:
|
||||
interfaces = append(interfaces, s)
|
||||
}
|
||||
}
|
||||
|
||||
return buildInterfaces(interfaces), nil
|
||||
}
|
||||
|
||||
func formatConfig(config string) []string {
|
||||
lines := []string{}
|
||||
config = strings.Replace(config, "\\\n", "", -1)
|
||||
for config != "" {
|
||||
split := strings.SplitN(config, "\n", 2)
|
||||
line := strings.TrimSpace(split[0])
|
||||
|
||||
if len(split) == 2 {
|
||||
config = split[1]
|
||||
} else {
|
||||
config = ""
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return lines
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatConfigs(t *testing.T) {
|
||||
for in, n := range map[string]int{
|
||||
"": 0,
|
||||
"line1\\\nis long": 1,
|
||||
"#comment": 0,
|
||||
"#comment\\\ncomment": 0,
|
||||
" #comment \\\n comment\nline 1\nline 2\\\n is long": 2,
|
||||
} {
|
||||
lines := formatConfig(in)
|
||||
if len(lines) != n {
|
||||
t.Fatalf("bad number of lines for config %q: got %d, want %d", in, len(lines), n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessDebianNetconf(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
in string
|
||||
fail bool
|
||||
n int
|
||||
}{
|
||||
{"", false, 0},
|
||||
{"iface", true, -1},
|
||||
{"auto eth1\nauto eth2", false, 0},
|
||||
{"iface eth1 inet manual", false, 1},
|
||||
} {
|
||||
interfaces, err := ProcessDebianNetconf(tt.in)
|
||||
failed := err != nil
|
||||
if tt.fail != failed {
|
||||
t.Fatalf("bad failure state for %q: got %b, want %b", failed, tt.fail)
|
||||
}
|
||||
if tt.n != -1 && tt.n != len(interfaces) {
|
||||
t.Fatalf("bad number of interfaces for %q: got %d, want %q", tt.in, len(interfaces), tt.n)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,321 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type stanza interface{}
|
||||
|
||||
type stanzaAuto struct {
|
||||
interfaces []string
|
||||
}
|
||||
|
||||
type stanzaInterface struct {
|
||||
name string
|
||||
kind interfaceKind
|
||||
auto bool
|
||||
configMethod configMethod
|
||||
options map[string][]string
|
||||
}
|
||||
|
||||
type interfaceKind int
|
||||
|
||||
const (
|
||||
interfaceBond = interfaceKind(iota)
|
||||
interfacePhysical
|
||||
interfaceVLAN
|
||||
)
|
||||
|
||||
type route struct {
|
||||
destination net.IPNet
|
||||
gateway net.IP
|
||||
}
|
||||
|
||||
type configMethod interface{}
|
||||
|
||||
type configMethodStatic struct {
|
||||
address net.IPNet
|
||||
nameservers []net.IP
|
||||
routes []route
|
||||
hwaddress net.HardwareAddr
|
||||
}
|
||||
|
||||
type configMethodLoopback struct{}
|
||||
|
||||
type configMethodManual struct{}
|
||||
|
||||
type configMethodDHCP struct {
|
||||
hwaddress net.HardwareAddr
|
||||
}
|
||||
|
||||
func parseStanzas(lines []string) (stanzas []stanza, err error) {
|
||||
rawStanzas, err := splitStanzas(lines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stanzas = make([]stanza, 0, len(rawStanzas))
|
||||
for _, rawStanza := range rawStanzas {
|
||||
if stanza, err := parseStanza(rawStanza); err == nil {
|
||||
stanzas = append(stanzas, stanza)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
autos := make([]string, 0)
|
||||
interfaceMap := make(map[string]*stanzaInterface)
|
||||
for _, stanza := range stanzas {
|
||||
switch c := stanza.(type) {
|
||||
case *stanzaAuto:
|
||||
autos = append(autos, c.interfaces...)
|
||||
case *stanzaInterface:
|
||||
interfaceMap[c.name] = c
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the auto attribute
|
||||
for _, auto := range autos {
|
||||
if iface, ok := interfaceMap[auto]; ok {
|
||||
iface.auto = true
|
||||
}
|
||||
}
|
||||
|
||||
return stanzas, nil
|
||||
}
|
||||
|
||||
func splitStanzas(lines []string) ([][]string, error) {
|
||||
var curStanza []string
|
||||
stanzas := make([][]string, 0)
|
||||
for _, line := range lines {
|
||||
if isStanzaStart(line) {
|
||||
if curStanza != nil {
|
||||
stanzas = append(stanzas, curStanza)
|
||||
}
|
||||
curStanza = []string{line}
|
||||
} else if curStanza != nil {
|
||||
curStanza = append(curStanza, line)
|
||||
} else {
|
||||
return nil, fmt.Errorf("missing stanza start %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
if curStanza != nil {
|
||||
stanzas = append(stanzas, curStanza)
|
||||
}
|
||||
|
||||
return stanzas, nil
|
||||
}
|
||||
|
||||
func isStanzaStart(line string) bool {
|
||||
switch strings.Split(line, " ")[0] {
|
||||
case "auto":
|
||||
fallthrough
|
||||
case "iface":
|
||||
fallthrough
|
||||
case "mapping":
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "allow-") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func parseStanza(rawStanza []string) (stanza, error) {
|
||||
if len(rawStanza) == 0 {
|
||||
panic("empty stanza")
|
||||
}
|
||||
tokens := strings.Fields(rawStanza[0])
|
||||
if len(tokens) < 2 {
|
||||
return nil, fmt.Errorf("malformed stanza start %q", rawStanza[0])
|
||||
}
|
||||
|
||||
kind := tokens[0]
|
||||
attributes := tokens[1:]
|
||||
|
||||
switch kind {
|
||||
case "auto":
|
||||
return parseAutoStanza(attributes, rawStanza[1:])
|
||||
case "iface":
|
||||
return parseInterfaceStanza(attributes, rawStanza[1:])
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown stanza %q", kind)
|
||||
}
|
||||
}
|
||||
|
||||
func parseAutoStanza(attributes []string, options []string) (*stanzaAuto, error) {
|
||||
return &stanzaAuto{interfaces: attributes}, nil
|
||||
}
|
||||
|
||||
func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterface, error) {
|
||||
if len(attributes) != 3 {
|
||||
return nil, fmt.Errorf("incorrect number of attributes")
|
||||
}
|
||||
|
||||
iface := attributes[0]
|
||||
confMethod := attributes[2]
|
||||
|
||||
optionMap := make(map[string][]string, 0)
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, "post-up") {
|
||||
tokens := strings.SplitAfterN(option, " ", 2)
|
||||
if len(tokens) != 2 {
|
||||
continue
|
||||
}
|
||||
if v, ok := optionMap["post-up"]; ok {
|
||||
optionMap["post-up"] = append(v, tokens[1])
|
||||
} else {
|
||||
optionMap["post-up"] = []string{tokens[1]}
|
||||
}
|
||||
} else if strings.HasPrefix(option, "pre-down") {
|
||||
tokens := strings.SplitAfterN(option, " ", 2)
|
||||
if len(tokens) != 2 {
|
||||
continue
|
||||
}
|
||||
if v, ok := optionMap["pre-down"]; ok {
|
||||
optionMap["pre-down"] = append(v, tokens[1])
|
||||
} else {
|
||||
optionMap["pre-down"] = []string{tokens[1]}
|
||||
}
|
||||
} else {
|
||||
tokens := strings.Fields(option)
|
||||
optionMap[tokens[0]] = tokens[1:]
|
||||
}
|
||||
}
|
||||
|
||||
var conf configMethod
|
||||
switch confMethod {
|
||||
case "static":
|
||||
config := configMethodStatic{
|
||||
routes: make([]route, 0),
|
||||
nameservers: make([]net.IP, 0),
|
||||
}
|
||||
if addresses, ok := optionMap["address"]; ok {
|
||||
if len(addresses) == 1 {
|
||||
config.address.IP = net.ParseIP(addresses[0])
|
||||
}
|
||||
}
|
||||
if netmasks, ok := optionMap["netmask"]; ok {
|
||||
if len(netmasks) == 1 {
|
||||
config.address.Mask = net.IPMask(net.ParseIP(netmasks[0]).To4())
|
||||
}
|
||||
}
|
||||
if config.address.IP == nil || config.address.Mask == nil {
|
||||
return nil, fmt.Errorf("malformed static network config for %q", iface)
|
||||
}
|
||||
if gateways, ok := optionMap["gateway"]; ok {
|
||||
if len(gateways) == 1 {
|
||||
config.routes = append(config.routes, route{
|
||||
destination: net.IPNet{
|
||||
IP: net.IPv4(0, 0, 0, 0),
|
||||
Mask: net.IPv4Mask(0, 0, 0, 0),
|
||||
},
|
||||
gateway: net.ParseIP(gateways[0]),
|
||||
})
|
||||
}
|
||||
}
|
||||
if hwaddress, err := parseHwaddress(optionMap, iface); err == nil {
|
||||
config.hwaddress = hwaddress
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
for _, nameserver := range optionMap["dns-nameservers"] {
|
||||
config.nameservers = append(config.nameservers, net.ParseIP(nameserver))
|
||||
}
|
||||
for _, postup := range optionMap["post-up"] {
|
||||
if strings.HasPrefix(postup, "route add") {
|
||||
route := route{}
|
||||
fields := strings.Fields(postup)
|
||||
for i, field := range fields[:len(fields)-1] {
|
||||
switch field {
|
||||
case "-net":
|
||||
route.destination.IP = net.ParseIP(fields[i+1])
|
||||
case "netmask":
|
||||
route.destination.Mask = net.IPMask(net.ParseIP(fields[i+1]).To4())
|
||||
case "gw":
|
||||
route.gateway = net.ParseIP(fields[i+1])
|
||||
}
|
||||
}
|
||||
if route.destination.IP != nil && route.destination.Mask != nil && route.gateway != nil {
|
||||
config.routes = append(config.routes, route)
|
||||
}
|
||||
}
|
||||
}
|
||||
conf = config
|
||||
case "loopback":
|
||||
conf = configMethodLoopback{}
|
||||
case "manual":
|
||||
conf = configMethodManual{}
|
||||
case "dhcp":
|
||||
config := configMethodDHCP{}
|
||||
if hwaddress, err := parseHwaddress(optionMap, iface); err == nil {
|
||||
config.hwaddress = hwaddress
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
conf = config
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid config method %q", confMethod)
|
||||
}
|
||||
|
||||
if _, ok := optionMap["vlan_raw_device"]; ok {
|
||||
return parseVLANStanza(iface, conf, attributes, optionMap)
|
||||
}
|
||||
|
||||
if strings.Contains(iface, ".") {
|
||||
return parseVLANStanza(iface, conf, attributes, optionMap)
|
||||
}
|
||||
|
||||
if _, ok := optionMap["bond-slaves"]; ok {
|
||||
return parseBondStanza(iface, conf, attributes, optionMap)
|
||||
}
|
||||
|
||||
return parsePhysicalStanza(iface, conf, attributes, optionMap)
|
||||
}
|
||||
|
||||
func parseHwaddress(options map[string][]string, iface string) (net.HardwareAddr, error) {
|
||||
if hwaddress, ok := options["hwaddress"]; ok && len(hwaddress) == 2 {
|
||||
switch hwaddress[0] {
|
||||
case "ether":
|
||||
if address, err := net.ParseMAC(hwaddress[1]); err == nil {
|
||||
return address, nil
|
||||
}
|
||||
return nil, fmt.Errorf("malformed hwaddress option for %q", iface)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
|
||||
return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
|
||||
}
|
||||
|
||||
func parsePhysicalStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
|
||||
return &stanzaInterface{name: iface, kind: interfacePhysical, configMethod: conf, options: options}, nil
|
||||
}
|
||||
|
||||
func parseVLANStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
|
||||
var id string
|
||||
if strings.Contains(iface, ".") {
|
||||
tokens := strings.Split(iface, ".")
|
||||
id = tokens[len(tokens)-1]
|
||||
} else if strings.HasPrefix(iface, "vlan") {
|
||||
id = strings.TrimPrefix(iface, "vlan")
|
||||
} else {
|
||||
return nil, fmt.Errorf("malformed vlan name %q", iface)
|
||||
}
|
||||
|
||||
if _, err := strconv.Atoi(id); err != nil {
|
||||
return nil, fmt.Errorf("malformed vlan name %q", iface)
|
||||
}
|
||||
options["id"] = []string{id}
|
||||
options["raw_device"] = options["vlan_raw_device"]
|
||||
|
||||
return &stanzaInterface{name: iface, kind: interfaceVLAN, configMethod: conf, options: options}, nil
|
||||
}
|
@@ -1,540 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitStanzasNoParent(t *testing.T) {
|
||||
in := []string{"test"}
|
||||
e := "missing stanza start"
|
||||
_, err := splitStanzas(in)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), e) {
|
||||
t.Fatalf("bad error for splitStanzas(%q): got %q, want %q", in, err, e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadParseStanzas(t *testing.T) {
|
||||
for in, e := range map[string]string{
|
||||
"": "missing stanza start",
|
||||
"iface": "malformed stanza start",
|
||||
"allow-?? unknown": "unknown stanza",
|
||||
} {
|
||||
_, err := parseStanzas([]string{in})
|
||||
if err == nil || !strings.HasPrefix(err.Error(), e) {
|
||||
t.Fatalf("bad error for parseStanzas(%q): got %q, want %q", in, err, e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadParseInterfaceStanza(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
in []string
|
||||
opts []string
|
||||
e string
|
||||
}{
|
||||
{[]string{}, nil, "incorrect number of attributes"},
|
||||
{[]string{"eth", "inet", "invalid"}, nil, "invalid config method"},
|
||||
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100"}, "malformed static network config"},
|
||||
{[]string{"eth", "inet", "static"}, []string{"netmask 255.255.255.0"}, "malformed static network config"},
|
||||
{[]string{"eth", "inet", "static"}, []string{"address invalid", "netmask 255.255.255.0"}, "malformed static network config"},
|
||||
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100", "netmask invalid"}, "malformed static network config"},
|
||||
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100", "netmask 255.255.255.0", "hwaddress ether NotAnAddress"}, "malformed hwaddress option"},
|
||||
{[]string{"eth", "inet", "dhcp"}, []string{"hwaddress ether NotAnAddress"}, "malformed hwaddress option"},
|
||||
} {
|
||||
_, err := parseInterfaceStanza(tt.in, tt.opts)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), tt.e) {
|
||||
t.Fatalf("bad error parsing interface stanza %q: got %q, want %q", tt.in, err.Error(), tt.e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadParseVLANStanzas(t *testing.T) {
|
||||
conf := configMethodManual{}
|
||||
options := map[string][]string{}
|
||||
for _, in := range []string{"myvlan", "eth.vlan"} {
|
||||
_, err := parseVLANStanza(in, conf, nil, options)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), "malformed vlan name") {
|
||||
t.Fatalf("did not error on bad vlan %q", in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitStanzas(t *testing.T) {
|
||||
expect := [][]string{
|
||||
{"auto lo"},
|
||||
{"iface eth1", "option: 1"},
|
||||
{"mapping"},
|
||||
{"allow-"},
|
||||
}
|
||||
lines := make([]string, 0, 5)
|
||||
for _, stanza := range expect {
|
||||
for _, line := range stanza {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
|
||||
stanzas, err := splitStanzas(lines)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
for i, stanza := range stanzas {
|
||||
if len(stanza) != len(expect[i]) {
|
||||
t.FailNow()
|
||||
}
|
||||
for j, line := range stanza {
|
||||
if line != expect[i][j] {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStanzaNil(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatal("parseStanza(nil) did not panic")
|
||||
}
|
||||
}()
|
||||
parseStanza(nil)
|
||||
}
|
||||
|
||||
func TestParseStanzaSuccess(t *testing.T) {
|
||||
for _, in := range []string{
|
||||
"auto a",
|
||||
"iface a inet manual",
|
||||
} {
|
||||
if _, err := parseStanza([]string{in}); err != nil {
|
||||
t.Fatalf("unexpected error parsing stanza %q: %s", in, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutoStanza(t *testing.T) {
|
||||
interfaces := []string{"test", "attribute"}
|
||||
stanza, err := parseAutoStanza(interfaces, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing auto stanza %q: %s", interfaces, err)
|
||||
}
|
||||
if !reflect.DeepEqual(stanza.interfaces, interfaces) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBondStanzaNoSlaves(t *testing.T) {
|
||||
bond, err := parseBondStanza("", nil, nil, map[string][]string{})
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if bond.options["bond-slaves"] != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBondStanza(t *testing.T) {
|
||||
conf := configMethodManual{}
|
||||
options := map[string][]string{
|
||||
"bond-slaves": []string{"1", "2"},
|
||||
}
|
||||
bond, err := parseBondStanza("test", conf, nil, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if bond.name != "test" {
|
||||
t.FailNow()
|
||||
}
|
||||
if bond.kind != interfaceBond {
|
||||
t.FailNow()
|
||||
}
|
||||
if bond.configMethod != conf {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePhysicalStanza(t *testing.T) {
|
||||
conf := configMethodManual{}
|
||||
options := map[string][]string{
|
||||
"a": []string{"1", "2"},
|
||||
"b": []string{"1"},
|
||||
}
|
||||
physical, err := parsePhysicalStanza("test", conf, nil, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if physical.name != "test" {
|
||||
t.FailNow()
|
||||
}
|
||||
if physical.kind != interfacePhysical {
|
||||
t.FailNow()
|
||||
}
|
||||
if physical.configMethod != conf {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(physical.options, options) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVLANStanzas(t *testing.T) {
|
||||
conf := configMethodManual{}
|
||||
options := map[string][]string{}
|
||||
for _, in := range []string{"vlan25", "eth.25"} {
|
||||
vlan, err := parseVLANStanza(in, conf, nil, options)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from parseVLANStanza(%q): %s", in, err)
|
||||
}
|
||||
if !reflect.DeepEqual(vlan.options["id"], []string{"25"}) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaStaticAddress(t *testing.T) {
|
||||
options := []string{"address 192.168.1.100", "netmask 255.255.255.0"}
|
||||
expect := net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 100),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
}
|
||||
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
static, ok := iface.configMethod.(configMethodStatic)
|
||||
if !ok {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(static.address, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaStaticGateway(t *testing.T) {
|
||||
options := []string{"address 192.168.1.100", "netmask 255.255.255.0", "gateway 192.168.1.1"}
|
||||
expect := []route{
|
||||
{
|
||||
destination: net.IPNet{
|
||||
IP: net.IPv4(0, 0, 0, 0),
|
||||
Mask: net.IPv4Mask(0, 0, 0, 0),
|
||||
},
|
||||
gateway: net.IPv4(192, 168, 1, 1),
|
||||
},
|
||||
}
|
||||
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
static, ok := iface.configMethod.(configMethodStatic)
|
||||
if !ok {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(static.routes, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaStaticDNS(t *testing.T) {
|
||||
options := []string{"address 192.168.1.100", "netmask 255.255.255.0", "dns-nameservers 192.168.1.10 192.168.1.11 192.168.1.12"}
|
||||
expect := []net.IP{
|
||||
net.IPv4(192, 168, 1, 10),
|
||||
net.IPv4(192, 168, 1, 11),
|
||||
net.IPv4(192, 168, 1, 12),
|
||||
}
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
static, ok := iface.configMethod.(configMethodStatic)
|
||||
if !ok {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(static.nameservers, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadParseInterfaceStanzasStaticPostUp(t *testing.T) {
|
||||
for _, in := range []string{
|
||||
"post-up invalid",
|
||||
"post-up route add",
|
||||
"post-up route add -net",
|
||||
"post-up route add gw",
|
||||
"post-up route add netmask",
|
||||
"gateway",
|
||||
"gateway 192.168.1.1 192.168.1.2",
|
||||
} {
|
||||
options := []string{"address 192.168.1.100", "netmask 255.255.255.0", in}
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
|
||||
if err != nil {
|
||||
t.Fatalf("parseInterfaceStanza with options %s got unexpected error", options)
|
||||
}
|
||||
static, ok := iface.configMethod.(configMethodStatic)
|
||||
if !ok {
|
||||
t.Fatalf("parseInterfaceStanza with options %s did not return configMethodStatic", options)
|
||||
}
|
||||
if len(static.routes) != 0 {
|
||||
t.Fatalf("parseInterfaceStanza with options %s did not return zero-length static routes", options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaStaticPostUp(t *testing.T) {
|
||||
options := []string{
|
||||
"address 192.168.1.100",
|
||||
"netmask 255.255.255.0",
|
||||
"post-up route add gw 192.168.1.1 -net 192.168.1.0 netmask 255.255.255.0",
|
||||
}
|
||||
expect := []route{
|
||||
{
|
||||
destination: net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 0),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
},
|
||||
gateway: net.IPv4(192, 168, 1, 1),
|
||||
},
|
||||
}
|
||||
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
static, ok := iface.configMethod.(configMethodStatic)
|
||||
if !ok {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(static.routes, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaLoopback(t *testing.T) {
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "loopback"}, nil)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if _, ok := iface.configMethod.(configMethodLoopback); !ok {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaManual(t *testing.T) {
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, nil)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if _, ok := iface.configMethod.(configMethodManual); !ok {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaDHCP(t *testing.T) {
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "dhcp"}, nil)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if _, ok := iface.configMethod.(configMethodDHCP); !ok {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaPostUpOption(t *testing.T) {
|
||||
options := []string{
|
||||
"post-up",
|
||||
"post-up 1 2",
|
||||
"post-up 3 4",
|
||||
}
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(iface.options["post-up"], []string{"1 2", "3 4"}) {
|
||||
t.Log(iface.options["post-up"])
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaPreDownOption(t *testing.T) {
|
||||
options := []string{
|
||||
"pre-down",
|
||||
"pre-down 3",
|
||||
"pre-down 4",
|
||||
}
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(iface.options["pre-down"], []string{"3", "4"}) {
|
||||
t.Log(iface.options["pre-down"])
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaEmptyOption(t *testing.T) {
|
||||
options := []string{
|
||||
"test",
|
||||
}
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(iface.options["test"], []string{}) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaOptions(t *testing.T) {
|
||||
options := []string{
|
||||
"test1 1",
|
||||
"test2 2 3",
|
||||
"test1 5 6",
|
||||
}
|
||||
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(iface.options["test1"], []string{"5", "6"}) {
|
||||
t.Log(iface.options["test1"])
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(iface.options["test2"], []string{"2", "3"}) {
|
||||
t.Log(iface.options["test2"])
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaHwaddress(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
attr []string
|
||||
opt []string
|
||||
hw net.HardwareAddr
|
||||
}{
|
||||
{
|
||||
[]string{"mybond", "inet", "dhcp"},
|
||||
[]string{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]string{"mybond", "inet", "dhcp"},
|
||||
[]string{"hwaddress ether 00:01:02:03:04:05"},
|
||||
net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}),
|
||||
},
|
||||
{
|
||||
[]string{"mybond", "inet", "static"},
|
||||
[]string{"hwaddress ether 00:01:02:03:04:05", "address 192.168.1.100", "netmask 255.255.255.0"},
|
||||
net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}),
|
||||
},
|
||||
} {
|
||||
iface, err := parseInterfaceStanza(tt.attr, tt.opt)
|
||||
if err != nil {
|
||||
t.Fatalf("error in parseInterfaceStanza (%q, %q): %q", tt.attr, tt.opt, err)
|
||||
}
|
||||
switch c := iface.configMethod.(type) {
|
||||
case configMethodStatic:
|
||||
if !reflect.DeepEqual(c.hwaddress, tt.hw) {
|
||||
t.Fatalf("bad hwaddress (%q, %q): got %q, want %q", tt.attr, tt.opt, c.hwaddress, tt.hw)
|
||||
}
|
||||
case configMethodDHCP:
|
||||
if !reflect.DeepEqual(c.hwaddress, tt.hw) {
|
||||
t.Fatalf("bad hwaddress (%q, %q): got %q, want %q", tt.attr, tt.opt, c.hwaddress, tt.hw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaBond(t *testing.T) {
|
||||
iface, err := parseInterfaceStanza([]string{"mybond", "inet", "manual"}, []string{"bond-slaves eth"})
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if iface.kind != interfaceBond {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaVLANName(t *testing.T) {
|
||||
iface, err := parseInterfaceStanza([]string{"eth0.1", "inet", "manual"}, nil)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if iface.kind != interfaceVLAN {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterfaceStanzaVLANOption(t *testing.T) {
|
||||
iface, err := parseInterfaceStanza([]string{"vlan1", "inet", "manual"}, []string{"vlan_raw_device eth"})
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if iface.kind != interfaceVLAN {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStanzasNone(t *testing.T) {
|
||||
stanzas, err := parseStanzas(nil)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if len(stanzas) != 0 {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStanzas(t *testing.T) {
|
||||
lines := []string{
|
||||
"auto lo",
|
||||
"iface lo inet loopback",
|
||||
"iface eth1 inet manual",
|
||||
"iface eth2 inet manual",
|
||||
"iface eth3 inet manual",
|
||||
"auto eth1 eth3",
|
||||
}
|
||||
expect := []stanza{
|
||||
&stanzaAuto{
|
||||
interfaces: []string{"lo"},
|
||||
},
|
||||
&stanzaInterface{
|
||||
name: "lo",
|
||||
kind: interfacePhysical,
|
||||
auto: true,
|
||||
configMethod: configMethodLoopback{},
|
||||
options: map[string][]string{},
|
||||
},
|
||||
&stanzaInterface{
|
||||
name: "eth1",
|
||||
kind: interfacePhysical,
|
||||
auto: true,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{},
|
||||
},
|
||||
&stanzaInterface{
|
||||
name: "eth2",
|
||||
kind: interfacePhysical,
|
||||
auto: false,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{},
|
||||
},
|
||||
&stanzaInterface{
|
||||
name: "eth3",
|
||||
kind: interfacePhysical,
|
||||
auto: true,
|
||||
configMethod: configMethodManual{},
|
||||
options: map[string][]string{},
|
||||
},
|
||||
&stanzaAuto{
|
||||
interfaces: []string{"eth1", "eth3"},
|
||||
},
|
||||
}
|
||||
stanzas, err := parseStanzas(lines)
|
||||
if err != err {
|
||||
t.FailNow()
|
||||
}
|
||||
if !reflect.DeepEqual(stanzas, expect) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
@@ -1,161 +0,0 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
neturl "net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
HTTP_2xx = 2
|
||||
HTTP_4xx = 4
|
||||
)
|
||||
|
||||
type Err error
|
||||
|
||||
type ErrTimeout struct {
|
||||
Err
|
||||
}
|
||||
|
||||
type ErrNotFound struct {
|
||||
Err
|
||||
}
|
||||
|
||||
type ErrInvalid struct {
|
||||
Err
|
||||
}
|
||||
|
||||
type ErrServer struct {
|
||||
Err
|
||||
}
|
||||
|
||||
type ErrNetwork struct {
|
||||
Err
|
||||
}
|
||||
|
||||
type HttpClient struct {
|
||||
// Maximum exp backoff duration. Defaults to 5 seconds
|
||||
MaxBackoff time.Duration
|
||||
|
||||
// Maximum number of connection retries. Defaults to 15
|
||||
MaxRetries int
|
||||
|
||||
// HTTP client timeout, this is suggested to be low since exponential
|
||||
// backoff will kick off too. Defaults to 2 seconds
|
||||
Timeout time.Duration
|
||||
|
||||
// Whether or not to skip TLS verification. Defaults to false
|
||||
SkipTLS bool
|
||||
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type Getter interface {
|
||||
Get(string) ([]byte, error)
|
||||
GetRetry(string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewHttpClient() *HttpClient {
|
||||
hc := &HttpClient{
|
||||
MaxBackoff: time.Second * 5,
|
||||
MaxRetries: 15,
|
||||
Timeout: time.Duration(2) * time.Second,
|
||||
SkipTLS: false,
|
||||
}
|
||||
|
||||
// We need to create our own client in order to add timeout support.
|
||||
// TODO(c4milo) Replace it once Go 1.3 is officially used by CoreOS
|
||||
// More info: https://code.google.com/p/go/source/detail?r=ada6f2d5f99f
|
||||
hc.client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: hc.SkipTLS,
|
||||
},
|
||||
Dial: func(network, addr string) (net.Conn, error) {
|
||||
deadline := time.Now().Add(hc.Timeout)
|
||||
c, err := net.DialTimeout(network, addr, hc.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.SetDeadline(deadline)
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return hc
|
||||
}
|
||||
|
||||
func ExpBackoff(interval, max time.Duration) time.Duration {
|
||||
interval = interval * 2
|
||||
if interval > max {
|
||||
interval = max
|
||||
}
|
||||
return interval
|
||||
}
|
||||
|
||||
// GetRetry fetches a given URL with support for exponential backoff and maximum retries
|
||||
func (h *HttpClient) GetRetry(rawurl string) ([]byte, error) {
|
||||
if rawurl == "" {
|
||||
return nil, ErrInvalid{errors.New("URL is empty. Skipping.")}
|
||||
}
|
||||
|
||||
url, err := neturl.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, ErrInvalid{err}
|
||||
}
|
||||
|
||||
// Unfortunately, url.Parse is too generic to throw errors if a URL does not
|
||||
// have a valid HTTP scheme. So, we have to do this extra validation
|
||||
if !strings.HasPrefix(url.Scheme, "http") {
|
||||
return nil, ErrInvalid{fmt.Errorf("URL %s does not have a valid HTTP scheme. Skipping.", rawurl)}
|
||||
}
|
||||
|
||||
dataURL := url.String()
|
||||
|
||||
duration := 50 * time.Millisecond
|
||||
for retry := 1; retry <= h.MaxRetries; retry++ {
|
||||
log.Printf("Fetching data from %s. Attempt #%d", dataURL, retry)
|
||||
|
||||
data, err := h.Get(dataURL)
|
||||
switch err.(type) {
|
||||
case ErrNetwork:
|
||||
log.Printf(err.Error())
|
||||
case ErrServer:
|
||||
log.Printf(err.Error())
|
||||
case ErrNotFound:
|
||||
return data, err
|
||||
default:
|
||||
return data, err
|
||||
}
|
||||
|
||||
duration = ExpBackoff(duration, h.MaxBackoff)
|
||||
log.Printf("Sleeping for %v...", duration)
|
||||
time.Sleep(duration)
|
||||
}
|
||||
|
||||
return nil, ErrTimeout{fmt.Errorf("Unable to fetch data. Maximum retries reached: %d", h.MaxRetries)}
|
||||
}
|
||||
|
||||
func (h *HttpClient) Get(dataURL string) ([]byte, error) {
|
||||
if resp, err := h.client.Get(dataURL); err == nil {
|
||||
defer resp.Body.Close()
|
||||
switch resp.StatusCode / 100 {
|
||||
case HTTP_2xx:
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
case HTTP_4xx:
|
||||
return nil, ErrNotFound{fmt.Errorf("Not found. HTTP status code: %d", resp.StatusCode)}
|
||||
default:
|
||||
return nil, ErrServer{fmt.Errorf("Server error. HTTP status code: %d", resp.StatusCode)}
|
||||
}
|
||||
} else {
|
||||
return nil, ErrNetwork{fmt.Errorf("Unable to fetch data: %s", err.Error())}
|
||||
}
|
||||
}
|
@@ -1,140 +0,0 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestExpBackoff(t *testing.T) {
|
||||
duration := time.Millisecond
|
||||
max := time.Hour
|
||||
for i := 0; i < math.MaxUint16; i++ {
|
||||
duration = ExpBackoff(duration, max)
|
||||
if duration < 0 {
|
||||
t.Fatalf("duration too small: %v %v", duration, i)
|
||||
}
|
||||
if duration > max {
|
||||
t.Fatalf("duration too large: %v %v", duration, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test exponential backoff and that it continues retrying if a 5xx response is
|
||||
// received
|
||||
func TestGetURLExpBackOff(t *testing.T) {
|
||||
var expBackoffTests = []struct {
|
||||
count int
|
||||
body string
|
||||
}{
|
||||
{0, "number of attempts: 0"},
|
||||
{1, "number of attempts: 1"},
|
||||
{2, "number of attempts: 2"},
|
||||
}
|
||||
client := NewHttpClient()
|
||||
|
||||
for i, tt := range expBackoffTests {
|
||||
mux := http.NewServeMux()
|
||||
count := 0
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if count == tt.count {
|
||||
io.WriteString(w, fmt.Sprintf("number of attempts: %d", count))
|
||||
return
|
||||
}
|
||||
count++
|
||||
http.Error(w, "", 500)
|
||||
})
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
data, err := client.GetRetry(ts.URL)
|
||||
if err != nil {
|
||||
t.Errorf("Test case %d produced error: %v", i, err)
|
||||
}
|
||||
|
||||
if count != tt.count {
|
||||
t.Errorf("Test case %d failed: %d != %d", i, count, tt.count)
|
||||
}
|
||||
|
||||
if string(data) != tt.body {
|
||||
t.Errorf("Test case %d failed: %s != %s", i, tt.body, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that it stops retrying if a 4xx response comes back
|
||||
func TestGetURL4xx(t *testing.T) {
|
||||
client := NewHttpClient()
|
||||
retries := 0
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
retries++
|
||||
http.Error(w, "", 404)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
_, err := client.GetRetry(ts.URL)
|
||||
if err == nil {
|
||||
t.Errorf("Incorrect result\ngot: %s\nwant: %s", err.Error(), "Not found. HTTP status code: 404")
|
||||
}
|
||||
|
||||
if retries > 1 {
|
||||
t.Errorf("Number of retries:\n%d\nExpected number of retries:\n%s", retries, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that it fetches and returns user-data just fine
|
||||
func TestGetURL2xx(t *testing.T) {
|
||||
var cloudcfg = `
|
||||
#cloud-config
|
||||
coreos:
|
||||
oem:
|
||||
id: test
|
||||
name: CoreOS.box for Test
|
||||
version-id: %VERSION_ID%+%BUILD_ID%
|
||||
home-url: https://github.com/coreos/coreos-cloudinit
|
||||
bug-report-url: https://github.com/coreos/coreos-cloudinit
|
||||
update:
|
||||
reboot-strategy: best-effort
|
||||
`
|
||||
|
||||
client := NewHttpClient()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, cloudcfg)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
data, err := client.GetRetry(ts.URL)
|
||||
if err != nil {
|
||||
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, nil)
|
||||
}
|
||||
|
||||
if string(data) != cloudcfg {
|
||||
t.Errorf("Incorrect result\ngot: %s\nwant: %s", string(data), cloudcfg)
|
||||
}
|
||||
}
|
||||
|
||||
// Test attempt to fetching using malformed URL
|
||||
func TestGetMalformedURL(t *testing.T) {
|
||||
client := NewHttpClient()
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
want string
|
||||
}{
|
||||
{"boo", "URL boo does not have a valid HTTP scheme. Skipping."},
|
||||
{"mailto://boo", "URL mailto://boo does not have a valid HTTP scheme. Skipping."},
|
||||
{"ftp://boo", "URL ftp://boo does not have a valid HTTP scheme. Skipping."},
|
||||
{"", "URL is empty. Skipping."},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
_, err := client.GetRetry(test.url)
|
||||
if err == nil || err.Error() != test.want {
|
||||
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, test.want)
|
||||
}
|
||||
}
|
||||
}
|
1
src/github.com/coreos/coreos-cloudinit
Symbolic link
1
src/github.com/coreos/coreos-cloudinit
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../
|
@@ -1,100 +0,0 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type EnvFile struct {
|
||||
Vars map[string]string
|
||||
// mask File.Content, it shouldn't be used.
|
||||
Content interface{} `json:"-" yaml:"-"`
|
||||
*File
|
||||
}
|
||||
|
||||
// only allow sh compatible identifiers
|
||||
var validKey = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
|
||||
|
||||
// match each line, optionally capturing valid identifiers, discarding dos line endings
|
||||
var lineLexer = regexp.MustCompile(`(?m)^((?:([a-zA-Z0-9_]+)=)?.*?)\r?\n`)
|
||||
|
||||
// mergeEnvContents: Update the existing file contents with new values,
|
||||
// preserving variable ordering and all content this code doesn't understand.
|
||||
// All new values are appended to the bottom of the old, sorted by key.
|
||||
func mergeEnvContents(old []byte, pending map[string]string) []byte {
|
||||
var buf bytes.Buffer
|
||||
var match [][]byte
|
||||
|
||||
// it is awkward for the regex to handle a missing newline gracefully
|
||||
if len(old) != 0 && !bytes.HasSuffix(old, []byte{'\n'}) {
|
||||
old = append(old, byte('\n'))
|
||||
}
|
||||
|
||||
for _, match = range lineLexer.FindAllSubmatch(old, -1) {
|
||||
key := string(match[2])
|
||||
if value, ok := pending[key]; ok {
|
||||
fmt.Fprintf(&buf, "%s=%s\n", key, value)
|
||||
delete(pending, key)
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "%s\n", match[1])
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range keys(pending) {
|
||||
value := pending[key]
|
||||
fmt.Fprintf(&buf, "%s=%s\n", key, value)
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// WriteEnvFile updates an existing env `KEY=value` formated file with
|
||||
// new values provided in EnvFile.Vars; File.Content is ignored.
|
||||
// Existing ordering and any unknown formatting such as comments are
|
||||
// preserved. If no changes are required the file is untouched.
|
||||
func WriteEnvFile(ef *EnvFile, root string) error {
|
||||
// validate new keys, mergeEnvContents uses pending to track writes
|
||||
pending := make(map[string]string, len(ef.Vars))
|
||||
for key, value := range ef.Vars {
|
||||
if !validKey.MatchString(key) {
|
||||
return fmt.Errorf("Invalid name %q for %s", key, ef.Path)
|
||||
}
|
||||
pending[key] = value
|
||||
}
|
||||
|
||||
if len(pending) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldContent, err := ioutil.ReadFile(path.Join(root, ef.Path))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
oldContent = []byte{}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
newContent := mergeEnvContents(oldContent, pending)
|
||||
if bytes.Equal(oldContent, newContent) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ef.File.Content = string(newContent)
|
||||
_, err = WriteFile(ef.File, root)
|
||||
return err
|
||||
}
|
||||
|
||||
// keys returns the keys of a map in sorted order
|
||||
func keys(m map[string]string) (s []string) {
|
||||
for k, _ := range m {
|
||||
s = append(s, k)
|
||||
}
|
||||
sort.Strings(s)
|
||||
return
|
||||
}
|
@@ -1,426 +0,0 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
base = "# a file\nFOO=base\n\nBAR= hi there\n"
|
||||
baseNoNewline = "# a file\nFOO=base\n\nBAR= hi there"
|
||||
baseDos = "# a file\r\nFOO=base\r\n\r\nBAR= hi there\r\n"
|
||||
expectUpdate = "# a file\nFOO=test\n\nBAR= hi there\nNEW=a value\n"
|
||||
expectCreate = "FOO=test\nNEW=a value\n"
|
||||
)
|
||||
|
||||
var (
|
||||
valueUpdate = map[string]string{
|
||||
"FOO": "test",
|
||||
"NEW": "a value",
|
||||
}
|
||||
valueNoop = map[string]string{
|
||||
"FOO": "base",
|
||||
}
|
||||
valueEmpty = map[string]string{}
|
||||
valueInvalid = map[string]string{
|
||||
"FOO-X": "test",
|
||||
}
|
||||
)
|
||||
|
||||
func TestWriteEnvFileUpdate(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
fullPath := path.Join(dir, name)
|
||||
ioutil.WriteFile(fullPath, []byte(base), 0644)
|
||||
|
||||
oldStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueUpdate,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != expectUpdate {
|
||||
t.Fatalf("File has incorrect contents: %q", contents)
|
||||
}
|
||||
|
||||
newStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
|
||||
t.Fatalf("File was not replaced: %s", fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteEnvFileUpdateNoNewline(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
fullPath := path.Join(dir, name)
|
||||
ioutil.WriteFile(fullPath, []byte(baseNoNewline), 0644)
|
||||
|
||||
oldStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueUpdate,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != expectUpdate {
|
||||
t.Fatalf("File has incorrect contents: %q", contents)
|
||||
}
|
||||
|
||||
newStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
|
||||
t.Fatalf("File was not replaced: %s", fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteEnvFileCreate(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
fullPath := path.Join(dir, name)
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueUpdate,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != expectCreate {
|
||||
t.Fatalf("File has incorrect contents: %q", contents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteEnvFileNoop(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
fullPath := path.Join(dir, name)
|
||||
ioutil.WriteFile(fullPath, []byte(base), 0644)
|
||||
|
||||
oldStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueNoop,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != base {
|
||||
t.Fatalf("File has incorrect contents: %q", contents)
|
||||
}
|
||||
|
||||
newStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino {
|
||||
t.Fatalf("File was replaced: %s", fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteEnvFileUpdateDos(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
fullPath := path.Join(dir, name)
|
||||
ioutil.WriteFile(fullPath, []byte(baseDos), 0644)
|
||||
|
||||
oldStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueUpdate,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != expectUpdate {
|
||||
t.Fatalf("File has incorrect contents: %q", contents)
|
||||
}
|
||||
|
||||
newStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
|
||||
t.Fatalf("File was not replaced: %s", fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
// A middle ground noop, values are unchanged but we did have a value.
|
||||
// Seems reasonable to rewrite the file in Unix format anyway.
|
||||
func TestWriteEnvFileDos2Unix(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
fullPath := path.Join(dir, name)
|
||||
ioutil.WriteFile(fullPath, []byte(baseDos), 0644)
|
||||
|
||||
oldStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueNoop,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != base {
|
||||
t.Fatalf("File has incorrect contents: %q", contents)
|
||||
}
|
||||
|
||||
newStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if oldStat.Sys().(*syscall.Stat_t).Ino == newStat.Sys().(*syscall.Stat_t).Ino {
|
||||
t.Fatalf("File was not replaced: %s", fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
// If it really is a noop (structure is empty) don't even do dos2unix
|
||||
func TestWriteEnvFileEmpty(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
fullPath := path.Join(dir, name)
|
||||
ioutil.WriteFile(fullPath, []byte(baseDos), 0644)
|
||||
|
||||
oldStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueEmpty,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
if string(contents) != baseDos {
|
||||
t.Fatalf("File has incorrect contents: %q", contents)
|
||||
}
|
||||
|
||||
newStat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if oldStat.Sys().(*syscall.Stat_t).Ino != newStat.Sys().(*syscall.Stat_t).Ino {
|
||||
t.Fatalf("File was replaced: %s", fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
// no point in creating empty files
|
||||
func TestWriteEnvFileEmptyNoCreate(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
fullPath := path.Join(dir, name)
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueEmpty,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err == nil {
|
||||
t.Fatalf("File has incorrect contents: %q", contents)
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Fatalf("Unexpected error while reading file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteEnvFilePermFailure(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
fullPath := path.Join(dir, name)
|
||||
ioutil.WriteFile(fullPath, []byte(base), 0000)
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueUpdate,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if !os.IsPermission(err) {
|
||||
t.Fatalf("Not a pemission denied error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteEnvFileNameFailure(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
name := "foo.conf"
|
||||
|
||||
ef := EnvFile{
|
||||
File: &File{
|
||||
Path: name,
|
||||
},
|
||||
Vars: valueInvalid,
|
||||
}
|
||||
|
||||
err = WriteEnvFile(&ef, dir)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), "Invalid name") {
|
||||
t.Fatalf("Not an invalid name error: %v", err)
|
||||
}
|
||||
}
|
@@ -31,55 +31,33 @@ func (f *File) Permissions() (os.FileMode, error) {
|
||||
return os.FileMode(perm), nil
|
||||
}
|
||||
|
||||
func WriteFile(f *File, root string) (string, error) {
|
||||
func WriteFile(f *File) error {
|
||||
if f.Encoding != "" {
|
||||
return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
|
||||
return fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
|
||||
}
|
||||
|
||||
fullpath := path.Join(root, f.Path)
|
||||
dir := path.Dir(fullpath)
|
||||
|
||||
if err := EnsureDirectoryExists(dir); err != nil {
|
||||
return "", err
|
||||
if err := os.MkdirAll(path.Dir(f.Path), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
perm, err := f.Permissions()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
var tmp *os.File
|
||||
// Create a temporary file in the same directory to ensure it's on the same filesystem
|
||||
if tmp, err = ioutil.TempFile(dir, "cloudinit-temp"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(tmp.Name(), []byte(f.Content), perm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := tmp.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Ensure the permissions are as requested (since WriteFile can be affected by sticky bit)
|
||||
if err := os.Chmod(tmp.Name(), perm); err != nil {
|
||||
return "", err
|
||||
if err := ioutil.WriteFile(f.Path, []byte(f.Content), perm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.Owner != "" {
|
||||
// We shell out since we don't have a way to look up unix groups natively
|
||||
cmd := exec.Command("chown", f.Owner, tmp.Name())
|
||||
cmd := exec.Command("chown", f.Owner, f.Path)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(tmp.Name(), fullpath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fullpath, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureDirectoryExists(dir string) error {
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -12,22 +13,18 @@ func TestWriteFileUnencodedContent(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
defer syscall.Rmdir(dir)
|
||||
|
||||
fn := "foo"
|
||||
fullPath := path.Join(dir, fn)
|
||||
fullPath := path.Join(dir, "tmp", "foo")
|
||||
|
||||
wf := File{
|
||||
Path: fn,
|
||||
Content: "bar",
|
||||
Path: fullPath,
|
||||
Content: "bar",
|
||||
RawFilePermissions: "0644",
|
||||
}
|
||||
|
||||
path, err := WriteFile(&wf, dir)
|
||||
if err != nil {
|
||||
if err := WriteFile(&wf); err != nil {
|
||||
t.Fatalf("Processing of WriteFile failed: %v", err)
|
||||
} else if path != fullPath {
|
||||
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
@@ -54,15 +51,15 @@ func TestWriteFileInvalidPermission(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
defer syscall.Rmdir(dir)
|
||||
|
||||
wf := File{
|
||||
Path: path.Join(dir, "tmp", "foo"),
|
||||
Content: "bar",
|
||||
Path: path.Join(dir, "tmp", "foo"),
|
||||
Content: "bar",
|
||||
RawFilePermissions: "pants",
|
||||
}
|
||||
|
||||
if _, err := WriteFile(&wf, dir); err == nil {
|
||||
if err := WriteFile(&wf); err == nil {
|
||||
t.Fatalf("Expected error to be raised when writing file with invalid permission")
|
||||
}
|
||||
}
|
||||
@@ -72,21 +69,17 @@ func TestWriteFilePermissions(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
defer syscall.Rmdir(dir)
|
||||
|
||||
fn := "foo"
|
||||
fullPath := path.Join(dir, fn)
|
||||
fullPath := path.Join(dir, "tmp", "foo")
|
||||
|
||||
wf := File{
|
||||
Path: fn,
|
||||
Path: fullPath,
|
||||
RawFilePermissions: "0755",
|
||||
}
|
||||
|
||||
path, err := WriteFile(&wf, dir)
|
||||
if err != nil {
|
||||
if err := WriteFile(&wf); err != nil {
|
||||
t.Fatalf("Processing of WriteFile failed: %v", err)
|
||||
} else if path != fullPath {
|
||||
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
@@ -104,15 +97,15 @@ func TestWriteFileEncodedContent(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
defer syscall.Rmdir(dir)
|
||||
|
||||
wf := File{
|
||||
Path: path.Join(dir, "tmp", "foo"),
|
||||
Content: "",
|
||||
Path: path.Join(dir, "tmp", "foo"),
|
||||
Content: "",
|
||||
Encoding: "base64",
|
||||
}
|
||||
|
||||
if _, err := WriteFile(&wf, dir); err == nil {
|
||||
if err := WriteFile(&wf); err == nil {
|
||||
t.Fatalf("Expected error to be raised when writing file with encoding")
|
||||
}
|
||||
}
|
||||
|
@@ -1,119 +0,0 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/network"
|
||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
runtimeNetworkPath = "/run/systemd/network"
|
||||
)
|
||||
|
||||
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
||||
defer func() {
|
||||
if e := restartNetworkd(); e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
// TODO(crawford): Get rid of this once networkd fixes the race
|
||||
// https://bugs.freedesktop.org/show_bug.cgi?id=76077
|
||||
time.Sleep(5 * time.Second)
|
||||
if e := restartNetworkd(); e != nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
if err = downNetworkInterfaces(interfaces); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = maybeProbe8012q(interfaces); err != nil {
|
||||
return
|
||||
}
|
||||
return maybeProbeBonding(interfaces)
|
||||
}
|
||||
|
||||
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
|
||||
sysInterfaceMap := make(map[string]*net.Interface)
|
||||
if systemInterfaces, err := net.Interfaces(); err == nil {
|
||||
for _, iface := range systemInterfaces {
|
||||
iface := iface
|
||||
sysInterfaceMap[iface.Name] = &iface
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok {
|
||||
log.Printf("Taking down interface %q\n", systemInterface.Name)
|
||||
if err := netlink.NetworkLinkDown(systemInterface); err != nil {
|
||||
fmt.Printf("Error while downing interface %q (%s). Continuing...\n", systemInterface.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeProbe8012q(interfaces []network.InterfaceGenerator) error {
|
||||
for _, iface := range interfaces {
|
||||
if iface.Type() == "vlan" {
|
||||
log.Printf("Probing LKM %q (%q)\n", "8021q", "8021q")
|
||||
return exec.Command("modprobe", "8021q").Run()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeProbeBonding(interfaces []network.InterfaceGenerator) error {
|
||||
args := []string{"bonding"}
|
||||
for _, iface := range interfaces {
|
||||
if iface.Type() == "bond" {
|
||||
args = append(args, strings.Split(iface.ModprobeParams(), " ")...)
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Printf("Probing LKM %q (%q)\n", "bonding", args)
|
||||
return exec.Command("modprobe", args...).Run()
|
||||
}
|
||||
|
||||
func restartNetworkd() error {
|
||||
log.Printf("Restarting networkd.service\n")
|
||||
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service")
|
||||
return err
|
||||
}
|
||||
|
||||
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
|
||||
for _, iface := range interfaces {
|
||||
filename := fmt.Sprintf("%s.netdev", iface.Filename())
|
||||
if err := writeConfig(filename, iface.Netdev()); err != nil {
|
||||
return err
|
||||
}
|
||||
filename = fmt.Sprintf("%s.link", iface.Filename())
|
||||
if err := writeConfig(filename, iface.Link()); err != nil {
|
||||
return err
|
||||
}
|
||||
filename = fmt.Sprintf("%s.network", iface.Filename())
|
||||
if err := writeConfig(filename, iface.Network()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeConfig(filename string, config string) error {
|
||||
if config == "" {
|
||||
return nil
|
||||
}
|
||||
log.Printf("Writing networkd unit %q\n", filename)
|
||||
_, err := WriteFile(&File{Content: config, Path: filename}, runtimeNetworkPath)
|
||||
return err
|
||||
}
|
@@ -13,21 +13,50 @@ import (
|
||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
|
||||
)
|
||||
|
||||
func NewUnitManager(root string) UnitManager {
|
||||
return &systemd{root}
|
||||
}
|
||||
|
||||
type systemd struct {
|
||||
root string
|
||||
}
|
||||
|
||||
// fakeMachineID is placed on non-usr CoreOS images and should
|
||||
// never be used as a true MachineID
|
||||
const fakeMachineID = "42000000000000000000000000000042"
|
||||
|
||||
type Unit struct {
|
||||
Name string
|
||||
Enable bool
|
||||
Runtime bool
|
||||
Content string
|
||||
Command string
|
||||
}
|
||||
|
||||
func (u *Unit) Type() string {
|
||||
ext := filepath.Ext(u.Name)
|
||||
return strings.TrimLeft(ext, ".")
|
||||
}
|
||||
|
||||
func (u *Unit) Group() (group string) {
|
||||
t := u.Type()
|
||||
if t == "network" || t == "netdev" || t == "link" {
|
||||
group = "network"
|
||||
} else {
|
||||
group = "system"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Script []byte
|
||||
|
||||
// UnitDestination builds the appropriate absolte file path for
|
||||
// the given unit. The root argument indicates the effective base
|
||||
// directory of the system (similar to a chroot).
|
||||
func UnitDestination(u *Unit, root string) string {
|
||||
dir := "etc"
|
||||
if u.Runtime {
|
||||
dir = "run"
|
||||
}
|
||||
|
||||
return path.Join(root, dir, "systemd", u.Group(), u.Name)
|
||||
}
|
||||
|
||||
// PlaceUnit writes a unit file at the provided destination, creating
|
||||
// parent directories as necessary.
|
||||
func (s *systemd) PlaceUnit(u *Unit, dst string) error {
|
||||
func PlaceUnit(u *Unit, dst string) error {
|
||||
dir := filepath.Dir(dst)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
|
||||
@@ -36,12 +65,12 @@ func (s *systemd) PlaceUnit(u *Unit, dst string) error {
|
||||
}
|
||||
|
||||
file := File{
|
||||
Path: filepath.Base(dst),
|
||||
Path: dst,
|
||||
Content: u.Content,
|
||||
RawFilePermissions: "0644",
|
||||
}
|
||||
|
||||
_, err := WriteFile(&file, dir)
|
||||
err := WriteFile(&file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -49,18 +78,18 @@ func (s *systemd) PlaceUnit(u *Unit, dst string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemd) EnableUnitFile(unit string, runtime bool) error {
|
||||
func EnableUnitFile(file string, runtime bool) error {
|
||||
conn, err := dbus.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
units := []string{unit}
|
||||
_, _, err = conn.EnableUnitFiles(units, runtime, true)
|
||||
files := []string{file}
|
||||
_, _, err = conn.EnableUnitFiles(files, runtime, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
|
||||
func RunUnitCommand(command, unit string) (string, error) {
|
||||
conn, err := dbus.New()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -89,7 +118,7 @@ func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
|
||||
return fn(unit, "replace")
|
||||
}
|
||||
|
||||
func (s *systemd) DaemonReload() error {
|
||||
func DaemonReload() error {
|
||||
conn, err := dbus.New()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -98,57 +127,6 @@ func (s *systemd) DaemonReload() error {
|
||||
return conn.Reload()
|
||||
}
|
||||
|
||||
// MaskUnit masks the given Unit by symlinking its unit file to
|
||||
// /dev/null, analogous to `systemctl mask`.
|
||||
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
||||
// file at the location*, to ensure that the mask will succeed.
|
||||
func (s *systemd) MaskUnit(unit *Unit) error {
|
||||
masked := unit.Destination(s.root)
|
||||
if _, err := os.Stat(masked); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := os.Remove(masked); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink("/dev/null", masked)
|
||||
}
|
||||
|
||||
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
|
||||
// associated with the given Unit is empty or appears to be a symlink to
|
||||
// /dev/null, it is removed.
|
||||
func (s *systemd) UnmaskUnit(unit *Unit) error {
|
||||
masked := unit.Destination(s.root)
|
||||
ne, err := nullOrEmpty(masked)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ne {
|
||||
log.Printf("%s is not null or empty, refusing to unmask", masked)
|
||||
return nil
|
||||
}
|
||||
return os.Remove(masked)
|
||||
}
|
||||
|
||||
// nullOrEmpty checks whether a given path appears to be an empty regular file
|
||||
// or a symlink to /dev/null
|
||||
func nullOrEmpty(path string) (bool, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
m := fi.Mode()
|
||||
if m.IsRegular() && fi.Size() <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
if m&os.ModeCharDevice > 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func ExecuteScript(scriptPath string) (string, error) {
|
||||
props := []dbus.Property{
|
||||
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
|
||||
@@ -187,3 +165,11 @@ func MachineID(root string) string {
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func MaskUnit(unit string, root string) error {
|
||||
masked := path.Join(root, "etc", "systemd", "system", unit)
|
||||
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink("/dev/null", masked)
|
||||
}
|
||||
|
@@ -25,15 +25,13 @@ Address=10.209.171.177/19
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := &systemd{dir}
|
||||
|
||||
dst := u.Destination(dir)
|
||||
dst := UnitDestination(&u, dir)
|
||||
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
||||
if dst != expectDst {
|
||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
|
||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||
if err := PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("PlaceUnit failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -62,30 +60,6 @@ Address=10.209.171.177/19
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitDestination(t *testing.T) {
|
||||
dir := "/some/dir"
|
||||
name := "foobar.service"
|
||||
|
||||
u := Unit{
|
||||
Name: name,
|
||||
DropIn: false,
|
||||
}
|
||||
|
||||
dst := u.Destination(dir)
|
||||
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
|
||||
if dst != expectDst {
|
||||
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
|
||||
u.DropIn = true
|
||||
|
||||
dst = u.Destination(dir)
|
||||
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
|
||||
if dst != expectDst {
|
||||
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceMountUnit(t *testing.T) {
|
||||
u := Unit{
|
||||
Name: "media-state.mount",
|
||||
@@ -102,15 +76,13 @@ Where=/media/state
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := &systemd{dir}
|
||||
|
||||
dst := u.Destination(dir)
|
||||
dst := UnitDestination(&u, dir)
|
||||
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
||||
if dst != expectDst {
|
||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
|
||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||
if err := PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("PlaceUnit failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -151,137 +123,22 @@ func TestMachineID(t *testing.T) {
|
||||
t.Fatalf("File has incorrect contents")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskUnit(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := &systemd{dir}
|
||||
|
||||
// Ensure mask works with units that do not currently exist
|
||||
uf := &Unit{Name: "foo.service"}
|
||||
if err := sd.MaskUnit(uf); err != nil {
|
||||
t.Fatalf("Unable to mask new unit: %v", err)
|
||||
if err := MaskUnit("foo.service", dir); err != nil {
|
||||
t.Fatalf("Unable to mask unit: %v", err)
|
||||
}
|
||||
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
|
||||
fooTgt, err := os.Readlink(fooPath)
|
||||
|
||||
fullPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
|
||||
target, err := os.Readlink(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read link", err)
|
||||
}
|
||||
if fooTgt != "/dev/null" {
|
||||
t.Fatalf("unit not masked, got unit target", fooTgt)
|
||||
}
|
||||
|
||||
// Ensure mask works with unit files that already exist
|
||||
ub := &Unit{Name: "bar.service"}
|
||||
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
|
||||
if _, err := os.Create(barPath); err != nil {
|
||||
t.Fatalf("Error creating new unit file: %v", err)
|
||||
}
|
||||
if err := sd.MaskUnit(ub); err != nil {
|
||||
t.Fatalf("Unable to mask existing unit: %v", err)
|
||||
}
|
||||
barTgt, err := os.Readlink(barPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read link", err)
|
||||
}
|
||||
if barTgt != "/dev/null" {
|
||||
t.Fatalf("unit not masked, got unit target", barTgt)
|
||||
if target != "/dev/null" {
|
||||
t.Fatalf("unit not masked, got unit target", target)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmaskUnit(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := &systemd{dir}
|
||||
|
||||
nilUnit := &Unit{Name: "null.service"}
|
||||
if err := sd.UnmaskUnit(nilUnit); err != nil {
|
||||
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
|
||||
}
|
||||
|
||||
uf := &Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}
|
||||
dst := uf.Destination(dir)
|
||||
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
|
||||
t.Fatalf("Unable to create unit directory: %v", err)
|
||||
}
|
||||
if _, err := os.Create(dst); err != nil {
|
||||
t.Fatalf("Unable to write unit file: %v", err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
|
||||
t.Fatalf("Unable to write unit file: %v", err)
|
||||
}
|
||||
if err := sd.UnmaskUnit(uf); err != nil {
|
||||
t.Errorf("unmask of non-empty unit returned unexpected error: %v", err)
|
||||
}
|
||||
got, _ := ioutil.ReadFile(dst)
|
||||
if string(got) != uf.Content {
|
||||
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
|
||||
}
|
||||
|
||||
ub := &Unit{Name: "bar.service"}
|
||||
dst = ub.Destination(dir)
|
||||
if err := os.Symlink("/dev/null", dst); err != nil {
|
||||
t.Fatalf("Unable to create masked unit: %v", err)
|
||||
}
|
||||
if err := sd.UnmaskUnit(ub); err != nil {
|
||||
t.Errorf("unmask of unit returned unexpected error: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(dst); !os.IsNotExist(err) {
|
||||
t.Errorf("expected %s to not exist after unmask, but got err: %s", 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,67 +0,0 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Name for drop-in service configuration files created by cloudconfig
|
||||
const cloudConfigDropIn = "20-cloudinit.conf"
|
||||
|
||||
type UnitManager interface {
|
||||
PlaceUnit(unit *Unit, dst string) error
|
||||
EnableUnitFile(unit string, runtime bool) error
|
||||
RunUnitCommand(command, unit string) (string, error)
|
||||
DaemonReload() error
|
||||
MaskUnit(unit *Unit) error
|
||||
UnmaskUnit(unit *Unit) error
|
||||
}
|
||||
|
||||
type Unit struct {
|
||||
Name string
|
||||
Mask bool
|
||||
Enable bool
|
||||
Runtime bool
|
||||
Content string
|
||||
Command string
|
||||
|
||||
// For drop-in units, a cloudinit.conf is generated.
|
||||
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
|
||||
// until the correct behaviour for multiple drop-in units is determined.
|
||||
DropIn bool `yaml:"-"`
|
||||
}
|
||||
|
||||
func (u *Unit) Type() string {
|
||||
ext := filepath.Ext(u.Name)
|
||||
return strings.TrimLeft(ext, ".")
|
||||
}
|
||||
|
||||
func (u *Unit) Group() (group string) {
|
||||
t := u.Type()
|
||||
if t == "network" || t == "netdev" || t == "link" {
|
||||
group = "network"
|
||||
} else {
|
||||
group = "system"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Script []byte
|
||||
|
||||
// Destination builds the appropriate absolute file path for
|
||||
// the Unit. The root argument indicates the effective base
|
||||
// directory of the system (similar to a chroot).
|
||||
func (u *Unit) Destination(root string) string {
|
||||
dir := "etc"
|
||||
if u.Runtime {
|
||||
dir = "run"
|
||||
}
|
||||
|
||||
if u.DropIn {
|
||||
return path.Join(root, dir, "systemd", u.Group(), fmt.Sprintf("%s.d", u.Name), cloudConfigDropIn)
|
||||
} else {
|
||||
return path.Join(root, dir, "systemd", u.Group(), u.Name)
|
||||
}
|
||||
}
|
13
test
13
test
@@ -13,21 +13,12 @@ COVER=${COVER:-"-cover"}
|
||||
|
||||
source ./build
|
||||
|
||||
declare -a TESTPKGS=(initialize
|
||||
system
|
||||
datasource
|
||||
datasource/configdrive
|
||||
datasource/file
|
||||
datasource/metadata/ec2
|
||||
datasource/proc_cmdline
|
||||
datasource/url
|
||||
pkg
|
||||
network)
|
||||
declare -a TESTPKGS=(initialize system datasource)
|
||||
|
||||
if [ -z "$PKG" ]; then
|
||||
GOFMTPATH="$TESTPKGS coreos-cloudinit.go"
|
||||
# prepend repo path to each package
|
||||
TESTPKGS="${TESTPKGS[@]/#/${REPO_PATH}/} ./"
|
||||
TESTPKGS=${TESTPKGS[@]/#/${REPO_PATH}/}
|
||||
else
|
||||
GOFMTPATH="$TESTPKGS"
|
||||
# strip out slashes and dots from PKG=./foo/
|
||||
|
@@ -1,2 +0,0 @@
|
||||
Michael Crosby <michael@crosbymichael.com> (@crosbymichael)
|
||||
Guillaume J. Charmes <guillaume@docker.com> (@creack)
|
@@ -1,23 +0,0 @@
|
||||
// Packet netlink provide access to low level Netlink sockets and messages.
|
||||
//
|
||||
// Actual implementations are in:
|
||||
// netlink_linux.go
|
||||
// netlink_darwin.go
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWrongSockType = errors.New("Wrong socket type")
|
||||
ErrShortResponse = errors.New("Got short response from netlink")
|
||||
)
|
||||
|
||||
// A Route is a subnet associated with the interface to reach it.
|
||||
type Route struct {
|
||||
*net.IPNet
|
||||
Iface *net.Interface
|
||||
Default bool
|
||||
}
|
@@ -1,891 +0,0 @@
|
||||
// +build amd64
|
||||
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
IFNAMSIZ = 16
|
||||
DEFAULT_CHANGE = 0xFFFFFFFF
|
||||
IFLA_INFO_KIND = 1
|
||||
IFLA_INFO_DATA = 2
|
||||
VETH_INFO_PEER = 1
|
||||
IFLA_NET_NS_FD = 28
|
||||
SIOC_BRADDBR = 0x89a0
|
||||
SIOC_BRADDIF = 0x89a2
|
||||
)
|
||||
|
||||
var nextSeqNr int
|
||||
|
||||
type ifreqHwaddr struct {
|
||||
IfrnName [16]byte
|
||||
IfruHwaddr syscall.RawSockaddr
|
||||
}
|
||||
|
||||
type ifreqIndex struct {
|
||||
IfrnName [16]byte
|
||||
IfruIndex int32
|
||||
}
|
||||
|
||||
func nativeEndian() binary.ByteOrder {
|
||||
var x uint32 = 0x01020304
|
||||
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
|
||||
return binary.BigEndian
|
||||
}
|
||||
return binary.LittleEndian
|
||||
}
|
||||
|
||||
func getSeq() int {
|
||||
nextSeqNr = nextSeqNr + 1
|
||||
return nextSeqNr
|
||||
}
|
||||
|
||||
func getIpFamily(ip net.IP) int {
|
||||
if len(ip) <= net.IPv4len {
|
||||
return syscall.AF_INET
|
||||
}
|
||||
if ip.To4() != nil {
|
||||
return syscall.AF_INET
|
||||
}
|
||||
return syscall.AF_INET6
|
||||
}
|
||||
|
||||
type NetlinkRequestData interface {
|
||||
Len() int
|
||||
ToWireFormat() []byte
|
||||
}
|
||||
|
||||
type IfInfomsg struct {
|
||||
syscall.IfInfomsg
|
||||
}
|
||||
|
||||
func newIfInfomsg(family int) *IfInfomsg {
|
||||
return &IfInfomsg{
|
||||
IfInfomsg: syscall.IfInfomsg{
|
||||
Family: uint8(family),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newIfInfomsgChild(parent *RtAttr, family int) *IfInfomsg {
|
||||
msg := newIfInfomsg(family)
|
||||
parent.children = append(parent.children, msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *IfInfomsg) ToWireFormat() []byte {
|
||||
native := nativeEndian()
|
||||
|
||||
length := syscall.SizeofIfInfomsg
|
||||
b := make([]byte, length)
|
||||
b[0] = msg.Family
|
||||
b[1] = 0
|
||||
native.PutUint16(b[2:4], msg.Type)
|
||||
native.PutUint32(b[4:8], uint32(msg.Index))
|
||||
native.PutUint32(b[8:12], msg.Flags)
|
||||
native.PutUint32(b[12:16], msg.Change)
|
||||
return b
|
||||
}
|
||||
|
||||
func (msg *IfInfomsg) Len() int {
|
||||
return syscall.SizeofIfInfomsg
|
||||
}
|
||||
|
||||
type IfAddrmsg struct {
|
||||
syscall.IfAddrmsg
|
||||
}
|
||||
|
||||
func newIfAddrmsg(family int) *IfAddrmsg {
|
||||
return &IfAddrmsg{
|
||||
IfAddrmsg: syscall.IfAddrmsg{
|
||||
Family: uint8(family),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *IfAddrmsg) ToWireFormat() []byte {
|
||||
native := nativeEndian()
|
||||
|
||||
length := syscall.SizeofIfAddrmsg
|
||||
b := make([]byte, length)
|
||||
b[0] = msg.Family
|
||||
b[1] = msg.Prefixlen
|
||||
b[2] = msg.Flags
|
||||
b[3] = msg.Scope
|
||||
native.PutUint32(b[4:8], msg.Index)
|
||||
return b
|
||||
}
|
||||
|
||||
func (msg *IfAddrmsg) Len() int {
|
||||
return syscall.SizeofIfAddrmsg
|
||||
}
|
||||
|
||||
type RtMsg struct {
|
||||
syscall.RtMsg
|
||||
}
|
||||
|
||||
func newRtMsg(family int) *RtMsg {
|
||||
return &RtMsg{
|
||||
RtMsg: syscall.RtMsg{
|
||||
Family: uint8(family),
|
||||
Table: syscall.RT_TABLE_MAIN,
|
||||
Scope: syscall.RT_SCOPE_UNIVERSE,
|
||||
Protocol: syscall.RTPROT_BOOT,
|
||||
Type: syscall.RTN_UNICAST,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *RtMsg) ToWireFormat() []byte {
|
||||
native := nativeEndian()
|
||||
|
||||
length := syscall.SizeofRtMsg
|
||||
b := make([]byte, length)
|
||||
b[0] = msg.Family
|
||||
b[1] = msg.Dst_len
|
||||
b[2] = msg.Src_len
|
||||
b[3] = msg.Tos
|
||||
b[4] = msg.Table
|
||||
b[5] = msg.Protocol
|
||||
b[6] = msg.Scope
|
||||
b[7] = msg.Type
|
||||
native.PutUint32(b[8:12], msg.Flags)
|
||||
return b
|
||||
}
|
||||
|
||||
func (msg *RtMsg) Len() int {
|
||||
return syscall.SizeofRtMsg
|
||||
}
|
||||
|
||||
func rtaAlignOf(attrlen int) int {
|
||||
return (attrlen + syscall.RTA_ALIGNTO - 1) & ^(syscall.RTA_ALIGNTO - 1)
|
||||
}
|
||||
|
||||
type RtAttr struct {
|
||||
syscall.RtAttr
|
||||
Data []byte
|
||||
children []NetlinkRequestData
|
||||
}
|
||||
|
||||
func newRtAttr(attrType int, data []byte) *RtAttr {
|
||||
return &RtAttr{
|
||||
RtAttr: syscall.RtAttr{
|
||||
Type: uint16(attrType),
|
||||
},
|
||||
children: []NetlinkRequestData{},
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func newRtAttrChild(parent *RtAttr, attrType int, data []byte) *RtAttr {
|
||||
attr := newRtAttr(attrType, data)
|
||||
parent.children = append(parent.children, attr)
|
||||
return attr
|
||||
}
|
||||
|
||||
func (a *RtAttr) Len() int {
|
||||
l := 0
|
||||
for _, child := range a.children {
|
||||
l += child.Len() + syscall.SizeofRtAttr
|
||||
}
|
||||
if l == 0 {
|
||||
l++
|
||||
}
|
||||
return rtaAlignOf(l + len(a.Data))
|
||||
}
|
||||
|
||||
func (a *RtAttr) ToWireFormat() []byte {
|
||||
native := nativeEndian()
|
||||
|
||||
length := a.Len()
|
||||
buf := make([]byte, rtaAlignOf(length+syscall.SizeofRtAttr))
|
||||
|
||||
if a.Data != nil {
|
||||
copy(buf[4:], a.Data)
|
||||
} else {
|
||||
next := 4
|
||||
for _, child := range a.children {
|
||||
childBuf := child.ToWireFormat()
|
||||
copy(buf[next:], childBuf)
|
||||
next += rtaAlignOf(len(childBuf))
|
||||
}
|
||||
}
|
||||
|
||||
if l := uint16(rtaAlignOf(length)); l != 0 {
|
||||
native.PutUint16(buf[0:2], l+1)
|
||||
}
|
||||
native.PutUint16(buf[2:4], a.Type)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
type NetlinkRequest struct {
|
||||
syscall.NlMsghdr
|
||||
Data []NetlinkRequestData
|
||||
}
|
||||
|
||||
func (rr *NetlinkRequest) ToWireFormat() []byte {
|
||||
native := nativeEndian()
|
||||
|
||||
length := rr.Len
|
||||
dataBytes := make([][]byte, len(rr.Data))
|
||||
for i, data := range rr.Data {
|
||||
dataBytes[i] = data.ToWireFormat()
|
||||
length += uint32(len(dataBytes[i]))
|
||||
}
|
||||
b := make([]byte, length)
|
||||
native.PutUint32(b[0:4], length)
|
||||
native.PutUint16(b[4:6], rr.Type)
|
||||
native.PutUint16(b[6:8], rr.Flags)
|
||||
native.PutUint32(b[8:12], rr.Seq)
|
||||
native.PutUint32(b[12:16], rr.Pid)
|
||||
|
||||
next := 16
|
||||
for _, data := range dataBytes {
|
||||
copy(b[next:], data)
|
||||
next += len(data)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (rr *NetlinkRequest) AddData(data NetlinkRequestData) {
|
||||
if data != nil {
|
||||
rr.Data = append(rr.Data, data)
|
||||
}
|
||||
}
|
||||
|
||||
func newNetlinkRequest(proto, flags int) *NetlinkRequest {
|
||||
return &NetlinkRequest{
|
||||
NlMsghdr: syscall.NlMsghdr{
|
||||
Len: uint32(syscall.NLMSG_HDRLEN),
|
||||
Type: uint16(proto),
|
||||
Flags: syscall.NLM_F_REQUEST | uint16(flags),
|
||||
Seq: uint32(getSeq()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type NetlinkSocket struct {
|
||||
fd int
|
||||
lsa syscall.SockaddrNetlink
|
||||
}
|
||||
|
||||
func getNetlinkSocket() (*NetlinkSocket, error) {
|
||||
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_ROUTE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &NetlinkSocket{
|
||||
fd: fd,
|
||||
}
|
||||
s.lsa.Family = syscall.AF_NETLINK
|
||||
if err := syscall.Bind(fd, &s.lsa); err != nil {
|
||||
syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *NetlinkSocket) Close() {
|
||||
syscall.Close(s.fd)
|
||||
}
|
||||
|
||||
func (s *NetlinkSocket) Send(request *NetlinkRequest) error {
|
||||
if err := syscall.Sendto(s.fd, request.ToWireFormat(), 0, &s.lsa); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NetlinkSocket) Receive() ([]syscall.NetlinkMessage, error) {
|
||||
rb := make([]byte, syscall.Getpagesize())
|
||||
nr, _, err := syscall.Recvfrom(s.fd, rb, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nr < syscall.NLMSG_HDRLEN {
|
||||
return nil, ErrShortResponse
|
||||
}
|
||||
rb = rb[:nr]
|
||||
return syscall.ParseNetlinkMessage(rb)
|
||||
}
|
||||
|
||||
func (s *NetlinkSocket) GetPid() (uint32, error) {
|
||||
lsa, err := syscall.Getsockname(s.fd)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch v := lsa.(type) {
|
||||
case *syscall.SockaddrNetlink:
|
||||
return v.Pid, nil
|
||||
}
|
||||
return 0, ErrWrongSockType
|
||||
}
|
||||
|
||||
func (s *NetlinkSocket) HandleAck(seq uint32) error {
|
||||
native := nativeEndian()
|
||||
|
||||
pid, err := s.GetPid()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
done:
|
||||
for {
|
||||
msgs, err := s.Receive()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range msgs {
|
||||
if m.Header.Seq != seq {
|
||||
return fmt.Errorf("Wrong Seq nr %d, expected %d", m.Header.Seq, seq)
|
||||
}
|
||||
if m.Header.Pid != pid {
|
||||
return fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid)
|
||||
}
|
||||
if m.Header.Type == syscall.NLMSG_DONE {
|
||||
break done
|
||||
}
|
||||
if m.Header.Type == syscall.NLMSG_ERROR {
|
||||
error := int32(native.Uint32(m.Data[0:4]))
|
||||
if error == 0 {
|
||||
break done
|
||||
}
|
||||
return syscall.Errno(-error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a new default gateway. Identical to:
|
||||
// ip route add default via $ip
|
||||
func AddDefaultGw(ip net.IP) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
family := getIpFamily(ip)
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_NEWROUTE, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
|
||||
|
||||
msg := newRtMsg(family)
|
||||
wb.AddData(msg)
|
||||
|
||||
var ipData []byte
|
||||
if family == syscall.AF_INET {
|
||||
ipData = ip.To4()
|
||||
} else {
|
||||
ipData = ip.To16()
|
||||
}
|
||||
|
||||
gateway := newRtAttr(syscall.RTA_GATEWAY, ipData)
|
||||
|
||||
wb.AddData(gateway)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
// Bring up a particular network interface
|
||||
func NetworkLinkUp(iface *net.Interface) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_ACK)
|
||||
|
||||
msg := newIfInfomsg(syscall.AF_UNSPEC)
|
||||
msg.Change = syscall.IFF_UP
|
||||
msg.Flags = syscall.IFF_UP
|
||||
msg.Index = int32(iface.Index)
|
||||
wb.AddData(msg)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
func NetworkLinkDown(iface *net.Interface) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_ACK)
|
||||
|
||||
msg := newIfInfomsg(syscall.AF_UNSPEC)
|
||||
msg.Change = syscall.IFF_UP
|
||||
msg.Flags = 0 & ^syscall.IFF_UP
|
||||
msg.Index = int32(iface.Index)
|
||||
wb.AddData(msg)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
func NetworkSetMTU(iface *net.Interface, mtu int) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
|
||||
|
||||
msg := newIfInfomsg(syscall.AF_UNSPEC)
|
||||
msg.Type = syscall.RTM_SETLINK
|
||||
msg.Flags = syscall.NLM_F_REQUEST
|
||||
msg.Index = int32(iface.Index)
|
||||
msg.Change = DEFAULT_CHANGE
|
||||
wb.AddData(msg)
|
||||
|
||||
var (
|
||||
b = make([]byte, 4)
|
||||
native = nativeEndian()
|
||||
)
|
||||
native.PutUint32(b, uint32(mtu))
|
||||
|
||||
data := newRtAttr(syscall.IFLA_MTU, b)
|
||||
wb.AddData(data)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
// same as ip link set $name master $master
|
||||
func NetworkSetMaster(iface, master *net.Interface) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
|
||||
|
||||
msg := newIfInfomsg(syscall.AF_UNSPEC)
|
||||
msg.Type = syscall.RTM_SETLINK
|
||||
msg.Flags = syscall.NLM_F_REQUEST
|
||||
msg.Index = int32(iface.Index)
|
||||
msg.Change = DEFAULT_CHANGE
|
||||
wb.AddData(msg)
|
||||
|
||||
var (
|
||||
b = make([]byte, 4)
|
||||
native = nativeEndian()
|
||||
)
|
||||
native.PutUint32(b, uint32(master.Index))
|
||||
|
||||
data := newRtAttr(syscall.IFLA_MASTER, b)
|
||||
wb.AddData(data)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
func NetworkSetNsPid(iface *net.Interface, nspid int) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
|
||||
|
||||
msg := newIfInfomsg(syscall.AF_UNSPEC)
|
||||
msg.Type = syscall.RTM_SETLINK
|
||||
msg.Flags = syscall.NLM_F_REQUEST
|
||||
msg.Index = int32(iface.Index)
|
||||
msg.Change = DEFAULT_CHANGE
|
||||
wb.AddData(msg)
|
||||
|
||||
var (
|
||||
b = make([]byte, 4)
|
||||
native = nativeEndian()
|
||||
)
|
||||
native.PutUint32(b, uint32(nspid))
|
||||
|
||||
data := newRtAttr(syscall.IFLA_NET_NS_PID, b)
|
||||
wb.AddData(data)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
func NetworkSetNsFd(iface *net.Interface, fd int) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
|
||||
|
||||
msg := newIfInfomsg(syscall.AF_UNSPEC)
|
||||
msg.Type = syscall.RTM_SETLINK
|
||||
msg.Flags = syscall.NLM_F_REQUEST
|
||||
msg.Index = int32(iface.Index)
|
||||
msg.Change = DEFAULT_CHANGE
|
||||
wb.AddData(msg)
|
||||
|
||||
var (
|
||||
b = make([]byte, 4)
|
||||
native = nativeEndian()
|
||||
)
|
||||
native.PutUint32(b, uint32(fd))
|
||||
|
||||
data := newRtAttr(IFLA_NET_NS_FD, b)
|
||||
wb.AddData(data)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
// Add an Ip address to an interface. This is identical to:
|
||||
// ip addr add $ip/$ipNet dev $iface
|
||||
func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
family := getIpFamily(ip)
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_NEWADDR, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
|
||||
|
||||
msg := newIfAddrmsg(family)
|
||||
msg.Index = uint32(iface.Index)
|
||||
prefixLen, _ := ipNet.Mask.Size()
|
||||
msg.Prefixlen = uint8(prefixLen)
|
||||
wb.AddData(msg)
|
||||
|
||||
var ipData []byte
|
||||
if family == syscall.AF_INET {
|
||||
ipData = ip.To4()
|
||||
} else {
|
||||
ipData = ip.To16()
|
||||
}
|
||||
|
||||
localData := newRtAttr(syscall.IFA_LOCAL, ipData)
|
||||
wb.AddData(localData)
|
||||
|
||||
addrData := newRtAttr(syscall.IFA_ADDRESS, ipData)
|
||||
wb.AddData(addrData)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
func zeroTerminated(s string) []byte {
|
||||
return []byte(s + "\000")
|
||||
}
|
||||
|
||||
func nonZeroTerminated(s string) []byte {
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
// Add a new network link of a specified type. This is identical to
|
||||
// running: ip add link $name type $linkType
|
||||
func NetworkLinkAdd(name string, linkType string) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
|
||||
|
||||
msg := newIfInfomsg(syscall.AF_UNSPEC)
|
||||
wb.AddData(msg)
|
||||
|
||||
if name != "" {
|
||||
nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name))
|
||||
wb.AddData(nameData)
|
||||
}
|
||||
|
||||
kindData := newRtAttr(IFLA_INFO_KIND, nonZeroTerminated(linkType))
|
||||
|
||||
infoData := newRtAttr(syscall.IFLA_LINKINFO, kindData.ToWireFormat())
|
||||
wb.AddData(infoData)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
// Returns an array of IPNet for all the currently routed subnets on ipv4
|
||||
// This is similar to the first column of "ip route" output
|
||||
func NetworkGetRoutes() ([]Route, error) {
|
||||
native := nativeEndian()
|
||||
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_GETROUTE, syscall.NLM_F_DUMP)
|
||||
|
||||
msg := newIfInfomsg(syscall.AF_UNSPEC)
|
||||
wb.AddData(msg)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pid, err := s.GetPid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]Route, 0)
|
||||
|
||||
done:
|
||||
for {
|
||||
msgs, err := s.Receive()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, m := range msgs {
|
||||
if m.Header.Seq != wb.Seq {
|
||||
return nil, fmt.Errorf("Wrong Seq nr %d, expected 1", m.Header.Seq)
|
||||
}
|
||||
if m.Header.Pid != pid {
|
||||
return nil, fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid)
|
||||
}
|
||||
if m.Header.Type == syscall.NLMSG_DONE {
|
||||
break done
|
||||
}
|
||||
if m.Header.Type == syscall.NLMSG_ERROR {
|
||||
error := int32(native.Uint32(m.Data[0:4]))
|
||||
if error == 0 {
|
||||
break done
|
||||
}
|
||||
return nil, syscall.Errno(-error)
|
||||
}
|
||||
if m.Header.Type != syscall.RTM_NEWROUTE {
|
||||
continue
|
||||
}
|
||||
|
||||
var r Route
|
||||
|
||||
msg := (*RtMsg)(unsafe.Pointer(&m.Data[0:syscall.SizeofRtMsg][0]))
|
||||
|
||||
if msg.Flags&syscall.RTM_F_CLONED != 0 {
|
||||
// Ignore cloned routes
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.Table != syscall.RT_TABLE_MAIN {
|
||||
// Ignore non-main tables
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.Family != syscall.AF_INET {
|
||||
// Ignore non-ipv4 routes
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.Dst_len == 0 {
|
||||
// Default routes
|
||||
r.Default = true
|
||||
}
|
||||
|
||||
attrs, err := syscall.ParseNetlinkRouteAttr(&m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, attr := range attrs {
|
||||
switch attr.Attr.Type {
|
||||
case syscall.RTA_DST:
|
||||
ip := attr.Value
|
||||
r.IPNet = &net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.CIDRMask(int(msg.Dst_len), 8*len(ip)),
|
||||
}
|
||||
case syscall.RTA_OIF:
|
||||
index := int(native.Uint32(attr.Value[0:4]))
|
||||
r.Iface, _ = net.InterfaceByIndex(index)
|
||||
}
|
||||
}
|
||||
if r.Default || r.IPNet != nil {
|
||||
res = append(res, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getIfSocket() (fd int, err error) {
|
||||
for _, socket := range []int{
|
||||
syscall.AF_INET,
|
||||
syscall.AF_PACKET,
|
||||
syscall.AF_INET6,
|
||||
} {
|
||||
if fd, err = syscall.Socket(socket, syscall.SOCK_DGRAM, 0); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
return fd, nil
|
||||
}
|
||||
return -1, err
|
||||
}
|
||||
|
||||
func NetworkChangeName(iface *net.Interface, newName string) error {
|
||||
fd, err := getIfSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
data := [IFNAMSIZ * 2]byte{}
|
||||
// the "-1"s here are very important for ensuring we get proper null
|
||||
// termination of our new C strings
|
||||
copy(data[:IFNAMSIZ-1], iface.Name)
|
||||
copy(data[IFNAMSIZ:IFNAMSIZ*2-1], newName)
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.SIOCSIFNAME, uintptr(unsafe.Pointer(&data[0]))); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NetworkCreateVethPair(name1, name2 string) error {
|
||||
s, err := getNetlinkSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
|
||||
|
||||
msg := newIfInfomsg(syscall.AF_UNSPEC)
|
||||
wb.AddData(msg)
|
||||
|
||||
nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name1))
|
||||
wb.AddData(nameData)
|
||||
|
||||
nest1 := newRtAttr(syscall.IFLA_LINKINFO, nil)
|
||||
newRtAttrChild(nest1, IFLA_INFO_KIND, zeroTerminated("veth"))
|
||||
nest2 := newRtAttrChild(nest1, IFLA_INFO_DATA, nil)
|
||||
nest3 := newRtAttrChild(nest2, VETH_INFO_PEER, nil)
|
||||
|
||||
newIfInfomsgChild(nest3, syscall.AF_UNSPEC)
|
||||
newRtAttrChild(nest3, syscall.IFLA_IFNAME, zeroTerminated(name2))
|
||||
|
||||
wb.AddData(nest1)
|
||||
|
||||
if err := s.Send(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.HandleAck(wb.Seq)
|
||||
}
|
||||
|
||||
// Create the actual bridge device. This is more backward-compatible than
|
||||
// netlink.NetworkLinkAdd and works on RHEL 6.
|
||||
func CreateBridge(name string, setMacAddr bool) error {
|
||||
s, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
|
||||
if err != nil {
|
||||
// ipv6 issue, creating with ipv4
|
||||
s, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer syscall.Close(s)
|
||||
|
||||
nameBytePtr, err := syscall.BytePtrFromString(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), SIOC_BRADDBR, uintptr(unsafe.Pointer(nameBytePtr))); err != 0 {
|
||||
return err
|
||||
}
|
||||
if setMacAddr {
|
||||
return setBridgeMacAddress(s, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a slave to abridge device. This is more backward-compatible than
|
||||
// netlink.NetworkSetMaster and works on RHEL 6.
|
||||
func AddToBridge(iface, master *net.Interface) error {
|
||||
s, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
|
||||
if err != nil {
|
||||
// ipv6 issue, creating with ipv4
|
||||
s, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer syscall.Close(s)
|
||||
|
||||
ifr := ifreqIndex{}
|
||||
copy(ifr.IfrnName[:], master.Name)
|
||||
ifr.IfruIndex = int32(iface.Index)
|
||||
|
||||
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), SIOC_BRADDIF, uintptr(unsafe.Pointer(&ifr))); err != 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setBridgeMacAddress(s int, name string) error {
|
||||
ifr := ifreqHwaddr{}
|
||||
ifr.IfruHwaddr.Family = syscall.ARPHRD_ETHER
|
||||
copy(ifr.IfrnName[:], name)
|
||||
|
||||
for i := 0; i < 6; i++ {
|
||||
ifr.IfruHwaddr.Data[i] = int8(rand.Intn(255))
|
||||
}
|
||||
|
||||
ifr.IfruHwaddr.Data[0] &^= 0x1 // clear multicast bit
|
||||
ifr.IfruHwaddr.Data[0] |= 0x2 // set local assignment bit (IEEE802)
|
||||
|
||||
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), syscall.SIOCSIFHWADDR, uintptr(unsafe.Pointer(&ifr))); err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
// +build !linux !amd64
|
||||
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
)
|
||||
|
||||
func NetworkGetRoutes() ([]Route, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func NetworkLinkAdd(name string, linkType string) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func NetworkLinkUp(iface *net.Interface) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func AddDefaultGw(ip net.IP) error {
|
||||
return ErrNotImplemented
|
||||
|
||||
}
|
||||
|
||||
func NetworkSetMTU(iface *net.Interface, mtu int) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func NetworkCreateVethPair(name1, name2 string) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func NetworkChangeName(iface *net.Interface, newName string) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func NetworkSetNsFd(iface *net.Interface, fd int) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func NetworkSetNsPid(iface *net.Interface, nspid int) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func NetworkSetMaster(iface, master *net.Interface) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func NetworkLinkDown(iface *net.Interface) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func CreateBridge(name string, setMacAddr bool) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func AddToBridge(iface, master *net.Interface) error {
|
||||
return ErrNotImplemented
|
||||
}
|
@@ -3,9 +3,9 @@
|
||||
ACTION!="add|change", GOTO="coreos_configdrive_end"
|
||||
|
||||
# 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}+="media-configdrive.mount"
|
||||
SUBSYSTEM=="block", ENV{ID_FS_TYPE}=="iso9660|vfat", ENV{ID_FS_LABEL}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="configdrive-block.service"
|
||||
|
||||
# Addtionally support virtfs from QEMU
|
||||
SUBSYSTEM=="virtio", DRIVER=="9pnet_virtio", ATTR{mount_tag}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="media-configvirtfs.mount"
|
||||
SUBSYSTEM=="virtio", DRIVER=="9pnet_virtio", ATTR{mount_tag}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="configdrive-virtfs.service"
|
||||
|
||||
LABEL="coreos_configdrive_end"
|
||||
|
15
units/configdrive-block.service
Normal file
15
units/configdrive-block.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[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
|
||||
|
||||
# OpenStack defined config drive so they get to stick their name in it
|
||||
Wants=user-cloudinit@media-configdrive-openstack-latest-user_data.service
|
||||
Before=user-cloudinit@media-configdrive-openstack-latest-user_data.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=no
|
||||
ExecStart=/bin/mount -t auto -o ro,x-mount.mkdir LABEL=config-2 /media/configdrive
|
18
units/configdrive-virtfs.service
Normal file
18
units/configdrive-virtfs.service
Normal file
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=Mount config drive from virtfs
|
||||
Conflicts=configdrive-block.service umount.target
|
||||
ConditionPathIsMountPoint=!/media/configdrive
|
||||
ConditionVirtualization=vm
|
||||
|
||||
# OpenStack defined config drive so they get to stick their name in it
|
||||
Wants=user-cloudinit@media-configdrive-openstack-latest-user_data.service
|
||||
Before=user-cloudinit@media-configdrive-openstack-latest-user_data.service
|
||||
|
||||
# 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
|
@@ -1,13 +0,0 @@
|
||||
[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
|
@@ -1,18 +0,0 @@
|
||||
[Unit]
|
||||
Wants=user-configvirtfs.service
|
||||
Before=user-configvirtfs.service
|
||||
# Only mount config drive block devices automatically in virtual machines
|
||||
# or any host that has it explicitly enabled and not explicitly disabled.
|
||||
ConditionVirtualization=|vm
|
||||
ConditionKernelCommandLine=|coreos.configdrive=1
|
||||
ConditionKernelCommandLine=!coreos.configdrive=0
|
||||
|
||||
# Support old style setup for now
|
||||
Wants=addon-run@media-configvirtfs.service addon-config@media-configvirtfs.service
|
||||
Before=addon-run@media-configvirtfs.service addon-config@media-configvirtfs.service
|
||||
|
||||
[Mount]
|
||||
What=config-2
|
||||
Where=/media/configvirtfs
|
||||
Options=ro,trans=virtio,version=9p2000.L
|
||||
Type=9p
|
@@ -1,5 +0,0 @@
|
||||
[Unit]
|
||||
Description=Watch for a cloud-config at %f
|
||||
|
||||
[Path]
|
||||
PathExists=%f
|
@@ -1,8 +1,9 @@
|
||||
[Unit]
|
||||
Description=Load cloud-config from /media/configdrive
|
||||
Description=Load cloud-config from %f
|
||||
Requires=coreos-setup-environment.service
|
||||
After=coreos-setup-environment.service system-config.target
|
||||
After=coreos-setup-environment.service
|
||||
Before=user-config.target
|
||||
ConditionFileNotEmpty=%f
|
||||
|
||||
# HACK: work around ordering between config drive and ec2 metadata It is
|
||||
# possible for OpenStack style systems to provide both the metadata service
|
||||
@@ -13,10 +14,11 @@ Before=user-config.target
|
||||
# systemd knows about the ordering as early as possible.
|
||||
# coreos-cloudinit could implement a simple lock but that cannot be used
|
||||
# until after the systemd dbus calls are made non-blocking.
|
||||
After=system-cloudinit@usr-share-oem-cloud\x2dconfig.yml.service
|
||||
After=ec2-cloudinit.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
EnvironmentFile=-/etc/environment
|
||||
ExecStart=/usr/bin/coreos-cloudinit --from-configdrive=/media/configdrive
|
||||
ExecStart=/usr/bin/coreos-cloudinit --from-file=%f
|
@@ -3,11 +3,9 @@ Description=Load user-provided cloud configs
|
||||
Requires=system-config.target
|
||||
After=system-config.target
|
||||
|
||||
# Watch for configs at a couple common paths
|
||||
Requires=user-configdrive.path
|
||||
After=user-configdrive.path
|
||||
Requires=user-cloudinit@var-lib-coreos\x2dinstall-user_data.path
|
||||
After=user-cloudinit@var-lib-coreos\x2dinstall-user_data.path
|
||||
# Load user_data placed by coreos-install
|
||||
Requires=user-cloudinit@var-lib-coreos\x2dinstall-user_data.service
|
||||
After=user-cloudinit@var-lib-coreos\x2dinstall-user_data.service
|
||||
|
||||
Requires=user-cloudinit-proc-cmdline.service
|
||||
After=user-cloudinit-proc-cmdline.service
|
||||
|
@@ -1,10 +0,0 @@
|
||||
[Unit]
|
||||
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]
|
||||
DirectoryNotEmpty=/media/configdrive
|
@@ -1,11 +0,0 @@
|
||||
[Unit]
|
||||
Description=Load cloud-config from /media/configvirtfs
|
||||
Requires=coreos-setup-environment.service
|
||||
After=coreos-setup-environment.service
|
||||
Before=user-config.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
EnvironmentFile=-/etc/environment
|
||||
ExecStart=/usr/bin/coreos-cloudinit --from-configdrive=/media/configvirtfs
|
Reference in New Issue
Block a user