Compare commits
46 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e64b61b312 | ||
|
d72e10125a | ||
|
3de3d2c050 | ||
|
2ff0762b0c | ||
|
d6bacb24bc | ||
|
926eb4dbb7 | ||
|
e7599fea58 | ||
|
e98c58c656 | ||
|
ae350a3b34 | ||
|
c3b53f24cf | ||
|
8bee85e63d | ||
|
4c02e99bc8 | ||
|
0fb5291dd0 | ||
|
7f55876378 | ||
|
eb51a89f78 | ||
|
588ff4c26c | ||
|
5472de8821 | ||
|
e6b632f817 | ||
|
13a3d892ca | ||
|
2e237ebead | ||
|
61bb63b6e6 | ||
|
476761cf62 | ||
|
5981e12ac0 | ||
|
78d8be8427 | ||
|
10d73930d9 | ||
|
ea12c0bfd1 | ||
|
6540d12d25 | ||
|
c438a42587 | ||
|
19f8fe49af | ||
|
58b091061e | ||
|
8a7df360ac | ||
|
ba7cf90315 | ||
|
8841740a2b | ||
|
dfe1255ac3 | ||
|
0fddd1735d | ||
|
f779a3f7f5 | ||
|
7015338aef | ||
|
34aa147ebe | ||
|
4d02e1da8e | ||
|
5ef3e1f32b | ||
|
23d02363ee | ||
|
3c4fe9e260 | ||
|
a594e053f5 | ||
|
f3ba47ac89 | ||
|
7d814396b7 | ||
|
47ca113385 |
@@ -1,16 +1,45 @@
|
||||
# Customize with Cloud-Config
|
||||
# Using Cloud-Config
|
||||
|
||||
CoreOS allows you to configure networking, create users, launch systemd units on startup and more. We've designed our implementation to allow the same cloud-config file to work across all of our supported platforms.
|
||||
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.
|
||||
|
||||
Only a subset of [cloud-config functionality][cloud-config] is implemented. A set of custom parameters were added to the cloud-config format that are specific to CoreOS. An example file containing all available options can be found at the bottom of this page.
|
||||
## Configuration File
|
||||
|
||||
The file used by this system initialization program is called a "cloud-config" file. It is inspired by the [cloud-init][cloud-init] project's [cloud-config][cloud-config] file. which is "the defacto multi-distribution package that handles early initialization of a cloud instance" ([cloud-init docs][cloud-init-docs]). Because the cloud-init project includes tools which aren't used by CoreOS, only the relevant subset of its configuration items will be implemented in our cloud-config file. In addition to those, we added a few CoreOS-specific items, such as etcd configuration, OEM definition, and systemd units.
|
||||
|
||||
We've designed our implementation to allow the same cloud-config file to work across all of our supported platforms.
|
||||
|
||||
[cloud-init]: https://launchpad.net/cloud-init
|
||||
[cloud-init-docs]: http://cloudinit.readthedocs.org/en/latest/index.html
|
||||
[cloud-config]: http://cloudinit.readthedocs.org/en/latest/topics/format.html#cloud-config-data
|
||||
|
||||
## CoreOS Parameters
|
||||
### File Format
|
||||
|
||||
### coreos.etcd
|
||||
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
|
||||
|
||||
The `coreos.etcd.*` options are translated to a partial systemd unit acting as an etcd configuration file.
|
||||
A cloud-config file should contain an associative array which has zero or more of the following keys:
|
||||
|
||||
- `coreos`
|
||||
- `ssh_authorized_keys`
|
||||
- `hostname`
|
||||
- `users`
|
||||
- `write_files`
|
||||
- `manage_etc_hosts`
|
||||
|
||||
The expected values for these keys are defined in the rest of this document.
|
||||
|
||||
[yaml]: https://en.wikipedia.org/wiki/YAML
|
||||
|
||||
### Providing Cloud-Config with Config-Drive
|
||||
|
||||
CoreOS tries to conform to each platform's native method to provide user data. Each cloud provider tends to be unique, but this complexity has been abstracted by CoreOS. You can view each platform's instructions on their documentation pages. The most universal way to provide cloud-config is [via config-drive](https://github.com/coreos/coreos-cloudinit/blob/master/Documentation/config-drive.md), which attaches a read-only device to the machine, that contains your cloud-config file.
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
### coreos
|
||||
|
||||
#### etcd
|
||||
|
||||
The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
|
||||
We can use the templating feature of coreos-cloudinit to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
|
||||
|
||||
```
|
||||
@@ -19,7 +48,9 @@ We can use the templating feature of coreos-cloudinit to automate etcd configura
|
||||
coreos:
|
||||
etcd:
|
||||
name: node001
|
||||
discovery: https://discovery.etcd.io/3445fa65423d8b04df07f59fb40218f8
|
||||
# generate a new token for each unique cluster from https://discovery.etcd.io/new
|
||||
discovery: https://discovery.etcd.io/<token>
|
||||
# multi-region and multi-cloud deployments need to use $public_ipv4
|
||||
addr: $public_ipv4:4001
|
||||
peer-addr: $private_ipv4:7001
|
||||
```
|
||||
@@ -29,20 +60,19 @@ coreos:
|
||||
```
|
||||
[Service]
|
||||
Environment="ETCD_NAME=node001"
|
||||
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/3445fa65423d8b04df07f59fb40218f8"
|
||||
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
|
||||
Environment="ETCD_ADDR=203.0.113.29:4001"
|
||||
Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
|
||||
```
|
||||
|
||||
For more information about the available configuration options, see the [etcd documentation][etcd-config].
|
||||
For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
|
||||
Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
|
||||
|
||||
[etcd-config]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md
|
||||
|
||||
### coreos.oem
|
||||
#### oem
|
||||
|
||||
These fields are borrowed from the [os-release spec][os-release] and repurposed
|
||||
as a way for coreos-cloudinit to know about the OEM partition on this machine:
|
||||
The `coreos.oem.*` parameters follow the [os-release spec][os-release], but have been repurposed as a way for coreos-cloudinit to know about the OEM partition on this machine:
|
||||
|
||||
- **id**: Lowercase string identifying the OEM
|
||||
- **name**: Human-friendly string representing the OEM
|
||||
@@ -69,22 +99,22 @@ coreos:
|
||||
...would be rendered to the following `/etc/oem-release`:
|
||||
|
||||
```
|
||||
ID="rackspace"
|
||||
ID=rackspace
|
||||
NAME="Rackspace Cloud Servers"
|
||||
VERSION_ID="168.0.0"
|
||||
VERSION_ID=168.0.0
|
||||
HOME_URL="https://www.rackspace.com/cloud/servers/"
|
||||
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
|
||||
```
|
||||
|
||||
[os-release]: http://www.freedesktop.org/software/systemd/man/os-release.html
|
||||
|
||||
### coreos.units
|
||||
#### units
|
||||
|
||||
Arbitrary systemd units may be provided in the `coreos.units` attribute.
|
||||
`coreos.units` is a list of objects 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 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.
|
||||
|
||||
@@ -128,14 +158,12 @@ coreos:
|
||||
command: start
|
||||
```
|
||||
|
||||
## Cloud-Config Parameters
|
||||
|
||||
### ssh_authorized_keys
|
||||
|
||||
Provided public SSH keys will be authorized for the `core` user.
|
||||
The `ssh_authorized_keys` parameter adds public SSH keys which will be authorized for the `core` user.
|
||||
|
||||
The keys will be named "coreos-cloudinit" by default.
|
||||
Override this with the `--ssh-key-name` flag when calling `coreos-cloudinit`.
|
||||
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
|
||||
|
||||
```
|
||||
#cloud-config
|
||||
@@ -146,7 +174,7 @@ ssh_authorized_keys:
|
||||
|
||||
### hostname
|
||||
|
||||
The provided value will be used to set the system's hostname.
|
||||
The `hostname` parameter defines the system's hostname.
|
||||
This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
|
||||
|
||||
```
|
||||
@@ -157,8 +185,7 @@ hostname: coreos1
|
||||
|
||||
### users
|
||||
|
||||
Add or modify users with the `users` directive by providing a list of user objects, each consisting of the following fields.
|
||||
Each field is optional and of type string unless otherwise noted.
|
||||
The `users` parameter adds or modifies the specified list of users. Each user is an object which consists of the following fields. Each field is optional and of type string unless otherwise noted.
|
||||
All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the user already exists.
|
||||
|
||||
- **name**: Required. Login name of user
|
||||
@@ -171,6 +198,7 @@ All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the use
|
||||
- **no-user-group**: Boolean. Skip default group creation.
|
||||
- **ssh-authorized-keys**: List of public SSH keys to authorize for this user
|
||||
- **coreos-ssh-import-github**: Authorize SSH keys from Github user
|
||||
- **coreos-ssh-import-url**: Authorize SSH keys imported from a url endpoint.
|
||||
- **system**: Create the user as a system user. No home directory will be created.
|
||||
- **no-log-init**: Boolean. Skip initialization of lastlog and faillog databases.
|
||||
|
||||
@@ -187,12 +215,12 @@ The following fields are not yet implemented:
|
||||
|
||||
users:
|
||||
- name: elroy
|
||||
passwd: $6$5s2u6/jR$un0AvWnqilcgaNB3Mkxd5yYv6mTlWfOoCYHZmfi3LDKVltj.E8XNKEcwWm...
|
||||
groups:
|
||||
- staff
|
||||
- docker
|
||||
ssh-authorized-keys:
|
||||
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
|
||||
passwd: $6$5s2u6/jR$un0AvWnqilcgaNB3Mkxd5yYv6mTlWfOoCYHZmfi3LDKVltj.E8XNKEcwWm...
|
||||
groups:
|
||||
- sudo
|
||||
- docker
|
||||
ssh-authorized-keys:
|
||||
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
|
||||
```
|
||||
|
||||
#### Generating a password hash
|
||||
@@ -215,12 +243,64 @@ perl -e 'print crypt("password","\$6\$SALT\$") . "\n"'
|
||||
|
||||
Using a higher number of rounds will help create more secure passwords, but given enough time, password hashes can be reversed. On most RPM based distributions there is a tool called mkpasswd available in the `expect` package, but this does not handle "rounds" nor advanced hashing algorithms.
|
||||
|
||||
#### Retrieving SSH Authorized Keys
|
||||
|
||||
##### From a GitHub User
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-github: elroy
|
||||
```
|
||||
|
||||
##### From an HTTP Endpoint
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
- name: elroy
|
||||
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:
|
||||
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-url: https://example.com/public-keys
|
||||
```
|
||||
|
||||
### write_files
|
||||
|
||||
Inject an arbitrary set of files to the local filesystem.
|
||||
Provide a list of objects with the following attributes:
|
||||
The `write-file` parameter defines a list of files to create on the local filesystem. Each file is represented as an associative array which has the following keys:
|
||||
|
||||
- **path**: Absolute location on disk where contents should be written
|
||||
- **content**: Data to write at the provided `path`
|
||||
- **permissions**: String representing file permissions in octal notation (i.e. '0644')
|
||||
- **owner**: User and group that should own the file written to disk. This is equivalent to the `<user>:<group>` argument to `chown <user>:<group> <path>`.
|
||||
|
||||
Explicitly not implemented is the **encoding** attribute.
|
||||
The **content** field must represent exactly what should be written to disk.
|
||||
|
||||
### manage_etc_hosts
|
||||
|
||||
The `manage_etc_hosts` parameter configures the contents of the `/etc/hosts` file, which is used for local name resolution.
|
||||
Currently, the only supported value is "localhost" which will cause your system's hostname
|
||||
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.
|
||||
|
||||
```
|
||||
#cloud-config
|
||||
|
||||
manage_etc_hosts: localhost
|
||||
```
|
||||
|
30
Documentation/config-drive.md
Normal file
30
Documentation/config-drive.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Distribution via Config Drive
|
||||
|
||||
CoreOS supports providing configuration data via [config drive][config-drive]
|
||||
disk images. Currently only providing a single script or cloud config file is
|
||||
supported.
|
||||
|
||||
[config-drive]: http://docs.openstack.org/user-guide/content/enable_config_drive.html#config_drive_contents
|
||||
|
||||
## Contents and Format
|
||||
|
||||
The image should be a single FAT or ISO9660 file system with the label
|
||||
`config-2` and the configuration data should be located at
|
||||
`openstack/latest/user_data`.
|
||||
|
||||
For example, to wrap up a config named `user_data` in a config drive image:
|
||||
|
||||
mkdir -p /tmp/new-drive/openstack/latest
|
||||
cp user_data /tmp/new-drive/openstack/latest/user_data
|
||||
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
|
||||
rm -r /tmp/new-drive
|
||||
|
||||
## QEMU virtfs
|
||||
|
||||
One exception to the above, when using QEMU it is possible to skip creating an
|
||||
image and use a plain directory containing the same contents:
|
||||
|
||||
qemu-system-x86_64 \
|
||||
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
|
||||
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
|
||||
[usual qemu options here...]
|
@@ -5,14 +5,13 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource"
|
||||
"github.com/coreos/coreos-cloudinit/initialize"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const version = "0.3.2"
|
||||
const version = "0.5.0"
|
||||
|
||||
func main() {
|
||||
var printVersion bool
|
||||
@@ -27,6 +26,9 @@ func main() {
|
||||
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")
|
||||
|
||||
@@ -40,8 +42,8 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if file != "" && url != "" {
|
||||
fmt.Println("Provide one of --from-file or --from-url")
|
||||
if file != "" && url != "" && !useProcCmdline {
|
||||
fmt.Println("Provide one of --from-file, --from-url or --from-proc-cmdline")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -50,8 +52,10 @@ func main() {
|
||||
ds = datasource.NewLocalFile(file)
|
||||
} else if url != "" {
|
||||
ds = datasource.NewMetadataService(url)
|
||||
} else if useProcCmdline {
|
||||
ds = datasource.NewProcCmdline()
|
||||
} else {
|
||||
fmt.Println("Provide one of --from-file or --from-url")
|
||||
fmt.Println("Provide one of --from-file, --from-url or --from-proc-cmdline")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -76,7 +80,7 @@ func main() {
|
||||
userdata := string(userdataBytes)
|
||||
userdata = env.Apply(userdata)
|
||||
|
||||
parsed, err := ParseUserData(userdata)
|
||||
parsed, err := initialize.ParseUserData(userdata)
|
||||
if err != nil {
|
||||
log.Printf("Failed parsing user-data: %v", err)
|
||||
if ignoreFailure {
|
||||
@@ -109,21 +113,3 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func ParseUserData(contents string) (interface{}, error) {
|
||||
header := strings.SplitN(contents, "\n", 2)[0]
|
||||
|
||||
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")
|
||||
cfg, err := initialize.NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
return *cfg, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,31 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Datasource interface {
|
||||
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
|
||||
}
|
||||
|
@@ -1,36 +1,15 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type metadataService struct {
|
||||
url string
|
||||
client http.Client
|
||||
url string
|
||||
}
|
||||
|
||||
func NewMetadataService(url string) *metadataService {
|
||||
return &metadataService{url, http.Client{}}
|
||||
return &metadataService{url}
|
||||
}
|
||||
|
||||
func (ms *metadataService) Fetch() ([]byte, error) {
|
||||
resp, err := ms.client.Get(ms.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
|
||||
return fetchURL(ms.url)
|
||||
}
|
||||
|
||||
func (ms *metadataService) Type() string {
|
||||
|
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
|
||||
}
|
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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,9 +17,10 @@ type CloudConfig struct {
|
||||
Units []system.Unit
|
||||
OEM OEMRelease
|
||||
}
|
||||
WriteFiles []system.File `yaml:"write_files"`
|
||||
Hostname string
|
||||
Users []system.User
|
||||
WriteFiles []system.File `yaml:"write_files"`
|
||||
Hostname string
|
||||
Users []system.User
|
||||
ManageEtcHosts string `yaml:"manage_etc_hosts"`
|
||||
}
|
||||
|
||||
func NewCloudConfig(contents string) (*CloudConfig, error) {
|
||||
@@ -91,6 +92,12 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,16 +130,17 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
|
||||
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", unit.Name)
|
||||
dst, err := system.PlaceUnit(&unit, env.Root())
|
||||
if err != nil {
|
||||
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 {
|
||||
@@ -148,7 +156,7 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
commands["systemd-networkd.service"] = "restart"
|
||||
} else {
|
||||
if unit.Command != "" {
|
||||
commands[unit.Name] = unit.Command
|
||||
commands[unit.Name] = unit.Command
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,5 +171,15 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.ManageEtcHosts != "" {
|
||||
|
||||
if err := WriteEtcHosts(cfg.ManageEtcHosts, env.Root()); err != nil {
|
||||
log.Fatalf("Failed to write /etc/hosts to filesystem: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Wrote /etc/hosts file to filesystem")
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,52 +1,18 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
type GithubUserKey struct {
|
||||
Id int `json:"id"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func fetchGithubKeys(github_url string) ([]string, error) {
|
||||
res, err := http.Get(github_url)
|
||||
defer res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data []GithubUserKey
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]string, 0)
|
||||
for _, key := range data {
|
||||
keys = append(keys, key.Key)
|
||||
}
|
||||
return keys, err
|
||||
|
||||
}
|
||||
|
||||
func SSHImportGithubUser(system_user string, github_user string) error {
|
||||
url := fmt.Sprintf("https://api.github.com/users/%s/keys", github_user)
|
||||
keys, err := fetchGithubKeys(url)
|
||||
keys, err := fetchUserKeys(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key_name := fmt.Sprintf("github-%s", github_user)
|
||||
err = system.AuthorizeSSHKeys(system_user, key_name, keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return system.AuthorizeSSHKeys(system_user, key_name, keys)
|
||||
}
|
||||
|
@@ -1,48 +1,9 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCloudConfigUsersGithubMarshal(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gh_res := `
|
||||
[
|
||||
{
|
||||
"id": 67057,
|
||||
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ=="
|
||||
},
|
||||
{
|
||||
"id": 3340477,
|
||||
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBANxpzIbTzKTeBRaOIdUxwwGwvDasTfU/PonhbNIuhYjc+xFGvBRTumox2F+luVAKKs4WdvA4nJXaY1OFi6DZftk5Bp4E2JaSzp8ulAzHsMexDdv6LGHGEJj/qdHAL1vHk2K89PpwRFSRZI8XRBLjvkr4ZgBKLG5ZILXPJEPP2j3lAAAAFQCtxoTnV8wy0c4grcGrQ+1sCsD7WQAAAIAqZsW2GviMe1RQrbZT0xAZmI64XRPrnLsoLxycHWlS7r6uUln2c6Ae2MB/YF0d4Kd1XZii9GHj7rrypqEo7MW8uSabhu70nmu1J8m2O3Dsr+4oJLeat9vwPsJV92IKO0jQwjKnAOHOiB9JKGeCw+NfXfogbti9/q38Q6XcS+SI5wAAAIEA1803Y2h+tOOpZXAsNIwl9mRfExWzLQ3L7knwJdznQu/6SW1H/1oyoYLebuk187Qj2UFI5qQ6AZNc49DvohWx0Cg6ABcyubNyoaCjZKWIdxVnItHWNbLe//+tyTu0I2eQwJOORsEPK5gMpf599C7wXQ//DzZOWbTWiHEX52gCTmk="
|
||||
},
|
||||
{
|
||||
"id": 5224438,
|
||||
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O"
|
||||
}
|
||||
]
|
||||
`
|
||||
fmt.Fprintln(w, gh_res)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
keys, err := fetchGithubKeys(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
expected := "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ=="
|
||||
if keys[0] != expected {
|
||||
t.Fatalf("expected %s, got %s", expected, keys[0])
|
||||
}
|
||||
expected = "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O"
|
||||
if keys[2] != expected {
|
||||
t.Fatalf("expected %s, got %s", expected, keys[2])
|
||||
}
|
||||
|
||||
}
|
||||
func TestCloudConfigUsersGithubUser(t *testing.T) {
|
||||
|
||||
contents := `
|
||||
|
44
initialize/manage_etc_hosts.go
Normal file
44
initialize/manage_etc_hosts.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
const DefaultIpv4Address = "127.0.0.1"
|
||||
|
||||
func generateEtcHosts(option string) (out string, err error) {
|
||||
if option != "localhost" {
|
||||
return "", errors.New("Invalid option to manage_etc_hosts")
|
||||
}
|
||||
|
||||
// use the operating system hostname
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s\n", DefaultIpv4Address, hostname), nil
|
||||
|
||||
}
|
||||
|
||||
// Write an /etc/hosts file
|
||||
func WriteEtcHosts(option string, root string) error {
|
||||
|
||||
etcHosts, err := generateEtcHosts(option)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file := system.File{
|
||||
Path: path.Join(root, "etc", "hosts"),
|
||||
RawFilePermissions: "0644",
|
||||
Content: etcHosts,
|
||||
}
|
||||
|
||||
return system.WriteFile(&file)
|
||||
}
|
76
initialize/manage_etc_hosts_test.go
Normal file
76
initialize/manage_etc_hosts_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCloudConfigManageEtcHosts(t *testing.T) {
|
||||
contents := `
|
||||
manage_etc_hosts: localhost
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
manageEtcHosts := cfg.ManageEtcHosts
|
||||
|
||||
if manageEtcHosts != "localhost" {
|
||||
t.Errorf("ManageEtcHosts value is %q, expected 'localhost'", manageEtcHosts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManageEtcHostsInvalidValue(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcHostsWrittenToDisk(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer rmdir(dir)
|
||||
|
||||
if err := WriteEtcHosts("localhost", dir); err != nil {
|
||||
t.Fatalf("WriteEtcHosts failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "hosts")
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode() != os.FileMode(0644) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read OS hostname: %v", err)
|
||||
}
|
||||
|
||||
expect := fmt.Sprintf("%s %s\n", DefaultIpv4Address, hostname)
|
||||
|
||||
if string(contents) != expect {
|
||||
t.Fatalf("File has incorrect contents")
|
||||
}
|
||||
}
|
@@ -18,9 +18,9 @@ type OEMRelease struct {
|
||||
|
||||
func (oem *OEMRelease) String() string {
|
||||
fields := []string{
|
||||
fmt.Sprintf("ID=%q", oem.ID),
|
||||
fmt.Sprintf("ID=%s", oem.ID),
|
||||
fmt.Sprintf("VERSION_ID=%s", oem.VersionID),
|
||||
fmt.Sprintf("NAME=%q", oem.Name),
|
||||
fmt.Sprintf("VERSION_ID=%q", oem.VersionID),
|
||||
fmt.Sprintf("HOME_URL=%q", oem.HomeURL),
|
||||
fmt.Sprintf("BUG_REPORT_URL=%q", oem.BugReportURL),
|
||||
}
|
||||
|
@@ -42,9 +42,9 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
expect := `ID="rackspace"
|
||||
expect := `ID=rackspace
|
||||
VERSION_ID=168.0.0
|
||||
NAME="Rackspace Cloud Servers"
|
||||
VERSION_ID="168.0.0"
|
||||
HOME_URL="https://www.rackspace.com/cloud/servers/"
|
||||
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
|
||||
`
|
||||
|
47
initialize/ssh_keys.go
Normal file
47
initialize/ssh_keys.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
type UserKey struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func SSHImportKeysFromURL(system_user string, url string) error {
|
||||
keys, err := fetchUserKeys(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key_name := fmt.Sprintf("coreos-cloudinit-%s", system_user)
|
||||
return system.AuthorizeSSHKeys(system_user, key_name, keys)
|
||||
}
|
||||
|
||||
func fetchUserKeys(url string) ([]string, error) {
|
||||
res, err := http.Get(url)
|
||||
defer res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 data {
|
||||
keys = append(keys, key.Key)
|
||||
}
|
||||
return keys, err
|
||||
}
|
69
initialize/ssh_keys_test.go
Normal file
69
initialize/ssh_keys_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCloudConfigUsersUrlMarshal(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gh_res := `
|
||||
[
|
||||
{
|
||||
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ=="
|
||||
},
|
||||
{
|
||||
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBANxpzIbTzKTeBRaOIdUxwwGwvDasTfU/PonhbNIuhYjc+xFGvBRTumox2F+luVAKKs4WdvA4nJXaY1OFi6DZftk5Bp4E2JaSzp8ulAzHsMexDdv6LGHGEJj/qdHAL1vHk2K89PpwRFSRZI8XRBLjvkr4ZgBKLG5ZILXPJEPP2j3lAAAAFQCtxoTnV8wy0c4grcGrQ+1sCsD7WQAAAIAqZsW2GviMe1RQrbZT0xAZmI64XRPrnLsoLxycHWlS7r6uUln2c6Ae2MB/YF0d4Kd1XZii9GHj7rrypqEo7MW8uSabhu70nmu1J8m2O3Dsr+4oJLeat9vwPsJV92IKO0jQwjKnAOHOiB9JKGeCw+NfXfogbti9/q38Q6XcS+SI5wAAAIEA1803Y2h+tOOpZXAsNIwl9mRfExWzLQ3L7knwJdznQu/6SW1H/1oyoYLebuk187Qj2UFI5qQ6AZNc49DvohWx0Cg6ABcyubNyoaCjZKWIdxVnItHWNbLe//+tyTu0I2eQwJOORsEPK5gMpf599C7wXQ//DzZOWbTWiHEX52gCTmk="
|
||||
},
|
||||
{
|
||||
"id": 5224438,
|
||||
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O"
|
||||
}
|
||||
]
|
||||
`
|
||||
fmt.Fprintln(w, gh_res)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
keys, err := fetchUserKeys(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
expected := "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ=="
|
||||
if keys[0] != expected {
|
||||
t.Fatalf("expected %s, got %s", expected, keys[0])
|
||||
}
|
||||
expected = "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O"
|
||||
if keys[2] != expected {
|
||||
t.Fatalf("expected %s, got %s", expected, keys[2])
|
||||
}
|
||||
|
||||
}
|
||||
func TestCloudConfigUsersSSHImportURL(t *testing.T) {
|
||||
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-url: https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.SSHImportURL != "https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys" {
|
||||
t.Errorf("ssh import url is %q, expected 'https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys'", user.SSHImportURL)
|
||||
}
|
||||
}
|
33
initialize/user_data.go
Normal file
33
initialize/user_data.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func ParseUserData(contents string) (interface{}, error) {
|
||||
header := strings.SplitN(contents, "\n", 2)[0]
|
||||
|
||||
// Explicitly trim the header so we can handle user-data from
|
||||
// non-unix operating systems. The rest of the file is parsed
|
||||
// by goyaml, which correctly handles CRLF.
|
||||
header = strings.TrimSpace(header)
|
||||
|
||||
if strings.HasPrefix(header, "#!") {
|
||||
log.Printf("Parsing user-data as script")
|
||||
return system.Script(contents), nil
|
||||
|
||||
} else if header == "#cloud-config" {
|
||||
log.Printf("Parsing user-data as cloud-config")
|
||||
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)
|
||||
}
|
||||
}
|
31
initialize/user_data_test.go
Normal file
31
initialize/user_data_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseHeaderCRLF(t *testing.T) {
|
||||
configs := []string{
|
||||
"#cloud-config\nfoo: bar",
|
||||
"#cloud-config\r\nfoo: bar",
|
||||
}
|
||||
|
||||
for i, config := range configs {
|
||||
_, err := ParseUserData(config)
|
||||
if err != nil {
|
||||
t.Errorf("Failed parsing config %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
scripts := []string{
|
||||
"#!bin/bash\necho foo",
|
||||
"#!bin/bash\r\necho foo",
|
||||
}
|
||||
|
||||
for i, script := range scripts {
|
||||
_, err := ParseUserData(script)
|
||||
if err != nil {
|
||||
t.Errorf("Failed parsing script %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,6 +19,7 @@ const fakeMachineID = "42000000000000000000000000000042"
|
||||
|
||||
type Unit struct {
|
||||
Name string
|
||||
Enable bool
|
||||
Runtime bool
|
||||
Content string
|
||||
Command string
|
||||
@@ -41,21 +42,28 @@ func (u *Unit) Group() (group string) {
|
||||
|
||||
type Script []byte
|
||||
|
||||
func PlaceUnit(u *Unit, root string) (string, error) {
|
||||
// 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"
|
||||
}
|
||||
|
||||
dst := path.Join(root, dir, "systemd", u.Group())
|
||||
if _, err := os.Stat(dst); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dst, os.FileMode(0755)); err != nil {
|
||||
return "", err
|
||||
return path.Join(root, dir, "systemd", u.Group(), u.Name)
|
||||
}
|
||||
|
||||
// PlaceUnit writes a unit file at the provided destination, creating
|
||||
// parent directories as necessary.
|
||||
func PlaceUnit(u *Unit, dst string) error {
|
||||
dir := filepath.Dir(dst)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
dst = path.Join(dst, u.Name)
|
||||
|
||||
file := File{
|
||||
Path: dst,
|
||||
Content: u.Content,
|
||||
@@ -64,10 +72,10 @@ func PlaceUnit(u *Unit, root string) (string, error) {
|
||||
|
||||
err := WriteFile(&file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
return dst, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnableUnitFile(file string, runtime bool) error {
|
||||
|
@@ -26,12 +26,17 @@ Address=10.209.171.177/19
|
||||
}
|
||||
defer syscall.Rmdir(dir)
|
||||
|
||||
if _, err := PlaceUnit(&u, dir); err != nil {
|
||||
dst := UnitDestination(&u, dir)
|
||||
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
||||
if dst != expectDst {
|
||||
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
|
||||
if err := PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("PlaceUnit failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
||||
fi, err := os.Stat(fullPath)
|
||||
fi, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
@@ -40,19 +45,19 @@ Address=10.209.171.177/19
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
contents, err := ioutil.ReadFile(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
expect := `[Match]
|
||||
expectContents := `[Match]
|
||||
Name=eth47
|
||||
|
||||
[Network]
|
||||
Address=10.209.171.177/19
|
||||
`
|
||||
if string(contents) != expect {
|
||||
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expect)
|
||||
if string(contents) != expectContents {
|
||||
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +77,17 @@ Where=/media/state
|
||||
}
|
||||
defer syscall.Rmdir(dir)
|
||||
|
||||
if _, err := PlaceUnit(&u, dir); err != nil {
|
||||
dst := UnitDestination(&u, dir)
|
||||
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
||||
if dst != expectDst {
|
||||
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
|
||||
if err := PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("PlaceUnit failed: %v", err)
|
||||
}
|
||||
|
||||
fullPath := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
||||
fi, err := os.Stat(fullPath)
|
||||
fi, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
@@ -86,17 +96,17 @@ Where=/media/state
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fullPath)
|
||||
contents, err := ioutil.ReadFile(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
expect := `[Mount]
|
||||
expectContents := `[Mount]
|
||||
What=/dev/sdb1
|
||||
Where=/media/state
|
||||
`
|
||||
if string(contents) != expect {
|
||||
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expect)
|
||||
if string(contents) != expectContents {
|
||||
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -13,6 +13,7 @@ type User struct {
|
||||
PasswordHash string `yaml:"passwd"`
|
||||
SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"`
|
||||
SSHImportGithubUser string `yaml:"coreos-ssh-import-github"`
|
||||
SSHImportURL string `yaml:"coreos-ssh-import-url"`
|
||||
GECOS string `yaml:"gecos"`
|
||||
Homedir string `yaml:"homedir"`
|
||||
NoCreateHome bool `yaml:"no-create-home"`
|
||||
@@ -33,6 +34,8 @@ func CreateUser(u *User) error {
|
||||
|
||||
if u.PasswordHash != "" {
|
||||
args = append(args, "--password", u.PasswordHash)
|
||||
} else {
|
||||
args = append(args, "--password", "*")
|
||||
}
|
||||
|
||||
if u.GECOS != "" {
|
||||
|
2
test
2
test
@@ -4,7 +4,7 @@ echo "Building bin/coreos-cloudinit"
|
||||
. build
|
||||
|
||||
echo "Running tests..."
|
||||
for pkg in "./initialize ./system"; do
|
||||
for pkg in "./initialize ./system ./datasource"; do
|
||||
go test -i $pkg
|
||||
go test -v $pkg
|
||||
done
|
||||
|
11
units/90-configdrive.rules
Normal file
11
units/90-configdrive.rules
Normal file
@@ -0,0 +1,11 @@
|
||||
# Automatically trigger configdrive mounting.
|
||||
|
||||
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}+="configdrive-block.service"
|
||||
|
||||
# Addtionally support virtfs from QEMU
|
||||
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
|
11
units/system-cloudinit@.service
Normal file
11
units/system-cloudinit@.service
Normal file
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Load cloud-config from %f
|
||||
Requires=dbus.service
|
||||
After=dbus.service
|
||||
Before=system-config.target
|
||||
ConditionFileNotEmpty=%f
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/coreos-cloudinit --from-file=%f
|
10
units/system-config.target
Normal file
10
units/system-config.target
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Load system-provided cloud configs
|
||||
|
||||
# Generate /etc/environment
|
||||
Requires=coreos-setup-environment.service
|
||||
After=coreos-setup-environment.service
|
||||
|
||||
# Load OEM cloud-config.yml
|
||||
Requires=system-cloudinit@usr-share-oem-cloud\x2dconfig.yml.service
|
||||
After=system-cloudinit@usr-share-oem-cloud\x2dconfig.yml.service
|
12
units/user-cloudinit@.service
Normal file
12
units/user-cloudinit@.service
Normal file
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Load cloud-config from %f
|
||||
Requires=coreos-setup-environment.service
|
||||
After=coreos-setup-environment.service
|
||||
Before=user-config.target
|
||||
ConditionFileNotEmpty=%f
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
EnvironmentFile=-/etc/environment
|
||||
ExecStart=/usr/bin/coreos-cloudinit --from-file=%f
|
8
units/user-config.target
Normal file
8
units/user-config.target
Normal file
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Load user-provided cloud configs
|
||||
Requires=system-config.target
|
||||
After=system-config.target
|
||||
|
||||
# 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
|
Reference in New Issue
Block a user