Compare commits

..

45 Commits

Author SHA1 Message Date
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
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
Brian Waldon
639c693153 chore(release): Bump version to v0.3.2 2014-03-27 18:14:16 -07:00
Brian Waldon
b4027077ff Merge pull request #55 from bcwaldon/drop-reload
fix(units): Drop automatic daemon-reload
2014-03-27 18:12:22 -07:00
Brian Waldon
580460ff3f fix(units): Drop automatic daemon-reload 2014-03-27 17:30:05 -07:00
Brian Waldon
b246ec0397 chore(release): Bump version to v0.3.1+git 2014-03-25 20:06:19 -07:00
Brian Waldon
4977c774d8 chore(release): Bump version to v0.3.1 2014-03-25 20:06:07 -07:00
Brian Waldon
661bae11fc Merge pull request #53 from bcwaldon/fix-reload
Fix systemd daemon-reload
2014-03-25 20:04:24 -07:00
Brian Waldon
58ae898948 fix(systemd): Update usage of dbus.Reload 2014-03-25 19:37:05 -07:00
Brian Waldon
f5f9a0a6a9 bump(github.com/coreos/go-systemd/dbus): 4fbc5060a317b142e6c7bfbedb65596d5f0ab99b 2014-03-25 19:37:05 -07:00
Brian Waldon
477ae29135 fix(systemd): Fail if daemon-reload returns error 2014-03-25 18:50:48 -07:00
Brian Waldon
0203d4a9f3 chore(release): Bump version to v0.3.0+git 2014-03-24 18:03:45 -07:00
Brian Waldon
e68134d884 chore(release): Bump version to v0.3.0 2014-03-24 18:03:34 -07:00
Brian Waldon
2ad33487d7 Merge pull request #51 from bcwaldon/default-command
fix(unit): Default unit command to NOP
2014-03-24 16:29:58 -07:00
Brian Waldon
b778fe6f41 fix(unit): Default unit command to NOP 2014-03-24 14:12:59 -07:00
Brian Waldon
3d7bda9f6b Merge pull request #49 from bcwaldon/oem-release
oem-release
2014-03-24 13:43:10 -07:00
Brian Waldon
3d01211937 feat(coreos.oem): Write coreos.oem fields to /etc/oem-release 2014-03-24 13:42:35 -07:00
Brian Waldon
61808c2002 chore(release): Bump version to v0.2.2+git 2014-03-21 14:43:16 -07:00
Brian Waldon
35655809ff chore(release): Bump version to v0.2.2 2014-03-21 14:43:05 -07:00
Brian Waldon
81e4f1f896 Merge pull request #46 from bcwaldon/doc-field-sub
doc(fields): Document field substitution
2014-03-21 14:41:53 -07:00
Brian Waldon
e0b65066ab doc(fields): Document field substitution 2014-03-21 14:36:12 -07:00
Brian Waldon
8e0f0998df Merge pull request #44 from bcwaldon/ip-sub
feat($ip): Substitute $[public|private]_ipv4 in whole user-data
2014-03-21 13:58:24 -07:00
Brian Waldon
ddd035aaa7 feat($ip): Substitute $[public|private]_ipv4 in whole user-data 2014-03-21 11:01:16 -07:00
Brian Waldon
568714cadb Merge pull request #43 from robszumski/master
fix(docs): remove extra quotation mark
2014-03-20 11:37:15 -07:00
Rob Szumski
9c94b3fe21 fix(docs): remove extra quotation mark 2014-03-20 11:26:17 -07:00
Brian Waldon
267617ed1f Merge pull request #42 from bcwaldon/doc
doc(user-data): Move user-data doc to README.md
2014-03-20 10:50:40 -07:00
Brian Waldon
bc37171a2e doc(user-data): Move user-data doc to README.md 2014-03-20 09:13:32 -07:00
Brian Waldon
490152bd16 chore(release): Bump version to v0.2.1+git 2014-03-19 19:08:00 -07:00
Brian Waldon
9ade6673ba chore(release): Bump version to v0.2.1 2014-03-19 19:07:01 -07:00
Brian Waldon
67043681cd fix(doc): Add missing backticks 2014-03-19 17:16:28 -07:00
Brian Waldon
b89ddae983 Merge pull request #40 from bcwaldon/unit-command
Implement unit.command
2014-03-19 16:06:01 -07:00
Brian Waldon
88a6e77449 feat(unit.command): Add command field to units 2014-03-19 15:56:29 -07:00
Brian Waldon
09c473a6cb fix(fleet): Drop coreos.fleet from cloud-config 2014-03-19 14:43:25 -07:00
Brian Waldon
48f733f448 Merge pull request #39 from bcwaldon/update-etcd-docs
Replace lost etcd docs
2014-03-19 14:30:01 -07:00
Brian Waldon
aeac9f987d doc(etcd): Update etcd docs 2014-03-19 14:26:23 -07:00
Brian Waldon
9757705ae8 chore(release): Bump version to v0.2.0+git 2014-03-19 08:57:49 -07:00
23 changed files with 921 additions and 249 deletions

View File

@@ -8,29 +8,91 @@ Only a subset of [cloud-config functionality][cloud-config] is implemented. A se
## CoreOS Parameters
### coreos.etcd.discovery_url
### coreos.etcd
The value of `coreos.etcd.discovery_url` will be used to discover the instance's etcd peers using the [etcd discovery protocol][disco-proto]. Usage of the [public discovery service][disco-service] is encouraged. **Note:** this is currently Amazon-only.
The `coreos.etcd.*` options are 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...
```
#cloud-config
coreos:
etcd:
discovery_url: https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877
name: node001
discovery: https://discovery.etcd.io/3445fa65423d8b04df07f59fb40218f8
addr: $public_ipv4:4001
peer-addr: $private_ipv4:7001
```
[disco-proto]: https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md
[disco-service]: http://discovery.etcd.io
...will generate a systemd unit drop-in like this:
```
[Service]
Environment="ETCD_NAME=node001"
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/3445fa65423d8b04df07f59fb40218f8"
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].
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
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:
- **id**: Lowercase string identifying the OEM
- **name**: Human-friendly string representing the OEM
- **version-id**: Lowercase string identifying the version of the OEM
- **home-url**: Link to the homepage of the provider or OEM
- **bug-report-url**: Link to a place to file bug reports about this OEM
coreos-cloudinit renders these fields to `/etc/oem-release`.
If no **id** field is provided, coreos-cloudinit will ignore this section.
For example, the following cloud-config document...
```
#cloud-config
coreos:
oem:
id: rackspace
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
```
...would be rendered to the following `/etc/oem-release`:
```
ID="rackspace"
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"
```
[os-release]: http://www.freedesktop.org/software/systemd/man/os-release.html
### coreos.units
Arbitrary systemd units may be provided in the `coreos.units` attribute.
`coreos.units` is a list of objects with the following fields:
- **name**: string representing unit's name
- **runtime**: boolean indicating whether or not to persist the unit across reboots. This is analagous to the `--runtime` flag to `systemd enable`.
- **content**: plaintext string representing entire unit file
- **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.
- **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.
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
##### Examples
Write a unit to disk, automatically starting it.
```
#cloud-config
@@ -53,6 +115,19 @@ coreos:
WantedBy=local.target
```
Start the builtin `etcd` and `fleet` services:
```
# cloud-config
coreos:
units:
- name: etcd.service
command: start
- name: fleet.service
command: start
```
## Cloud-Config Parameters
### ssh_authorized_keys
@@ -96,6 +171,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.
@@ -140,6 +216,41 @@ 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 field `coreos-ssh-import-github` you can make coreos-cloudinit to add the public ssh keys from a GitHub user as authorized keys to a server.
```
#cloud-config
users:
- name: elroy
coreos-ssh-import-github: elroy
```
#### Retrieving ssh authorized keys from an http endpoint
coreos-cloudinit can also pull public SSH keys from any http endpoint that 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 provide any url which response matches that 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.
@@ -150,12 +261,15 @@ Provide a list of objects with the following attributes:
- **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>`.
## user-data Script
### manage_etc_hosts
Simply set your user-data to a script where the first line is a shebang:
Have coreos-cloudinit manage your /etc/hosts file for local name resolution.
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.
```
#!/bin/bash
#cloud-config
echo 'Hello, world!'
manage_etc_hosts: localhost
```

View File

@@ -1,9 +1,79 @@
# coreos-cloudinit
coreos-cloudinit enables a user to customize CoreOS machines by providing either an executable script or a cloud-config document as instance user-data.
coreos-cloudinit enables a user to customize CoreOS machines by providing either a cloud-config document or an executable script through user-data.
## Supported Cloud-Config Features
## Configuration with cloud-config
A subset of [cloud-config][cloud-config] is implemented in coreos-cloudinit and is [documented here](https://github.com/coreos/coreos-cloudinit/tree/master/Documentation/cloud-config.md). In addition specific CoreOS paramaters were added for unit files, etcd discovery urls, and others.
A subset of the [official cloud-config spec][official-cloud-config] is implemented by coreos-cloudinit.
Additionally, several [CoreOS-specific options][custom-cloud-config] have been implemented to support interacting with unit files, bootstrapping etcd clusters, and more.
All supported cloud-config parameters are [documented here][all-cloud-config].
[cloud-config]: http://cloudinit.readthedocs.org/en/latest/topics/format.html#cloud-config-data
[official-cloud-config]: http://cloudinit.readthedocs.org/en/latest/topics/format.html#cloud-config-data
[custom-cloud-config]: https://github.com/coreos/coreos-cloudinit/blob/master/Documentation/cloud-config.md#coreos-parameters
[all-cloud-config]: https://github.com/coreos/coreos-cloudinit/tree/master/Documentation/cloud-config.md
The following is an example cloud-config document:
```
#cloud-config
coreos:
units:
- name: etcd.service
command: start
users:
- name: core
passwd: $1$allJZawX$00S5T756I5PGdQga5qhqv1
write_files:
- path: /etc/resolv.conf
content: |
nameserver 192.0.2.2
nameserver 192.0.2.3
```
## Executing a Script
coreos-cloudinit supports executing user-data as a script instead of parsing it as a cloud-config document.
Make sure the first line of your user-data is a shebang and coreos-cloudinit will attempt to execute it:
```
#!/bin/bash
echo 'Hello, world!'
```
## user-data Field Substitution
coreos-cloudinit will replace the following set of tokens in your user-data with system-generated values.
| Token | Description |
| ------------- | ----------- |
| $public_ipv4 | Public IPv4 address of machine |
| $private_ipv4 | Private IPv4 address of machine |
These values are determined by CoreOS based on the given provider on which your machine is running.
Read more about provider-specific functionality in the [CoreOS OEM documentation][oem-doc].
[oem-doc]: https://coreos.com/docs/sdk-distributors/distributors/notes-for-distributors/
For example, submitting the following user-data...
```
#cloud-config
coreos:
etcd:
addr: $public_ipv4:4001
peer-addr: $private_ipv4:7001
```
...will result in this cloud-config document being executed:
```
#cloud-config
coreos:
etcd:
addr: 203.0.113.29:4001
peer-addr: 192.0.2.13:7001
```

View File

@@ -1,8 +1,6 @@
package main
import (
"bufio"
"bytes"
"flag"
"fmt"
"log"
@@ -14,7 +12,7 @@ import (
"github.com/coreos/coreos-cloudinit/system"
)
const version = "0.2.0"
const version = "0.4.0"
func main() {
var printVersion bool
@@ -58,7 +56,7 @@ func main() {
}
log.Printf("Fetching user-data from datasource of type %q", ds.Type())
userdata, err := ds.Fetch()
userdataBytes, err := ds.Fetch()
if err != nil {
log.Printf("Failed fetching user-data from datasource: %v", err)
if ignoreFailure {
@@ -68,11 +66,16 @@ func main() {
}
}
if len(userdata) == 0 {
if len(userdataBytes) == 0 {
log.Printf("No user data to handle, exiting.")
os.Exit(0)
}
env := initialize.NewEnvironment("/", workspace)
userdata := string(userdataBytes)
userdata = env.Apply(userdata)
parsed, err := ParseUserData(userdata)
if err != nil {
log.Printf("Failed parsing user-data: %v", err)
@@ -83,7 +86,6 @@ func main() {
}
}
env := initialize.NewEnvironment("/", workspace)
err = initialize.PrepWorkspace(env.Workspace())
if err != nil {
log.Fatalf("Failed preparing workspace: %v", err)
@@ -107,16 +109,14 @@ func main() {
}
}
func ParseUserData(contents []byte) (interface{}, error) {
bytereader := bytes.NewReader(contents)
bufreader := bufio.NewReader(bytereader)
header, _ := bufreader.ReadString('\n')
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\n" {
} else if header == "#cloud-config" {
log.Printf("Parsing user-data as cloud-config")
cfg, err := initialize.NewCloudConfig(contents)
if err != nil {

View File

@@ -14,17 +14,18 @@ type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct {
Etcd EtcdEnvironment
Fleet struct{ Autostart bool }
Units []system.Unit
OEM OEMRelease
}
WriteFiles []system.File `yaml:"write_files"`
Hostname string
Users []system.User
ManageEtcHosts string `yaml:"manage_etc_hosts"`
}
func NewCloudConfig(contents []byte) (*CloudConfig, error) {
func NewCloudConfig(contents string) (*CloudConfig, error) {
var cfg CloudConfig
err := goyaml.Unmarshal(contents, &cfg)
err := goyaml.Unmarshal([]byte(contents), &cfg)
return &cfg, err
}
@@ -48,6 +49,13 @@ func Apply(cfg CloudConfig, env *Environment) error {
log.Printf("Set hostname to %s", cfg.Hostname)
}
if cfg.Coreos.OEM.ID != "" {
if err := WriteOEMRelease(&cfg.Coreos.OEM, env.Root()); err != nil {
return err
}
log.Printf("Wrote /etc/oem-release to filesystem")
}
if len(cfg.Users) > 0 {
for _, user := range cfg.Users {
if user.Name == "" {
@@ -84,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
}
}
}
}
@@ -115,8 +129,11 @@ 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 {
log.Printf("Placing unit %s on filesystem", unit.Name)
if unit.Content != "" {
log.Printf("Writing unit %s to filesystem", unit.Name)
dst, err := system.PlaceUnit(&unit, env.Root())
if err != nil {
return err
@@ -133,17 +150,34 @@ func Apply(cfg CloudConfig, env *Environment) error {
log.Printf("Skipping enable for network-like unit %s", unit.Name)
}
}
system.DaemonReload()
system.StartUnits(cfg.Coreos.Units)
if unit.Group() == "network" {
commands["systemd-networkd.service"] = "restart"
} else {
if unit.Command != "" {
commands[unit.Name] = unit.Command
}
}
}
if cfg.Coreos.Fleet.Autostart {
err := system.StartUnitByName("fleet.service")
if err == nil {
log.Printf("Started fleet service.")
} else {
for unit, command := range commands {
log.Printf("Calling unit command '%s %s'", command, unit)
res, err := system.RunUnitCommand(command, unit)
if err != nil {
return err
}
log.Printf("Result of '%s %s': %s", command, unit, res)
}
}
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

@@ -7,7 +7,7 @@ import (
// Assert that the parsing of a cloud config file "generally works"
func TestCloudConfigEmpty(t *testing.T) {
cfg, err := NewCloudConfig([]byte{})
cfg, err := NewCloudConfig("")
if err != nil {
t.Fatalf("Encountered unexpected error :%v", err)
}
@@ -17,10 +17,6 @@ func TestCloudConfigEmpty(t *testing.T) {
t.Error("Parsed incorrect number of SSH keys")
}
if cfg.Coreos.Fleet.Autostart {
t.Error("Expected AutostartFleet not to be defined")
}
if len(cfg.WriteFiles) != 0 {
t.Error("Expected zero WriteFiles")
}
@@ -32,12 +28,10 @@ func TestCloudConfigEmpty(t *testing.T) {
// Assert that the parsing of a cloud config file "generally works"
func TestCloudConfig(t *testing.T) {
contents := []byte(`
contents := `
coreos:
etcd:
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
fleet:
autostart: Yes
units:
- name: 50-eth0.network
runtime: yes
@@ -51,6 +45,12 @@ coreos:
Address=10.209.171.177/19
'
oem:
id: rackspace
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
ssh_authorized_keys:
- foobar
- foobaz
@@ -62,7 +62,7 @@ write_files:
permissions: '0644'
owner: root:dogepack
hostname: trontastic
`)
`
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error :%v", err)
@@ -77,10 +77,6 @@ hostname: trontastic
t.Error("Expected first SSH key to be 'foobaz'")
}
if !cfg.Coreos.Fleet.Autostart {
t.Error("Expected AutostartFleet to be true")
}
if len(cfg.WriteFiles) != 1 {
t.Error("Failed to parse correct number of write_files")
} else {
@@ -126,6 +122,10 @@ Address=10.209.171.177/19
}
}
if cfg.Coreos.OEM.ID != "rackspace" {
t.Errorf("Failed parsing coreos.oem. Expected ID 'rackspace', got %q.", cfg.Coreos.OEM.ID)
}
if cfg.Hostname != "trontastic" {
t.Errorf("Failed to parse hostname")
}
@@ -133,10 +133,10 @@ Address=10.209.171.177/19
// Assert that our interface conversion doesn't panic
func TestCloudConfigKeysNotList(t *testing.T) {
contents := []byte(`
contents := `
ssh_authorized_keys:
- foo: bar
`)
`
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error :%v", err)
@@ -149,7 +149,7 @@ ssh_authorized_keys:
}
func TestCloudConfigSerializationHeader(t *testing.T) {
cfg, _ := NewCloudConfig([]byte{})
cfg, _ := NewCloudConfig("")
contents := cfg.String()
header := strings.SplitN(contents, "\n", 2)[0]
if header != "#cloud-config" {
@@ -158,7 +158,7 @@ func TestCloudConfigSerializationHeader(t *testing.T) {
}
func TestCloudConfigUsers(t *testing.T) {
contents := []byte(`
contents := `
users:
- name: elroy
passwd: somehash
@@ -174,7 +174,7 @@ users:
no-user-group: true
system: y
no-log-init: True
`)
`
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error: %v", err)

View File

@@ -1,7 +1,9 @@
package initialize
import (
"os"
"path"
"strings"
)
const DefaultSSHKeyName = "coreos-cloudinit"
@@ -10,10 +12,15 @@ type Environment struct {
root string
workspace string
sshKeyName string
substitutions map[string]string
}
func NewEnvironment(root, workspace string) *Environment {
return &Environment{root, workspace, DefaultSSHKeyName}
substitutions := map[string]string{
"$public_ipv4": os.Getenv("COREOS_PUBLIC_IPV4"),
"$private_ipv4": os.Getenv("COREOS_PRIVATE_IPV4"),
}
return &Environment{root, workspace, DefaultSSHKeyName, substitutions}
}
func (self *Environment) Workspace() string {
@@ -31,3 +38,10 @@ func (self *Environment) SSHKeyName() string {
func (self *Environment) SetSSHKeyName(name string) {
self.sshKeyName = name
}
func (self *Environment) Apply(data string) string {
for key, val := range self.substitutions {
data = strings.Replace(data, key, val, -1)
}
return data
}

27
initialize/env_test.go Normal file
View File

@@ -0,0 +1,27 @@
package initialize
import (
"os"
"testing"
)
func TestEnvironmentApply(t *testing.T) {
os.Setenv("COREOS_PUBLIC_IPV4", "192.0.2.3")
os.Setenv("COREOS_PRIVATE_IPV4", "192.0.2.203")
env := NewEnvironment("./", "./")
input := `[Service]
ExecStart=/usr/bin/echo "$public_ipv4"
ExecStop=/usr/bin/echo $private_ipv4
ExecStop=/usr/bin/echo $unknown
`
expected := `[Service]
ExecStart=/usr/bin/echo "192.0.2.3"
ExecStop=/usr/bin/echo 192.0.2.203
ExecStop=/usr/bin/echo $unknown
`
output := env.Apply(input)
if output != expected {
t.Fatalf("Environment incorrectly applied.\nOutput:\n%s\nExpected:\n%s", output, expected)
}
}

View File

@@ -3,7 +3,6 @@ package initialize
import (
"errors"
"fmt"
"os"
"path"
"strings"
@@ -32,20 +31,9 @@ func (ec EtcdEnvironment) String() (out string) {
}
}
public := os.Getenv("COREOS_PUBLIC_IPV4")
private := os.Getenv("COREOS_PRIVATE_IPV4")
out += "[Service]\n"
for key, val := range norm {
if public != "" {
val = strings.Replace(val, "$public_ipv4", public, -1)
}
if private != "" {
val = strings.Replace(val, "$private_ipv4", private, -1)
}
out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val)
}

View File

@@ -58,25 +58,6 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
}
}
func TestEtcdEnvironmentReplacement(t *testing.T) {
os.Clearenv()
os.Setenv("COREOS_PUBLIC_IPV4", "203.0.113.29")
os.Setenv("COREOS_PRIVATE_IPV4", "192.0.2.13")
cfg := make(EtcdEnvironment, 0)
cfg["bind-addr"] = "$public_ipv4:4001"
cfg["peer-bind-addr"] = "$private_ipv4:7001"
env := cfg.String()
expect := `[Service]
Environment="ETCD_BIND_ADDR=203.0.113.29:4001"
Environment="ETCD_PEER_BIND_ADDR=192.0.2.13:7001"
`
if env != expect {
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
}
}
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
ec := EtcdEnvironment{
"name": "node001",

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,55 +1,16 @@
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 := []byte(`
contents := `
users:
- name: elroy
coreos-ssh-import-github: bcwaldon
`)
`
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error: %v", err)

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", 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", DefaultIpv4Address, hostname)
if string(contents) != expect {
t.Fatalf("File has incorrect contents")
}
}

39
initialize/oem.go Normal file
View File

@@ -0,0 +1,39 @@
package initialize
import (
"fmt"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
type OEMRelease struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
VersionID string `yaml:"version-id"`
HomeURL string `yaml:"home-url"`
BugReportURL string `yaml:"bug-report-url"`
}
func (oem *OEMRelease) String() string {
fields := []string{
fmt.Sprintf("ID=%s", oem.ID),
fmt.Sprintf("VERSION_ID=%s", oem.VersionID),
fmt.Sprintf("NAME=%q", oem.Name),
fmt.Sprintf("HOME_URL=%q", oem.HomeURL),
fmt.Sprintf("BUG_REPORT_URL=%q", oem.BugReportURL),
}
return strings.Join(fields, "\n") + "\n"
}
func WriteOEMRelease(oem *OEMRelease, root string) error {
file := system.File{
Path: path.Join(root, "etc", "oem-release"),
RawFilePermissions: "0644",
Content: oem.String(),
}
return system.WriteFile(&file)
}

54
initialize/oem_test.go Normal file
View File

@@ -0,0 +1,54 @@
package initialize
import (
"io/ioutil"
"os"
"path"
"syscall"
"testing"
)
func TestOEMReleaseWrittenToDisk(t *testing.T) {
oem := OEMRelease{
ID: "rackspace",
Name: "Rackspace Cloud Servers",
VersionID: "168.0.0",
HomeURL: "https://www.rackspace.com/cloud/servers/",
BugReportURL: "https://github.com/coreos/coreos-overlay",
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
if err := WriteOEMRelease(&oem, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "etc", "oem-release")
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)
}
expect := `ID=rackspace
VERSION_ID=168.0.0
NAME="Rackspace Cloud Servers"
HOME_URL="https://www.rackspace.com/cloud/servers/"
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
`
if string(contents) != expect {
t.Fatalf("File has incorrect contents")
}
}

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

View File

@@ -21,6 +21,7 @@ type Unit struct {
Name string
Runtime bool
Content string
Command string
}
func (u *Unit) Type() string {
@@ -80,34 +81,33 @@ func EnableUnitFile(file string, runtime bool) error {
return err
}
func separateNetworkUnits(units []Unit) ([]Unit, []Unit) {
networkUnits := make([]Unit, 0)
nonNetworkUnits := make([]Unit, 0)
for _, unit := range units {
if unit.Group() == "network" {
networkUnits = append(networkUnits, unit)
} else {
nonNetworkUnits = append(nonNetworkUnits, unit)
}
}
return networkUnits, nonNetworkUnits
}
func StartUnits(units []Unit) error {
networkUnits, nonNetworkUnits := separateNetworkUnits(units)
if len(networkUnits) > 0 {
if err := RestartUnitByName("systemd-networkd.service"); err != nil {
return err
}
func RunUnitCommand(command, unit string) (string, error) {
conn, err := dbus.New()
if err != nil {
return "", err
}
for _, unit := range nonNetworkUnits {
if err := RestartUnitByName(unit.Name); err != nil {
return err
}
var fn func(string, string) (string, error)
switch command {
case "start":
fn = conn.StartUnit
case "stop":
fn = conn.StopUnit
case "restart":
fn = conn.RestartUnit
case "reload":
fn = conn.ReloadUnit
case "try-restart":
fn = conn.TryRestartUnit
case "reload-or-restart":
fn = conn.ReloadOrRestartUnit
case "reload-or-try-restart":
fn = conn.ReloadOrTryRestartUnit
default:
return "", fmt.Errorf("Unsupported systemd command %q", command)
}
return nil
return fn(unit, "replace")
}
func DaemonReload() error {
@@ -116,31 +116,7 @@ func DaemonReload() error {
return err
}
_, err = conn.Reload()
return err
}
func RestartUnitByName(name string) error {
log.Printf("Restarting unit %s", name)
conn, err := dbus.New()
if err != nil {
return err
}
output, err := conn.RestartUnit(name, "replace")
log.Printf("Restart completed with '%s'", output)
return err
}
func StartUnitByName(name string) error {
conn, err := dbus.New()
if err != nil {
return err
}
_, err = conn.StartUnit(name, "replace")
return err
return conn.Reload()
}
func ExecuteScript(scriptPath string) (string, error) {

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

View File

@@ -18,6 +18,8 @@ limitations under the License.
package dbus
import (
"os"
"strconv"
"strings"
"sync"
@@ -73,7 +75,12 @@ func (c *Conn) initConnection() error {
return err
}
err = c.sysconn.Auth(nil)
// Only use EXTERNAL method, and hardcode the uid (not username)
// to avoid a username lookup (which requires a dynamically linked
// libc)
methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))}
err = c.sysconn.Auth(methods)
if err != nil {
c.sysconn.Close()
return err

View File

@@ -35,6 +35,7 @@ func (c *Conn) jobComplete(signal *dbus.Signal) {
out, ok := c.jobListener.jobs[job]
if ok {
out <- result
delete(c.jobListener.jobs, job)
}
c.jobListener.Unlock()
}
@@ -137,8 +138,8 @@ func (c *Conn) KillUnit(name string, signal int32) {
c.sysobj.Call("org.freedesktop.systemd1.Manager.KillUnit", 0, name, "all", signal).Store()
}
// GetUnitProperties takes the unit name and returns all of its dbus object properties.
func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) {
// getProperties takes the unit name and returns all of its dbus object properties, for the given dbus interface
func (c *Conn) getProperties(unit string, dbusInterface string) (map[string]interface{}, error) {
var err error
var props map[string]dbus.Variant
@@ -148,7 +149,7 @@ func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) {
}
obj := c.sysconn.Object("org.freedesktop.systemd1", path)
err = obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, "org.freedesktop.systemd1.Unit").Store(&props)
err = obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, dbusInterface).Store(&props)
if err != nil {
return nil, err
}
@@ -161,6 +162,55 @@ func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) {
return out, nil
}
// GetUnitProperties takes the unit name and returns all of its dbus object properties.
func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) {
return c.getProperties(unit, "org.freedesktop.systemd1.Unit")
}
func (c *Conn) getProperty(unit string, dbusInterface string, propertyName string) (*Property, error) {
var err error
var prop dbus.Variant
path := ObjectPath("/org/freedesktop/systemd1/unit/" + unit)
if !path.IsValid() {
return nil, errors.New("invalid unit name: " + unit)
}
obj := c.sysconn.Object("org.freedesktop.systemd1", path)
err = obj.Call("org.freedesktop.DBus.Properties.Get", 0, dbusInterface, propertyName).Store(&prop)
if err != nil {
return nil, err
}
return &Property{Name: propertyName, Value: prop}, nil
}
func (c *Conn) GetUnitProperty(unit string, propertyName string) (*Property, error) {
return c.getProperty(unit, "org.freedesktop.systemd1.Unit", propertyName)
}
// GetUnitTypeProperties returns the extra properties for a unit, specific to the unit type.
// Valid values for unitType: Service, Socket, Target, Device, Mount, Automount, Snapshot, Timer, Swap, Path, Slice, Scope
// return "dbus.Error: Unknown interface" if the unitType is not the correct type of the unit
func (c *Conn) GetUnitTypeProperties(unit string, unitType string) (map[string]interface{}, error) {
return c.getProperties(unit, "org.freedesktop.systemd1."+unitType)
}
// SetUnitProperties() may be used to modify certain unit properties at runtime.
// Not all properties may be changed at runtime, but many resource management
// settings (primarily those in systemd.cgroup(5)) may. The changes are applied
// instantly, and stored on disk for future boots, unless runtime is true, in which
// case the settings only apply until the next reboot. name is the name of the unit
// to modify. properties are the settings to set, encoded as an array of property
// name and value pairs.
func (c *Conn) SetUnitProperties(name string, runtime bool, properties ...Property) error {
return c.sysobj.Call("SetUnitProperties", 0, name, runtime, properties).Store()
}
func (c *Conn) GetUnitTypeProperty(unit string, unitType string, propertyName string) (*Property, error) {
return c.getProperty(unit, "org.freedesktop.systemd1." + unitType, propertyName)
}
// ListUnits returns an array with all currently loaded units. Note that
// units may be known by multiple names at the same time, and hence there might
// be more unit names loaded than actual units behind them.
@@ -253,8 +303,52 @@ type EnableUnitFileChange struct {
Destination string // Destination of the symlink
}
// DisableUnitFiles() may be used to disable one or more units in the system (by
// removing symlinks to them from /etc or /run).
//
// It takes a list of unit files to disable (either just file names or full
// absolute paths if the unit files are residing outside the usual unit
// search paths), and one boolean: whether the unit was enabled for runtime
// only (true, /run), or persistently (false, /etc).
//
// This call returns an array with the changes made. The changes list
// consists of structures with three strings: the type of the change (one of
// symlink or unlink), the file name of the symlink and the destination of the
// symlink.
func (c *Conn) DisableUnitFiles(files []string, runtime bool) ([]DisableUnitFileChange, error) {
result := make([][]interface{}, 0)
err := c.sysobj.Call("DisableUnitFiles", 0, files, runtime).Store(&result)
if err != nil {
return nil, err
}
resultInterface := make([]interface{}, len(result))
for i := range result {
resultInterface[i] = result[i]
}
changes := make([]DisableUnitFileChange, len(result))
changesInterface := make([]interface{}, len(changes))
for i := range changes {
changesInterface[i] = &changes[i]
}
err = dbus.Store(resultInterface, changesInterface...)
if err != nil {
return nil, err
}
return changes, nil
}
type DisableUnitFileChange struct {
Type string // Type of the change (one of symlink or unlink)
Filename string // File name of the symlink
Destination string // Destination of the symlink
}
// Reload instructs systemd to scan for and reload unit files. This is
// equivalent to a 'systemctl daemon-reload'.
func (c *Conn) Reload() (string, error) {
return c.runJob("org.freedesktop.systemd1.Manager.Reload")
func (c *Conn) Reload() error {
return c.sysobj.Call("org.freedesktop.systemd1.Manager.Reload", 0).Store()
}

View File

@@ -18,9 +18,11 @@ package dbus
import (
"fmt"
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"math/rand"
"os"
"path/filepath"
"reflect"
"testing"
)
@@ -50,13 +52,16 @@ func setupUnit(target string, conn *Conn, t *testing.T) {
fixture := []string{abs}
install, changes, err := conn.EnableUnitFiles(fixture, true, true)
if err != nil {
t.Fatal(err)
}
if install != false {
t.Fatal("Install was true")
}
if len(changes) < 1 {
t.Fatal("Expected one change, got %v", changes)
t.Fatalf("Expected one change, got %v", changes)
}
if changes[0].Filename != targetRun {
@@ -118,6 +123,37 @@ func TestStartStopUnit(t *testing.T) {
}
}
// Enables a unit and then immediately tears it down
func TestEnableDisableUnit(t *testing.T) {
target := "enable-disable.service"
conn := setupConn(t)
setupUnit(target, conn, t)
abs, err := filepath.Abs("../fixtures/" + target)
if err != nil {
t.Fatal(err)
}
path := filepath.Join("/run/systemd/system/", target)
// 2. Disable the unit
changes, err := conn.DisableUnitFiles([]string{abs}, true)
if err != nil {
t.Fatal(err)
}
if len(changes) != 1 {
t.Fatalf("Changes should include the path, %v", changes)
}
if changes[0].Filename != path {
t.Fatalf("Change should include correct filename, %+v", changes[0])
}
if changes[0].Destination != "" {
t.Fatalf("Change destination should be empty, %+v", changes[0])
}
}
// TestGetUnitProperties reads the `-.mount` which should exist on all systemd
// systems and ensures that one of its properties is valid.
func TestGetUnitProperties(t *testing.T) {
@@ -139,6 +175,20 @@ func TestGetUnitProperties(t *testing.T) {
if names[0] != "system.slice" {
t.Fatal("unexpected wants for /")
}
prop, err := conn.GetUnitProperty(unit, "Wants")
if err != nil {
t.Fatal(err)
}
if prop.Name != "Wants" {
t.Fatal("unexpected property name")
}
val := prop.Value.Value().([]string)
if !reflect.DeepEqual(val, names) {
t.Fatal("unexpected property value")
}
}
// TestGetUnitPropertiesRejectsInvalidName attempts to get the properties for a
@@ -150,10 +200,37 @@ func TestGetUnitPropertiesRejectsInvalidName(t *testing.T) {
unit := "//invalid#$^/"
_, err := conn.GetUnitProperties(unit)
if err == nil {
t.Fatal("Expected an error, got nil")
}
_, err = conn.GetUnitProperty(unit, "Wants")
if err == nil {
t.Fatal("Expected an error, got nil")
}
}
// TestSetUnitProperties changes a cgroup setting on the `tmp.mount`
// which should exist on all systemd systems and ensures that the
// property was set.
func TestSetUnitProperties(t *testing.T) {
conn := setupConn(t)
unit := "tmp.mount"
if err := conn.SetUnitProperties(unit, true, Property{"CPUShares", dbus.MakeVariant(uint64(1023))}); err != nil {
t.Fatal(err)
}
info, err := conn.GetUnitTypeProperties(unit, "Mount")
if err != nil {
t.Fatal(err)
}
value := info["CPUShares"].(uint64)
if value != 1023 {
t.Fatal("CPUShares of unit is not 1023, %s", value)
}
}
// Ensure that basic transient unit starting and stopping works.
@@ -211,3 +288,27 @@ func TestStartStopTransientUnit(t *testing.T) {
t.Fatalf("Test unit found in list, should be stopped")
}
}
func TestConnJobListener(t *testing.T) {
target := "start-stop.service"
conn := setupConn(t)
setupUnit(target, conn, t)
jobSize := len(conn.jobListener.jobs)
_, err := conn.StartUnit(target, "replace")
if err != nil {
t.Fatal(err)
}
_, err = conn.StopUnit(target, "replace")
if err != nil {
t.Fatal(err)
}
currentJobSize := len(conn.jobListener.jobs)
if jobSize != currentJobSize {
t.Fatal("JobListener jobs leaked")
}
}

View File

@@ -209,3 +209,12 @@ func PropPropagatesReloadTo(units ...string) Property {
func PropRequiresMountsFor(units ...string) Property {
return propDependency("RequiresMountsFor", units)
}
// PropSlice sets the Slice unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#Slice=
func PropSlice(slice string) Property {
return Property{
Name: "Slice",
Value: dbus.MakeVariant(slice),
}
}