Compare commits

...

46 Commits

Author SHA1 Message Date
Brian Waldon
e64b61b312 chore(release): Bump version to v0.5.0 2014-04-22 16:48:21 -07:00
Brian Waldon
d72e10125a Merge pull request #84 from bcwaldon/proc-cmdline
feat(proc-cmdline): Parse /proc/cmdline for cloud-config-url
2014-04-22 16:43:05 -07:00
Brian Waldon
3de3d2c050 feat(proc-cmdline): Parse /proc/cmdline for cloud-config-url
If the --from-proc-cmdline flag is given to coreos-cloudinit, the local
/proc/cmdline file will be parsed for a cloud-config-url
2014-04-22 16:38:01 -07:00
Brian Waldon
2ff0762b0c Merge pull request #83 from robszumski/correct-headers
docs(cloud-config): correct headers
2014-04-21 19:15:50 -07:00
Rob Szumski
d6bacb24bc docs(cloud-config): correct headers 2014-04-21 17:56:35 -07:00
Brian Waldon
926eb4dbb7 Merge pull request #77 from chexxor/master
Update cloud-config.md to include expected file format
2014-04-21 14:27:22 -07:00
Brian Waldon
e7599fea58 Merge pull request #82 from bcwaldon/fix-68
fix(userdata): Strip \r when checking header
2014-04-21 14:26:31 -07:00
Brian Waldon
e98c58c656 fix(userdata): Strip \r when checking header
Fix #68
2014-04-21 13:40:26 -07:00
Alex Berg
ae350a3b34 Update cloud-config.md - use "you" 2014-04-18 11:45:02 -05:00
Alex Berg
c3b53f24cf Update cloud-config.md to use "parameter", not "option" 2014-04-18 11:45:01 -05:00
Alex Berg
8bee85e63d Update cloud-config.md based on feedback 2014-04-18 11:43:54 -05:00
Alex Berg
4c02e99bc8 Update cloud-config.md option descriptions
Re-word a few more things to look more like docs.
2014-04-18 11:43:53 -05:00
Alex Berg
0fb5291dd0 Update cloud-config.md to include expected file format
Clarify root-level keys. Use page structure to indicate expected values.
2014-04-18 11:43:53 -05:00
Brian Waldon
7f55876378 Merge pull request #79 from robszumski/note-config-drive
feat(docs): note config-drive
2014-04-17 09:36:57 -07:00
Brian Waldon
eb51a89f78 Merge pull request #72 from bcwaldon/unit-enable
Address unit enabling issues
2014-04-17 09:32:47 -07:00
Rob Szumski
588ff4c26c feat(docs): note config-drive 2014-04-16 22:35:39 -07:00
Michael Marineau
5472de8821 Merge pull request #78 from robszumski/update-user-group
fix(docs): use better group example
2014-04-16 16:49:50 -07:00
Rob Szumski
e6b632f817 fix(docs): use better group example 2014-04-16 16:48:04 -07:00
Michael Marineau
13a3d892ca Merge pull request #76 from marineam/units2
fix(units): Relax ordering requirements for now.
2014-04-15 15:19:13 -07:00
Brian Waldon
2e237ebead Merge pull request #66 from bcwaldon/doc-encoding
doc(write_files): Explicitly document lack of encoding support
2014-04-15 10:06:14 -07:00
Brian Waldon
61bb63b6e6 feat(unit): Allow units to be enabled even if contents not provided 2014-04-15 09:00:53 -07:00
Brian Waldon
476761cf62 refactor(unit): Separate UnitDestination from PlaceUnit 2014-04-15 09:00:53 -07:00
Brian Waldon
5981e12ac0 feat(unit): Allow user to control enabling units
Fix #69 - A user may provide an `enable` attribute of a unit in their
cloud config document. If true, coreos-cloudinit will instruct systemd
to enable the associated unit. If false, the unit will not be enabled.

Fix #71 - The default enable behavior has been changed from on to off.
2014-04-15 09:00:52 -07:00
Michael Marineau
78d8be8427 fix(units): Relax ordering requirements for now.
The current cloudinit implementation blocks when starting units which
causes it to deadlock the boot process if a system cloud config starts a
user cloud config because the user configs want to run after system is
done. Until cloudinit switches to non-blocking calls user configs will
go back to just depending on coreos-setup-environment.service.
2014-04-14 21:39:40 -04:00
Michael Marineau
10d73930d9 Merge pull request #62 from marineam/units
add(units): Generic config drive and other systemd units.
2014-04-11 13:26:59 -07:00
Brandon Philips
ea12c0bfd1 Merge pull request #67 from robszumski/remove_disco
fix(docs): remove real discovery token
2014-04-09 21:56:26 -07:00
Rob Szumski
6540d12d25 fix(docs): remove real discovery token 2014-04-09 21:55:19 -07:00
Michael Marineau
c438a42587 feat(units): Generic config drive and other systemd units. 2014-04-09 19:10:07 -07:00
Brian Waldon
19f8fe49af doc(write_files): Explicitly document lack of encoding support 2014-04-08 08:34:39 -07:00
Michael Marineau
58b091061e Merge pull request #57 from marineam/passwd
fix(user): Use '*' as default password field rather than '!'
2014-04-07 14:13:25 -07:00
Brian Waldon
8a7df360ac Merge pull request #65 from bcwaldon/hosts-newline
fix(manage_etc_hosts): Append newline to /etc/hosts
2014-04-07 11:09:27 -07:00
Brian Waldon
ba7cf90315 fix(manage_etc_hosts): Append newline to /etc/hosts 2014-04-07 11:01:17 -07:00
Brian Waldon
8841740a2b doc(oem): remove quotes from oem doc 2014-04-07 10:58:13 -07:00
Brian Waldon
dfe1255ac3 chore(release): Bump version to v0.4.0+git 2014-04-07 10:23:58 -07:00
Brian Waldon
0fddd1735d chore(release): Bump version to v0.4.0 2014-04-07 10:23:28 -07:00
Brandon Philips
f779a3f7f5 Merge pull request #64 from philips/no-quotes-on-oem-id-or-version
fix(initialize): don't quote version or ID
2014-04-07 10:17:29 -07:00
Brandon Philips
7015338aef fix(initialize): don't quote version or ID
The update_engine parsing and XML generation code is very naive. Instead
of trying to implement a correct parser and generater in C++ just
generate a file that doesn't have quote's around fields that we know
won't have spaces.
2014-04-07 09:56:57 -07:00
Brian Waldon
34aa147ebe Merge pull request #58 from gabrtv/manage_etc_hosts
feat(etc-hosts) add support for manage_etc_hosts: localhost
2014-04-02 23:11:03 -07:00
Gabriel Monroy
4d02e1da8e feat(etc-hosts) add support for manage_etc_hosts: localhost
This feature is based on https://github.com/number5/cloud-init/blob/master/doc/examples/cloud-config.txt#L447:L482
2014-04-01 16:02:12 -06:00
Michael Marineau
5ef3e1f32b fix(user): Use '*' as default password field rather than '!'
When using openssh without pam it checks for a ! prefix in the password
field, locking the account entirely if found. The other common lock
character, *, is allowed by ssh to login via ssh keys so use it instead.
2014-03-31 22:20:02 -07:00
polvi
23d02363ee Merge pull request #56 from cbmd/patch-1
Fixed indentation for users creation example
2014-03-28 08:40:34 -07:00
Vadym Okun
3c4fe9e260 Fixed indentation for users creation example 2014-03-28 13:23:58 +02:00
Brian Waldon
a594e053f5 chore(doc): clean up formatting 2014-03-27 20:19:42 -07:00
Brian Waldon
f3ba47ac89 Merge pull request #48 from calavera/key_import_url
feat(ssh-import): Add ssh-import-url user attribute.
2014-03-27 20:16:10 -07:00
David Calavera
7d814396b7 feat(ssh-import): Add ssh-import-url user attribute. 2014-03-28 09:39:47 +08:00
Brian Waldon
47ca113385 chore(release): Bump version to v0.3.2+git 2014-03-27 18:14:24 -07:00
29 changed files with 756 additions and 192 deletions

View File

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

View 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...]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 := `

View 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)
}

View 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")
}
}

View File

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

View File

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

View 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
View 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)
}
}

View 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)
}
}
}

View File

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

View File

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

View File

@@ -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
View File

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

View 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"

View 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

View 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

View 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

View 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

View 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
View 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