Compare commits

...

122 Commits

Author SHA1 Message Date
Brian Waldon
a398ce82f7 chore(release): Bump version to v0.6.1 2014-05-14 10:52:22 -07:00
Brian Waldon
e814b37839 Merge pull request #107 from bcwaldon/locksmith-no-etc
fix(coreos-cloudinit): Ensure /etc/coreos exists before writing to it
2014-05-14 10:49:23 -07:00
Brian Waldon
cb4d9e81a4 fix(coreos-cloudinit): Ensure /etc/coreos exists before writing to it 2014-05-14 10:47:18 -07:00
Jonathan Boulle
b87a4628e6 Merge pull request #99 from jonboulle/simple
chore(cloudinit): remove superfluous check
2014-05-12 10:51:51 -07:00
Jonathan Boulle
b22fdd5ac9 Merge pull request #104 from jonboulle/tests
feat(tests): add coverage script
2014-05-12 10:51:38 -07:00
Jonathan Boulle
6939fc2ddc feat(tests): add cover script 2014-05-10 01:42:57 -07:00
Jonathan Boulle
e3117269cb chore(cloudinit): remove superfluous check 2014-05-09 20:32:51 -07:00
Brandon Philips
3bb3a683a4 Merge pull request #98 from philips/remove-oem-from-default
chore(Documentation): move OEM into its own doc
2014-05-08 09:41:42 -07:00
Brandon Philips
e1033c979e chore(Documentation): move OEM into its own doc
People are customizing the OEM needlessly. Just move it into its own
doc.
2014-05-08 09:32:21 -07:00
Jonathan Boulle
9a4d24826f Merge pull request #80 from jonboulle/master
users[i].primary-group option seems invalid
2014-05-07 21:12:45 -07:00
Jonathan Boulle
7bed1307e1 fix(user): user correct primary group flag for useradd 2014-05-07 14:06:51 -07:00
Brandon Philips
47b536532d chore(coreos-cloudinit): version +git 2014-05-06 21:09:40 -07:00
Brandon Philips
7df5cf761e chore(coreos-cloudinit): bump to 0.6.0
The major feature in this release is coreos.update.reboot-strategy
2014-05-06 21:05:42 -07:00
Brandon Philips
799c02865c Merge pull request #96 from philips/locksmith-support
Add locksmith support v2
2014-05-06 21:00:44 -07:00
Brandon Philips
9f38792d43 fix(initialize): use REBOOT_STRATEGY in update.conf
Change from STRATEGY to REBOOT_STRATEGY and update the function names to
reflect that this is a config now.
2014-05-06 20:57:29 -07:00
Alex Polvi
7e4fa423e4 feat(initialize): add locksmith configuration
configure locksmith strategy based on the cloud config.
2014-05-06 20:57:28 -07:00
Brandon Philips
c3f17bd07b feat(system): add MaskUnit to systemd 2014-05-06 17:46:16 -07:00
Brandon Philips
85a473d972 Merge pull request #95 from philips/various-code-cleanups
chore(initialize): code cleanups and gofmt
2014-05-06 16:19:35 -07:00
Brandon Philips
aea5ca5252 chore(initialize): code cleanups and gofmt 2014-05-06 16:13:21 -07:00
Michael Marineau
4e84180ad5 chore(release): Bump version to v0.5.2+git 2014-05-05 14:09:08 -07:00
Michael Marineau
0f1717bf26 chore(release): Bump version to v0.5.2 2014-05-05 14:07:50 -07:00
Michael Marineau
6a9aa60a8d Merge pull request #93 from marineam/reload
Revert "fix(units): Drop automatic daemon-reload"
2014-05-05 14:02:16 -07:00
Michael Marineau
7cacb2e127 Revert "fix(units): Drop automatic daemon-reload"
daemon-reload should be fixed now and the latest CoreOS with locksmith
is causing the etcd unit to get lazy-loaded before all the cloudinit
processes have finished configuring etcd via dropin files. In short,
the luck we were relying on to get by without daemon-reload has
officially run out. Cross your fingers!

This reverts commit 580460ff3f.
2014-05-05 13:16:07 -07:00
Brian Waldon
1f688dcdca Merge pull request #92 from bcwaldon/crlf-test
test(crlf): Add test that parses user-data with carriage returns
2014-05-05 10:50:25 -07:00
Brian Waldon
f6d8190e8f test(crlf): Add test that parses user-data with carriage returns 2014-05-05 10:49:02 -07:00
Brandon Philips
3263816cf5 Merge https://github.com/coreos/template-project 2014-05-05 09:44:59 -07:00
Michael Marineau
96e1cb5a7a Merge pull request #89 from robszumski/doc-write-files
feat(docs): include write_files example
2014-04-29 11:26:44 -07:00
Rob Szumski
cf556d2a81 feat(docs): include write_files example 2014-04-29 11:17:22 -07:00
Jonathan Boulle
62bda8e6cc Merge pull request #88 from robszumski/master
fix(docs): start the example unit
2014-04-29 12:15:44 -06:00
Rob Szumski
0d1d1f77be fix(docs): start the example unit 2014-04-28 10:57:11 -07:00
Michael Marineau
a7e21747fa Merge pull request #87 from marineam/hack
fix(configdrive): Always run after OEM and ec2 metadata.
2014-04-23 14:54:19 -07:00
Michael Marineau
26b54534d6 fix(configdrive): Always run after OEM and ec2 metadata.
A workaround for https://github.com/coreos/coreos-cloudinit/issues/86

Longer term cloudinit needs to be fixed to not corrupt the system when
multiple config sources are being used. We've pretty much gotten this
far without this coming up because most configs don't conflict so badly.
2014-04-23 14:38:54 -07:00
Brian Waldon
8201d75115 chore(release): Bump version to v0.5.1+git 2014-04-22 18:22:35 -07:00
Brian Waldon
1d024af4c1 chore(release): Bump version to v0.5.1 2014-04-22 18:22:24 -07:00
Brian Waldon
09c690cbe7 Merge pull request #85 from bcwaldon/pxe-unit
feat(proc-cmdline): Add proc-cmdline unit
2014-04-22 18:21:51 -07:00
Brian Waldon
49adf19081 feat(proc-cmdline): Add proc-cmdline unit
This unit will always be started, but will only do anything if
a `cloud-config-url=<url>` token is provided in /proc/cmdline.
2014-04-22 17:56:52 -07:00
Brian Waldon
46b046c82e chore(release): Bump version to v0.5.0+git 2014-04-22 16:48:32 -07:00
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
Jonathan Boulle
e01a1f70c3 Merge pull request #2 from jonboulle/master
Clean up CONTRIBUTING.md and other bits of template-project
2014-04-04 10:41:40 -07:00
Jonathan Boulle
2e4ea503b0 chore(contributing): clean up CONTRIBUTING.md and split out DCO 2014-04-04 10:40:37 -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
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
Brandon Philips
c7aef5fdf2 Merge pull request #1 from bcwaldon/fix-case
fix(CONTRIBUTING.md): Fix title case
2014-02-05 15:52:24 -08:00
Brian Waldon
c4605160c5 fix(CONTRIBUTING.md): Fix title case 2014-02-05 15:51:24 -08:00
Brandon Philips
054de85da2 feat(*): initial commit 2014-01-19 12:25:11 -08:00
52 changed files with 2059 additions and 371 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
*.swp *.swp
bin/ bin/
coverage/
pkg/ pkg/

87
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,87 @@
# How to Contribute
CoreOS projects are [Apache 2.0 licensed](LICENSE) and accept contributions via
GitHub pull requests. This document outlines some of the conventions on
development workflow, commit message formatting, contact points and other
resources to make it easier to get your contribution accepted.
# Certificate of Origin
By contributing to this project you agree to the Developer Certificate of
Origin (DCO). This document was created by the Linux Kernel community and is a
simple statement that you, as a contributor, have the legal right to make the
contribution. See the [DCO](DCO) file for details.
# Email and Chat
The project currently uses the general CoreOS email list and IRC channel:
- Email: [coreos-dev](https://groups.google.com/forum/#!forum/coreos-dev)
- IRC: #[coreos](irc://irc.freenode.org:6667/#coreos) IRC channel on freenode.org
## Getting Started
- Fork the repository on GitHub
- Read the [README](README.md) for build and test instructions
- Play with the project, submit bugs, submit patches!
## Contribution Flow
This is a rough outline of what a contributor's workflow looks like:
- Create a topic branch from where you want to base your work (usually master).
- Make commits of logical units.
- Make sure your commit messages are in the proper format (see below).
- Push your changes to a topic branch in your fork of the repository.
- Make sure the tests pass, and add any new tests as appropriate.
- Submit a pull request to the original repository.
Thanks for your contributions!
### Format of the Commit Message
We follow a rough convention for commit messages borrowed from AngularJS. This
is an example of a commit:
```
feat(scripts/test-cluster): add a cluster test command
this uses tmux to setup a test cluster that you can easily kill and
start for debugging.
```
The format can be described more formally as follows:
```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The first line is the subject and should be no longer than 70 characters, the
second line is always blank, and other lines should be wrapped at 80 characters.
This allows the message to be easier to read on GitHub as well as in various
git tools.
#### Subject Line
The subject line contains a succinct description of the change.
#### Allowed `<type>`s
- *feat* (feature)
- *fix* (bug fix)
- *docs* (documentation)
- *style* (formatting, missing semi colons, …)
- *refactor*
- *test* (when adding missing tests)
- *chore* (maintain)
#### Allowed `<scope>`s
Scopes can anything specifying the place of the commit change in the code base -
for example, "api", "store", etc.
For more details on the commit format, see the [AngularJS commit style
guide](https://docs.google.com/a/coreos.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#).

36
DCO Normal file
View File

@@ -0,0 +1,36 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.

View File

@@ -0,0 +1,37 @@
## OEM configuration
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. Customizing this section is only needed when generating a new OEM of CoreOS from the SDK. The fields include:
- **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

View File

@@ -1,36 +1,107 @@
# 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 [cloud-config]: http://cloudinit.readthedocs.org/en/latest/topics/format.html#cloud-config-data
## CoreOS Parameters ### File Format
### coreos.etcd.discovery_url The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
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. 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...
``` ```
#cloud-config #cloud-config
coreos: coreos:
etcd: etcd:
discovery_url: https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877 name: node001
# 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
``` ```
[disco-proto]: https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md ...will generate a systemd unit drop-in like this:
[disco-service]: http://discovery.etcd.io
### coreos.units ```
[Service]
Environment="ETCD_NAME=node001"
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"
```
Arbitrary systemd units may be provided in the `coreos.units` attribute. For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
`coreos.units` is a list of objects with the following fields: Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
- **name**: string representing unit's name [etcd-config]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md
- **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 #### update
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
- **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed.
- _reboot_: Reboot immediately after an update is applied.
- _etcd-lock_: Reboot after first taking a distributed lock in etcd, this guarantees that only one host will reboot concurrently and that the cluster will remain available during the update.
- _best-effort_ - If etcd is running, "etcd-lock", otherwise simply "reboot".
- _off_ - Disable rebooting after updates are applied (not recommended).
```
#cloud-config
coreos:
update:
reboot-strategy: etcd-lock
```
#### units
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.
**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 #cloud-config
@@ -38,6 +109,7 @@ Arbitrary systemd units may be provided in the `coreos.units` attribute.
coreos: coreos:
units: units:
- name: docker-redis.service - name: docker-redis.service
command: start
content: | content: |
[Unit] [Unit]
Description=Redis container Description=Redis container
@@ -53,14 +125,25 @@ coreos:
WantedBy=local.target WantedBy=local.target
``` ```
## Cloud-Config Parameters Start the builtin `etcd` and `fleet` services:
```
# cloud-config
coreos:
units:
- name: etcd.service
command: start
- name: fleet.service
command: start
```
### ssh_authorized_keys ### 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. 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 #cloud-config
@@ -71,7 +154,7 @@ ssh_authorized_keys:
### hostname ### 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`). This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
``` ```
@@ -82,8 +165,7 @@ hostname: coreos1
### users ### users
Add or modify users with the `users` directive by providing a list of user objects, each consisting of the following fields. 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.
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. All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the user already exists.
- **name**: Required. Login name of user - **name**: Required. Login name of user
@@ -96,6 +178,7 @@ All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the use
- **no-user-group**: Boolean. Skip default group creation. - **no-user-group**: Boolean. Skip default group creation.
- **ssh-authorized-keys**: List of public SSH keys to authorize for this user - **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-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. - **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. - **no-log-init**: Boolean. Skip initialization of lastlog and faillog databases.
@@ -112,12 +195,12 @@ The following fields are not yet implemented:
users: users:
- name: elroy - name: elroy
passwd: $6$5s2u6/jR$un0AvWnqilcgaNB3Mkxd5yYv6mTlWfOoCYHZmfi3LDKVltj.E8XNKEcwWm... passwd: $6$5s2u6/jR$un0AvWnqilcgaNB3Mkxd5yYv6mTlWfOoCYHZmfi3LDKVltj.E8XNKEcwWm...
groups: groups:
- staff - sudo
- docker - docker
ssh-authorized-keys: ssh-authorized-keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h... - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
``` ```
#### Generating a password hash #### Generating a password hash
@@ -140,22 +223,74 @@ 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. 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 ### write_files
Inject an arbitrary set of files to the local filesystem. 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:
Provide a list of objects with the following attributes:
- **path**: Absolute location on disk where contents should be written - **path**: Absolute location on disk where contents should be written
- **content**: Data to write at the provided `path` - **content**: Data to write at the provided `path`
- **permissions**: String representing file permissions in octal notation (i.e. '0644') - **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>`. - **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 Explicitly not implemented is the **encoding** attribute.
The **content** field must represent exactly what should be written to disk.
Simply set your user-data to a script where the first line is a shebang:
``` ```
#!/bin/bash #cloud-config
write_files:
echo 'Hello, world!' - path: /etc/fleet/fleet.conf
permissions: 0644
content: |
verbosity=1
metadata="region=us-west,type=ssd"
```
### 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...]

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

5
NOTICE Normal file
View File

@@ -0,0 +1,5 @@
CoreOS Project
Copyright 2014 CoreOS, Inc
This product includes software developed at CoreOS, Inc.
(http://www.coreos.com/).

View File

@@ -1,9 +1,79 @@
# coreos-cloudinit # 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
```

5
build
View File

@@ -1,6 +1,9 @@
#!/bin/bash -e #!/bin/bash -e
ORG_PATH="github.com/coreos"
REPO_PATH="${ORG_PATH}/coreos-cloudinit"
export GOBIN=${PWD}/bin export GOBIN=${PWD}/bin
export GOPATH=${PWD} export GOPATH=${PWD}
go build -o bin/coreos-cloudinit github.com/coreos/coreos-cloudinit go build -o bin/coreos-cloudinit ${REPO_PATH}

View File

@@ -1,20 +1,17 @@
package main package main
import ( import (
"bufio"
"bytes"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"strings"
"github.com/coreos/coreos-cloudinit/datasource" "github.com/coreos/coreos-cloudinit/datasource"
"github.com/coreos/coreos-cloudinit/initialize" "github.com/coreos/coreos-cloudinit/initialize"
"github.com/coreos/coreos-cloudinit/system" "github.com/coreos/coreos-cloudinit/system"
) )
const version = "0.2.0" const version = "0.6.1"
func main() { func main() {
var printVersion bool var printVersion bool
@@ -29,6 +26,9 @@ func main() {
var url string var url string
flag.StringVar(&url, "from-url", "", "Download user-data from provided url") 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 var workspace string
flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data") flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
@@ -42,23 +42,20 @@ func main() {
os.Exit(0) os.Exit(0)
} }
if file != "" && url != "" {
fmt.Println("Provide one of --from-file or --from-url")
os.Exit(1)
}
var ds datasource.Datasource var ds datasource.Datasource
if file != "" { if file != "" {
ds = datasource.NewLocalFile(file) ds = datasource.NewLocalFile(file)
} else if url != "" { } else if url != "" {
ds = datasource.NewMetadataService(url) ds = datasource.NewMetadataService(url)
} else if useProcCmdline {
ds = datasource.NewProcCmdline()
} else { } 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) os.Exit(1)
} }
log.Printf("Fetching user-data from datasource of type %q", ds.Type()) log.Printf("Fetching user-data from datasource of type %q", ds.Type())
userdata, err := ds.Fetch() userdataBytes, err := ds.Fetch()
if err != nil { if err != nil {
log.Printf("Failed fetching user-data from datasource: %v", err) log.Printf("Failed fetching user-data from datasource: %v", err)
if ignoreFailure { if ignoreFailure {
@@ -68,12 +65,17 @@ func main() {
} }
} }
if len(userdata) == 0 { if len(userdataBytes) == 0 {
log.Printf("No user data to handle, exiting.") log.Printf("No user data to handle, exiting.")
os.Exit(0) os.Exit(0)
} }
parsed, err := ParseUserData(userdata) env := initialize.NewEnvironment("/", workspace)
userdata := string(userdataBytes)
userdata = env.Apply(userdata)
parsed, err := initialize.ParseUserData(userdata)
if err != nil { if err != nil {
log.Printf("Failed parsing user-data: %v", err) log.Printf("Failed parsing user-data: %v", err)
if ignoreFailure { if ignoreFailure {
@@ -83,7 +85,6 @@ func main() {
} }
} }
env := initialize.NewEnvironment("/", workspace)
err = initialize.PrepWorkspace(env.Workspace()) err = initialize.PrepWorkspace(env.Workspace())
if err != nil { if err != nil {
log.Fatalf("Failed preparing workspace: %v", err) log.Fatalf("Failed preparing workspace: %v", err)
@@ -106,24 +107,3 @@ func main() {
log.Fatalf("Failed resolving user-data: %v", err) log.Fatalf("Failed resolving user-data: %v", err)
} }
} }
func ParseUserData(contents []byte) (interface{}, error) {
bytereader := bytes.NewReader(contents)
bufreader := bufio.NewReader(bytereader)
header, _ := bufreader.ReadString('\n')
if strings.HasPrefix(header, "#!") {
log.Printf("Parsing user-data as script")
return system.Script(contents), nil
} else if header == "#cloud-config\n" {
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)
}
}

27
cover Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash -e
#
# Generate coverage HTML for a package
# e.g. PKG=./initialize ./cover
#
if [ -z "$PKG" ]; then
echo "cover only works with a single package, sorry"
exit 255
fi
COVEROUT="coverage"
if ! [ -d "$COVEROUT" ]; then
mkdir "$COVEROUT"
fi
# strip out slashes and dots
COVERPKG=${PKG//\//}
COVERPKG=${COVERPKG//./}
# generate arg for "go test"
export COVER="-coverprofile ${COVEROUT}/${COVERPKG}.out"
source ./test
go tool cover -html=${COVEROUT}/${COVERPKG}.out

View File

@@ -1,6 +1,31 @@
package datasource package datasource
import (
"io/ioutil"
"net/http"
)
type Datasource interface { type Datasource interface {
Fetch() ([]byte, error) Fetch() ([]byte, error)
Type() string 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 package datasource
import (
"io/ioutil"
"net/http"
)
type metadataService struct { type metadataService struct {
url string url string
client http.Client
} }
func NewMetadataService(url string) *metadataService { func NewMetadataService(url string) *metadataService {
return &metadataService{url, http.Client{}} return &metadataService{url}
} }
func (ms *metadataService) Fetch() ([]byte, error) { func (ms *metadataService) Fetch() ([]byte, error) {
resp, err := ms.client.Get(ms.url) return fetchURL(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
} }
func (ms *metadataService) Type() string { 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

@@ -13,18 +13,20 @@ import (
type CloudConfig struct { type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"` SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct { Coreos struct {
Etcd EtcdEnvironment Etcd EtcdEnvironment
Fleet struct{ Autostart bool } Update map[string]string
Units []system.Unit Units []system.Unit
OEM OEMRelease
} }
WriteFiles []system.File `yaml:"write_files"` WriteFiles []system.File `yaml:"write_files"`
Hostname string Hostname string
Users []system.User Users []system.User
ManageEtcHosts string `yaml:"manage_etc_hosts"`
} }
func NewCloudConfig(contents []byte) (*CloudConfig, error) { func NewCloudConfig(contents string) (*CloudConfig, error) {
var cfg CloudConfig var cfg CloudConfig
err := goyaml.Unmarshal(contents, &cfg) err := goyaml.Unmarshal([]byte(contents), &cfg)
return &cfg, err return &cfg, err
} }
@@ -48,6 +50,13 @@ func Apply(cfg CloudConfig, env *Environment) error {
log.Printf("Set hostname to %s", cfg.Hostname) 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 { if len(cfg.Users) > 0 {
for _, user := range cfg.Users { for _, user := range cfg.Users {
if user.Name == "" { if user.Name == "" {
@@ -84,6 +93,12 @@ func Apply(cfg CloudConfig, env *Environment) error {
return err 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
}
}
} }
} }
@@ -114,36 +129,68 @@ func Apply(cfg CloudConfig, env *Environment) error {
log.Printf("Wrote etcd config file to filesystem") log.Printf("Wrote etcd config file to filesystem")
} }
if s, ok := cfg.Coreos.Update["reboot-strategy"]; ok {
if err := WriteLocksmithConfig(s, env.Root()); err != nil {
log.Fatalf("Failed to write locksmith config to filesystem: %v", err)
}
log.Printf("Wrote locksmith config file to filesystem")
}
if len(cfg.Coreos.Units) > 0 { if len(cfg.Coreos.Units) > 0 {
commands := make(map[string]string, 0)
for _, unit := range cfg.Coreos.Units { for _, unit := range cfg.Coreos.Units {
log.Printf("Placing unit %s on filesystem", unit.Name) dst := system.UnitDestination(&unit, env.Root())
dst, err := system.PlaceUnit(&unit, env.Root()) if unit.Content != "" {
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
if err := system.PlaceUnit(&unit, dst); err != nil {
return err
}
log.Printf("Placed unit %s at %s", unit.Name, dst)
}
if unit.Enable {
if unit.Group() != "network" {
log.Printf("Enabling unit file %s", dst)
if err := system.EnableUnitFile(dst, unit.Runtime); err != nil {
return err
}
log.Printf("Enabled unit %s", unit.Name)
} else {
log.Printf("Skipping enable for network-like unit %s", unit.Name)
}
}
if unit.Group() == "network" {
commands["systemd-networkd.service"] = "restart"
} else {
if unit.Command != "" {
commands[unit.Name] = unit.Command
}
}
}
if err := system.DaemonReload(); err != nil {
log.Fatalf("Failed systemd daemon-reload: %v", err)
}
for unit, command := range commands {
log.Printf("Calling unit command '%s %s'", command, unit)
res, err := system.RunUnitCommand(command, unit)
if err != nil { if err != nil {
return err return err
} }
log.Printf("Placed unit %s at %s", unit.Name, dst) log.Printf("Result of '%s %s': %s", command, unit, res)
if unit.Group() != "network" {
log.Printf("Enabling unit file %s", dst)
if err := system.EnableUnitFile(dst, unit.Runtime); err != nil {
return err
}
log.Printf("Enabled unit %s", unit.Name)
} else {
log.Printf("Skipping enable for network-like unit %s", unit.Name)
}
} }
system.DaemonReload()
system.StartUnits(cfg.Coreos.Units)
} }
if cfg.Coreos.Fleet.Autostart { if cfg.ManageEtcHosts != "" {
err := system.StartUnitByName("fleet.service")
if err == nil { if err := WriteEtcHosts(cfg.ManageEtcHosts, env.Root()); err != nil {
log.Printf("Started fleet service.") log.Fatalf("Failed to write /etc/hosts to filesystem: %v", err)
} else {
return err
} }
log.Printf("Wrote /etc/hosts file to filesystem")
} }
return nil return nil

View File

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

View File

@@ -1,19 +1,26 @@
package initialize package initialize
import ( import (
"os"
"path" "path"
"strings"
) )
const DefaultSSHKeyName = "coreos-cloudinit" const DefaultSSHKeyName = "coreos-cloudinit"
type Environment struct { type Environment struct {
root string root string
workspace string workspace string
sshKeyName string sshKeyName string
substitutions map[string]string
} }
func NewEnvironment(root, workspace string) *Environment { 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 { func (self *Environment) Workspace() string {
@@ -31,3 +38,10 @@ func (self *Environment) SSHKeyName() string {
func (self *Environment) SetSSHKeyName(name string) { func (self *Environment) SetSSHKeyName(name string) {
self.sshKeyName = name 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 ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"path" "path"
"strings" "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" out += "[Service]\n"
for key, val := range norm { 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) out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val)
} }

View File

@@ -5,7 +5,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"syscall"
"testing" "testing"
) )
@@ -58,36 +57,17 @@ 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) { func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
ec := EtcdEnvironment{ ec := EtcdEnvironment{
"name": "node001", "name": "node001",
"discovery": "http://disco.example.com/foobar", "discovery": "http://disco.example.com/foobar",
"peer-bind-addr": "127.0.0.1:7002", "peer-bind-addr": "127.0.0.1:7002",
} }
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer syscall.Rmdir(dir) defer os.RemoveAll(dir)
if err := WriteEtcdEnvironment(ec, dir); err != nil { if err := WriteEtcdEnvironment(ec, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err) t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
@@ -125,7 +105,7 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer syscall.Rmdir(dir) defer os.RemoveAll(dir)
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755)) os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444)) err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
@@ -153,6 +133,6 @@ Environment="ETCD_NAME=node007"
} }
func rmdir(path string) error { func rmdir(path string) error {
cmd := exec.Command("rm", "-rf", path) cmd := exec.Command("rm", "-rf", path)
return cmd.Run() return cmd.Run()
} }

View File

@@ -1,52 +1,18 @@
package initialize package initialize
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"github.com/coreos/coreos-cloudinit/system" "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 { func SSHImportGithubUser(system_user string, github_user string) error {
url := fmt.Sprintf("https://api.github.com/users/%s/keys", github_user) url := fmt.Sprintf("https://api.github.com/users/%s/keys", github_user)
keys, err := fetchGithubKeys(url) keys, err := fetchUserKeys(url)
if err != nil { if err != nil {
return err return err
} }
key_name := fmt.Sprintf("github-%s", github_user) key_name := fmt.Sprintf("github-%s", github_user)
err = system.AuthorizeSSHKeys(system_user, key_name, keys) return system.AuthorizeSSHKeys(system_user, key_name, keys)
if err != nil {
return err
}
return nil
} }

View File

@@ -1,55 +1,16 @@
package initialize package initialize
import ( import (
"fmt"
"net/http"
"net/http/httptest"
"testing" "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) { func TestCloudConfigUsersGithubUser(t *testing.T) {
contents := []byte(` contents := `
users: users:
- name: elroy - name: elroy
coreos-ssh-import-github: bcwaldon coreos-ssh-import-github: bcwaldon
`) `
cfg, err := NewCloudConfig(contents) cfg, err := NewCloudConfig(contents)
if err != nil { if err != nil {
t.Fatalf("Encountered unexpected error: %v", err) t.Fatalf("Encountered unexpected error: %v", err)

85
initialize/locksmith.go Normal file
View File

@@ -0,0 +1,85 @@
package initialize
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
const locksmithUnit = "locksmithd.service"
// addStrategy creates an `/etc/coreos/update.conf` file with the requested
// strategy via rewriting the file on disk or by starting from
// `/usr/share/coreos/update.conf`.
func addStrategy(strategy string, root string) error {
etcUpdate := path.Join(root, "etc", "coreos", "update.conf")
usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf")
// Ensure /etc/coreos/ exists before attempting to write a file in it
os.MkdirAll(path.Join(root, "etc", "coreos"), 0755)
tmp, err := ioutil.TempFile(path.Join(root, "etc", "coreos"), ".update.conf")
if err != nil {
return err
}
err = tmp.Chmod(0644)
if err != nil {
return err
}
conf, err := os.Open(etcUpdate)
if os.IsNotExist(err) {
conf, err = os.Open(usrUpdate)
if err != nil {
return err
}
}
scanner := bufio.NewScanner(conf)
sawStrat := false
stratLine := "REBOOT_STRATEGY="+strategy
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "REBOOT_STRATEGY=") {
line = stratLine
sawStrat = true
}
fmt.Fprintln(tmp, line)
if err := scanner.Err(); err != nil {
return err
}
}
if !sawStrat {
fmt.Fprintln(tmp, stratLine)
}
return os.Rename(tmp.Name(), etcUpdate)
}
// WriteLocksmithConfig updates the `update.conf` file with a REBOOT_STRATEGY for locksmith.
func WriteLocksmithConfig(strategy string, root string) error {
cmd := "restart"
if strategy == "off" {
err := system.MaskUnit(locksmithUnit, root)
if err != nil {
return err
}
cmd = "stop"
} else {
return addStrategy(strategy, root)
}
if err := system.DaemonReload(); err != nil {
return err
}
if _, err := system.RunUnitCommand(cmd, locksmithUnit); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,91 @@
package initialize
import (
"io/ioutil"
"os"
"path"
"testing"
)
const (
base = `SERVER=https://example.com
GROUP=thegroupc`
configured = base + `
REBOOT_STRATEGY=awesome
`
expected = base + `
REBOOT_STRATEGY=etcd-lock
`
)
func setupFixtures(dir string) {
os.MkdirAll(path.Join(dir, "usr", "share", "coreos"), 0755)
os.MkdirAll(path.Join(dir, "run", "systemd", "system"), 0755)
ioutil.WriteFile(path.Join(dir, "usr", "share", "coreos", "update.conf"), []byte(base), 0644)
}
func TestLocksmithEnvironmentWrittenToDisk(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
setupFixtures(dir)
for i := 0; i < 2; i++ {
if i == 1 {
err = ioutil.WriteFile(path.Join(dir, "etc", "coreos", "update.conf"), []byte(configured), 0644)
if err != nil {
t.Fatal(err)
}
}
if err := WriteLocksmithConfig("etcd-lock", dir); err != nil {
t.Fatalf("Processing of LocksmithEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "etc", "coreos", "update.conf")
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != expected {
t.Fatalf("File has incorrect contents, got %v, wanted %v", string(contents), expected)
}
}
}
func TestLocksmithEnvironmentMasked(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
setupFixtures(dir)
if err := WriteLocksmithConfig("off", dir); err != nil {
t.Fatalf("Processing of LocksmithEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "etc", "systemd", "system", "locksmithd.service")
target, err := os.Readlink(fullPath)
if err != nil {
t.Fatalf("Unable to read link %v", err)
}
if target != "/dev/null" {
t.Fatalf("Locksmith not masked, unit target %v", target)
}
}

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

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

53
initialize/oem_test.go Normal file
View File

@@ -0,0 +1,53 @@
package initialize
import (
"io/ioutil"
"os"
"path"
"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 os.RemoveAll(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)
}
}

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,49 @@
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)
}
}
}
func TestParseConfigCRLF(t *testing.T) {
contents := "#cloud-config\r\nhostname: foo\r\nssh_authorized_keys:\r\n - foobar\r\n"
ud, err := ParseUserData(contents)
if err != nil {
t.Fatalf("Failed parsing config: %v", err)
}
cfg := ud.(CloudConfig)
if cfg.Hostname != "foo" {
t.Error("Failed parsing hostname from config")
}
if len(cfg.SSHAuthorizedKeys) != 1 {
t.Error("Parsed incorrect number of SSH keys")
}
}

View File

@@ -11,10 +11,10 @@ import (
) )
type File struct { type File struct {
Encoding string Encoding string
Content string Content string
Owner string Owner string
Path string Path string
RawFilePermissions string `yaml:"permissions"` RawFilePermissions string `yaml:"permissions"`
} }
@@ -31,7 +31,6 @@ func (f *File) Permissions() (os.FileMode, error) {
return os.FileMode(perm), nil return os.FileMode(perm), nil
} }
func WriteFile(f *File) error { func WriteFile(f *File) error {
if f.Encoding != "" { if f.Encoding != "" {
return fmt.Errorf("Unable to write file with encoding %s", f.Encoding) return fmt.Errorf("Unable to write file with encoding %s", f.Encoding)

View File

@@ -19,8 +19,10 @@ const fakeMachineID = "42000000000000000000000000000042"
type Unit struct { type Unit struct {
Name string Name string
Enable bool
Runtime bool Runtime bool
Content string Content string
Command string
} }
func (u *Unit) Type() string { func (u *Unit) Type() string {
@@ -40,33 +42,40 @@ func (u *Unit) Group() (group string) {
type Script []byte 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" dir := "etc"
if u.Runtime { if u.Runtime {
dir = "run" dir = "run"
} }
dst := path.Join(root, dir, "systemd", u.Group()) return path.Join(root, dir, "systemd", u.Group(), u.Name)
if _, err := os.Stat(dst); os.IsNotExist(err) { }
if err := os.MkdirAll(dst, os.FileMode(0755)); err != nil {
return "", err // 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{ file := File{
Path: dst, Path: dst,
Content: u.Content, Content: u.Content,
RawFilePermissions: "0644", RawFilePermissions: "0644",
} }
err := WriteFile(&file) err := WriteFile(&file)
if err != nil { if err != nil {
return "", err return err
} }
return dst, nil return nil
} }
func EnableUnitFile(file string, runtime bool) error { func EnableUnitFile(file string, runtime bool) error {
@@ -80,34 +89,33 @@ func EnableUnitFile(file string, runtime bool) error {
return err return err
} }
func separateNetworkUnits(units []Unit) ([]Unit, []Unit) { func RunUnitCommand(command, unit string) (string, error) {
networkUnits := make([]Unit, 0) conn, err := dbus.New()
nonNetworkUnits := make([]Unit, 0) if err != nil {
for _, unit := range units { return "", err
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
}
} }
for _, unit := range nonNetworkUnits { var fn func(string, string) (string, error)
if err := RestartUnitByName(unit.Name); err != nil { switch command {
return err 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 { func DaemonReload() error {
@@ -116,31 +124,7 @@ func DaemonReload() error {
return err return err
} }
_, err = conn.Reload() return 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
} }
func ExecuteScript(scriptPath string) (string, error) { func ExecuteScript(scriptPath string) (string, error) {
@@ -181,3 +165,11 @@ func MachineID(root string) string {
return id return id
} }
func MaskUnit(unit string, root string) error {
masked := path.Join(root, "etc", "systemd", "system", unit)
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
return err
}
return os.Symlink("/dev/null", masked)
}

View File

@@ -4,15 +4,14 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"syscall"
"testing" "testing"
) )
func TestPlaceNetworkUnit(t *testing.T) { func TestPlaceNetworkUnit(t *testing.T) {
u := Unit{ u := Unit{
Name: "50-eth0.network", Name: "50-eth0.network",
Runtime: true, Runtime: true,
Content: `[Match] Content: `[Match]
Name=eth47 Name=eth47
[Network] [Network]
@@ -24,14 +23,19 @@ Address=10.209.171.177/19
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer syscall.Rmdir(dir) defer os.RemoveAll(dir)
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) t.Fatalf("PlaceUnit failed: %v", err)
} }
fullPath := path.Join(dir, "run", "systemd", "network", "50-eth0.network") fi, err := os.Stat(dst)
fi, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatalf("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
@@ -40,27 +44,27 @@ Address=10.209.171.177/19
t.Errorf("File has incorrect mode: %v", fi.Mode()) t.Errorf("File has incorrect mode: %v", fi.Mode())
} }
contents, err := ioutil.ReadFile(fullPath) contents, err := ioutil.ReadFile(dst)
if err != nil { if err != nil {
t.Fatalf("Unable to read expected file: %v", err) t.Fatalf("Unable to read expected file: %v", err)
} }
expect := `[Match] expectContents := `[Match]
Name=eth47 Name=eth47
[Network] [Network]
Address=10.209.171.177/19 Address=10.209.171.177/19
` `
if string(contents) != expect { if string(contents) != expectContents {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expect) t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
} }
} }
func TestPlaceMountUnit(t *testing.T) { func TestPlaceMountUnit(t *testing.T) {
u := Unit{ u := Unit{
Name: "media-state.mount", Name: "media-state.mount",
Runtime: false, Runtime: false,
Content: `[Mount] Content: `[Mount]
What=/dev/sdb1 What=/dev/sdb1
Where=/media/state Where=/media/state
`, `,
@@ -70,14 +74,19 @@ Where=/media/state
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer syscall.Rmdir(dir) defer os.RemoveAll(dir)
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) t.Fatalf("PlaceUnit failed: %v", err)
} }
fullPath := path.Join(dir, "etc", "systemd", "system", "media-state.mount") fi, err := os.Stat(dst)
fi, err := os.Stat(fullPath)
if err != nil { if err != nil {
t.Fatalf("Unable to stat file: %v", err) t.Fatalf("Unable to stat file: %v", err)
} }
@@ -86,17 +95,17 @@ Where=/media/state
t.Errorf("File has incorrect mode: %v", fi.Mode()) t.Errorf("File has incorrect mode: %v", fi.Mode())
} }
contents, err := ioutil.ReadFile(fullPath) contents, err := ioutil.ReadFile(dst)
if err != nil { if err != nil {
t.Fatalf("Unable to read expected file: %v", err) t.Fatalf("Unable to read expected file: %v", err)
} }
expect := `[Mount] expectContents := `[Mount]
What=/dev/sdb1 What=/dev/sdb1
Where=/media/state Where=/media/state
` `
if string(contents) != expect { if string(contents) != expectContents {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expect) t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
} }
} }
@@ -105,7 +114,7 @@ func TestMachineID(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Unable to create tempdir: %v", err) t.Fatalf("Unable to create tempdir: %v", err)
} }
defer syscall.Rmdir(dir) defer os.RemoveAll(dir)
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755)) os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007\n"), os.FileMode(0444)) ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007\n"), os.FileMode(0444))
@@ -114,3 +123,22 @@ func TestMachineID(t *testing.T) {
t.Fatalf("File has incorrect contents") t.Fatalf("File has incorrect contents")
} }
} }
func TestMaskUnit(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
if err := MaskUnit("foo.service", dir); err != nil {
t.Fatalf("Unable to mask unit: %v", err)
}
fullPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
target, err := os.Readlink(fullPath)
if err != nil {
t.Fatalf("Unable to read link", err)
}
if target != "/dev/null" {
t.Fatalf("unit not masked, got unit target", target)
}
}

View File

@@ -13,6 +13,7 @@ type User struct {
PasswordHash string `yaml:"passwd"` PasswordHash string `yaml:"passwd"`
SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"` SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"`
SSHImportGithubUser string `yaml:"coreos-ssh-import-github"` SSHImportGithubUser string `yaml:"coreos-ssh-import-github"`
SSHImportURL string `yaml:"coreos-ssh-import-url"`
GECOS string `yaml:"gecos"` GECOS string `yaml:"gecos"`
Homedir string `yaml:"homedir"` Homedir string `yaml:"homedir"`
NoCreateHome bool `yaml:"no-create-home"` NoCreateHome bool `yaml:"no-create-home"`
@@ -33,6 +34,8 @@ func CreateUser(u *User) error {
if u.PasswordHash != "" { if u.PasswordHash != "" {
args = append(args, "--password", u.PasswordHash) args = append(args, "--password", u.PasswordHash)
} else {
args = append(args, "--password", "*")
} }
if u.GECOS != "" { if u.GECOS != "" {
@@ -50,7 +53,7 @@ func CreateUser(u *User) error {
} }
if u.PrimaryGroup != "" { if u.PrimaryGroup != "" {
args = append(args, "--primary-group", u.PrimaryGroup) args = append(args, "--gid", u.PrimaryGroup)
} }
if len(u.Groups) > 0 { if len(u.Groups) > 0 {

39
test
View File

@@ -1,10 +1,37 @@
#!/bin/bash -e #!/bin/bash -e
#
# Run all coreos-cloudinit tests
# ./test
# ./test -v
#
# Run tests for one package
# PKG=initialize ./test
#
echo "Building bin/coreos-cloudinit" # Invoke ./cover for HTML output
. build COVER=${COVER:-"-cover"}
source ./build
declare -a TESTPKGS=(initialize system datasource)
if [ -z "$PKG" ]; then
GOFMTPATH="$TESTPKGS coreos-cloudinit.go"
# prepend repo path to each package
TESTPKGS=${TESTPKGS[@]/#/${REPO_PATH}/}
else
GOFMTPATH="$TESTPKGS"
# strip out slashes and dots from PKG=./foo/
TESTPKGS=${PKG//\//}
TESTPKGS=${TESTPKGS//./}
TESTPKGS=${TESTPKGS/#/${REPO_PATH}/}
fi
echo "Running tests..." echo "Running tests..."
for pkg in "./initialize ./system"; do go test -i ${TESTPKGS}
go test -i $pkg go test ${COVER} $@ ${TESTPKGS}
go test -v $pkg
done echo "Checking gofmt..."
fmtRes=$(gofmt -l $GOFMTPATH)
echo "Success"

View File

@@ -18,6 +18,8 @@ limitations under the License.
package dbus package dbus
import ( import (
"os"
"strconv"
"strings" "strings"
"sync" "sync"
@@ -73,7 +75,12 @@ func (c *Conn) initConnection() error {
return err 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 { if err != nil {
c.sysconn.Close() c.sysconn.Close()
return err return err

View File

@@ -35,6 +35,7 @@ func (c *Conn) jobComplete(signal *dbus.Signal) {
out, ok := c.jobListener.jobs[job] out, ok := c.jobListener.jobs[job]
if ok { if ok {
out <- result out <- result
delete(c.jobListener.jobs, job)
} }
c.jobListener.Unlock() 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() 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. // getProperties takes the unit name and returns all of its dbus object properties, for the given dbus interface
func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) { func (c *Conn) getProperties(unit string, dbusInterface string) (map[string]interface{}, error) {
var err error var err error
var props map[string]dbus.Variant 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) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -161,6 +162,55 @@ func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) {
return out, nil 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 // 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 // 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. // be more unit names loaded than actual units behind them.
@@ -253,8 +303,52 @@ type EnableUnitFileChange struct {
Destination string // Destination of the symlink 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 // Reload instructs systemd to scan for and reload unit files. This is
// equivalent to a 'systemctl daemon-reload'. // equivalent to a 'systemctl daemon-reload'.
func (c *Conn) Reload() (string, error) { func (c *Conn) Reload() error {
return c.runJob("org.freedesktop.systemd1.Manager.Reload") return c.sysobj.Call("org.freedesktop.systemd1.Manager.Reload", 0).Store()
} }

View File

@@ -18,9 +18,11 @@ package dbus
import ( import (
"fmt" "fmt"
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"testing" "testing"
) )
@@ -50,13 +52,16 @@ func setupUnit(target string, conn *Conn, t *testing.T) {
fixture := []string{abs} fixture := []string{abs}
install, changes, err := conn.EnableUnitFiles(fixture, true, true) install, changes, err := conn.EnableUnitFiles(fixture, true, true)
if err != nil {
t.Fatal(err)
}
if install != false { if install != false {
t.Fatal("Install was true") t.Fatal("Install was true")
} }
if len(changes) < 1 { 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 { 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 // TestGetUnitProperties reads the `-.mount` which should exist on all systemd
// systems and ensures that one of its properties is valid. // systems and ensures that one of its properties is valid.
func TestGetUnitProperties(t *testing.T) { func TestGetUnitProperties(t *testing.T) {
@@ -139,6 +175,20 @@ func TestGetUnitProperties(t *testing.T) {
if names[0] != "system.slice" { if names[0] != "system.slice" {
t.Fatal("unexpected wants for /") 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 // TestGetUnitPropertiesRejectsInvalidName attempts to get the properties for a
@@ -150,10 +200,37 @@ func TestGetUnitPropertiesRejectsInvalidName(t *testing.T) {
unit := "//invalid#$^/" unit := "//invalid#$^/"
_, err := conn.GetUnitProperties(unit) _, err := conn.GetUnitProperties(unit)
if err == nil { if err == nil {
t.Fatal("Expected an error, got 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. // 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") 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 { func PropRequiresMountsFor(units ...string) Property {
return propDependency("RequiresMountsFor", units) 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),
}
}

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 url defined in /proc/cmdline
Requires=coreos-setup-environment.service
After=coreos-setup-environment.service
Before=user-config.target
ConditionKernelCommandLine=cloud-config-url
[Service]
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=-/etc/environment
ExecStart=/usr/bin/coreos-cloudinit --from-proc-cmdline

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

View File

@@ -0,0 +1,24 @@
[Unit]
Description=Load cloud-config from %f
Requires=coreos-setup-environment.service
After=coreos-setup-environment.service
Before=user-config.target
ConditionFileNotEmpty=%f
# HACK: work around ordering between config drive and ec2 metadata It is
# possible for OpenStack style systems to provide both the metadata service
# and config drive, to prevent the two from stomping on eachother force
# this to run after OEM and after metadata (if it exsts). I'm doing this
# here instead of in the ec2 service because the ec2 unit is not written
# to disk until the OEM cloud config is evaluated and I want to make sure
# systemd knows about the ordering as early as possible.
# coreos-cloudinit could implement a simple lock but that cannot be used
# until after the systemd dbus calls are made non-blocking.
After=system-cloudinit@usr-share-oem-cloud\x2dconfig.yml.service
After=ec2-cloudinit.service
[Service]
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=-/etc/environment
ExecStart=/usr/bin/coreos-cloudinit --from-file=%f

11
units/user-config.target Normal file
View File

@@ -0,0 +1,11 @@
[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
Requires=user-cloudinit-proc-cmdline.service
After=user-cloudinit-proc-cmdline.service