Compare commits

...

120 Commits

Author SHA1 Message Date
Michael Marineau
24f181f7a3 chore(coreos-cloudinit): bump to 0.7.1 2014-05-16 21:21:47 -07:00
Michael Marineau
61e70fcce8 Merge pull request #119 from marineam/container
container and panic fixes
2014-05-16 21:19:43 -07:00
Michael Marineau
ea6262f0ae fix(etcd): fix runtime panic when etcd section is missing.
The etcd code tries to assign ee["name"] even when the map was never
defined and assigning to an uninitialized map causes a panic.
2014-05-16 20:38:49 -07:00
Michael Marineau
f83ce07416 feat(units): Add generic cloudinit path unit
Switch to triggering common user configs via a path unit. This is
particularly useful for config drive so that a config drive can be
mounted by something other than the udev triggered services, a bind
mount when running in a container for example.
2014-05-16 20:38:49 -07:00
Brandon Philips
140682350d chore(coreos-cloudinit): bump to 0.7.0+git 2014-05-16 18:22:22 -07:00
Brandon Philips
289ada4668 chore(coreos-cloudinit): bump to 0.7.0 2014-05-16 18:22:22 -07:00
Jonathan Boulle
d95df78c6d Merge pull request #117 from c4milo/travis-support
chore(travis): Adds travis yaml file as well as badge in README
2014-05-16 14:11:37 -07:00
Camilo Aguilar
ac4c969454 chore(travis): Adds travis yaml file and badge in README 2014-05-16 17:09:59 -04:00
Jonathan Boulle
04fcd3935f Merge pull request #114 from c4milo/fetch-url-refactor
refactor(datastore/fetch): Makes more failure proof fetching user-data files.
2014-05-16 14:03:54 -07:00
Camilo Aguilar
36efcc9d69 test(datastore/fetch): Makes sure err is not nil 2014-05-16 16:57:58 -04:00
Jonathan Boulle
f7ecc2461c Merge pull request #109 from jonboulle/fleet
fix(docs): add documentation for fleet section
2014-05-16 13:38:12 -07:00
Jonathan Boulle
8df9ee3ca2 Merge pull request #115 from burke/master
Response body must not be closed if request error'd.
2014-05-16 13:20:27 -07:00
Burke Libbey
321ceaa0da Response body must not be closed if request error'd. 2014-05-16 15:42:11 -04:00
Jonathan Boulle
05daad692e fix(docs): add documentation for fleet section 2014-05-16 12:10:21 -07:00
Camilo Aguilar
4b6fc63e8c fix(datastore/fetch): off-by-one oversight 2014-05-16 12:36:05 -04:00
Camilo Aguilar
fcccfb085f style(datastore/fetch): Adjusts comments formatting 2014-05-16 12:35:39 -04:00
Camilo Aguilar
ebf134f181 refactor(datastore/fetch): Makes more failure proof fetching user-data files
- Adds URL validations
- Adds timeout support for http client
- Limits the amount of retries to not spin forever
- Fails faster if response status code is 4xx
- Does a little bit more of logging
- Adds more tests
2014-05-16 12:35:06 -04:00
Jonathan Boulle
51d77516a5 Merge pull request #90 from jonboulle/90
Warn or error on unrecognized keys in cloud-config.yml
2014-05-15 18:53:48 -07:00
Jonathan Boulle
98f5ead730 fix(*): catch more unknown keys in user and file sections 2014-05-15 18:53:17 -07:00
Jonathan Boulle
81fe0dc9e0 fix(initialize): also check for unknown coreos keys 2014-05-15 18:53:17 -07:00
Jonathan Boulle
e852be65f7 feat(*): warn on encountering unrecognized keys in cloud-config 2014-05-15 18:53:17 -07:00
Brandon Philips
0a16532d4b Merge pull request #113 from c4milo/exponential_backoff
Exponential backoff with sleep capping
2014-05-15 10:16:42 -07:00
Camilo Aguilar
ff70a60fbc Adds sleep cap to exponential backoff so it does not go too high 2014-05-15 13:04:37 -04:00
Kelsey Hightower
31f61d7531 Use exponential backoff when fetching user-data from an URL.
The user-cloudinit-proc-cmdline systemd unit is responsible for
fetching user-data from various sources during the cloud-init
process. When fetching user-data from an URL datasource we face
a race condition since the network may not be available, which
can cause the job to fail and no further attempts to fetch the
user-data are made.

Eliminate the race condition when fetching user-data from an URL
datasource. Retry the fetch using an exponential backoff until
the user-data is retrieved.

Fixes issue 105.
2014-05-14 23:15:49 -07:00
Jonathan Boulle
b505e6241c Merge pull request #103 from jonboulle/20
feat(*): add more configuration options for update.conf
2014-05-14 13:14:35 -07:00
Jonathan Boulle
e413a97741 feat(update): add more configuration options for update.conf 2014-05-14 13:13:19 -07:00
Jonathan Boulle
41cbec8729 Merge pull request #101 from jonboulle/fleet
feat(*): add basic fleet configuration to cloud-config
2014-05-14 12:28:52 -07:00
Jonathan Boulle
919298e545 feat(fleet): add basic fleet configuration to cloud-config 2014-05-14 12:28:20 -07:00
Jonathan Boulle
ae424b5637 Merge pull request #106 from jonboulle/locksmith_to_update
refactor(init): rename locksmith to update
2014-05-14 11:50:09 -07:00
Jonathan Boulle
e93911344d refactor(init): rename locksmith to update 2014-05-14 11:40:39 -07:00
Jonathan Boulle
32c52d8729 Merge pull request #100 from jonboulle/rework
refactor(*): rework cloudconfig for better extensibility and consistency
2014-05-14 11:39:53 -07:00
Jonathan Boulle
cdee32d245 refactor(systemd): don't allow users to set DropIn=true yet 2014-05-14 11:34:13 -07:00
Jonathan Boulle
31cfad91e3 refactor(*): rework cloudconfig for better extensibility and consistency
This change creates a few simple interfaces for coreos-specific
configuration options and moves things to them wherever possible; so if
an option needs to write a file, or create a unit, it is acted on
exactly the same way as every other file/unit that needs to be written
during the cloud configuration process.
2014-05-14 11:34:07 -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
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 2391 additions and 382 deletions

1
.gitignore vendored
View File

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

8
.travis.yml Normal file
View File

@@ -0,0 +1,8 @@
language: go
go: 1.2
install:
- go get code.google.com/p/go.tools/cmd/cover
script:
- ./test

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,16 +1,45 @@
# Customize with Cloud-Config
# Using Cloud-Config
CoreOS allows you to configure networking, create users, launch systemd units on startup and more. We've designed our implementation to allow the same cloud-config file to work across all of our supported platforms.
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime.
Only a subset of [cloud-config functionality][cloud-config] is implemented. A set of custom parameters were added to the cloud-config format that are specific to CoreOS. An example file containing all available options can be found at the bottom of this page.
## Configuration File
The file used by this system initialization program is called a "cloud-config" file. It is inspired by the [cloud-init][cloud-init] project's [cloud-config][cloud-config] file. which is "the defacto multi-distribution package that handles early initialization of a cloud instance" ([cloud-init docs][cloud-init-docs]). Because the cloud-init project includes tools which aren't used by CoreOS, only the relevant subset of its configuration items will be implemented in our cloud-config file. In addition to those, we added a few CoreOS-specific items, such as etcd configuration, OEM definition, and systemd units.
We've designed our implementation to allow the same cloud-config file to work across all of our supported platforms.
[cloud-init]: https://launchpad.net/cloud-init
[cloud-init-docs]: http://cloudinit.readthedocs.org/en/latest/index.html
[cloud-config]: http://cloudinit.readthedocs.org/en/latest/topics/format.html#cloud-config-data
## CoreOS Parameters
### File Format
### coreos.etcd
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
The `coreos.etcd.*` options are translated to a partial systemd unit acting as an etcd configuration file.
A cloud-config file should contain an associative array which has zero or more of the following keys:
- `coreos`
- `ssh_authorized_keys`
- `hostname`
- `users`
- `write_files`
- `manage_etc_hosts`
The expected values for these keys are defined in the rest of this document.
[yaml]: https://en.wikipedia.org/wiki/YAML
### Providing Cloud-Config with Config-Drive
CoreOS tries to conform to each platform's native method to provide user data. Each cloud provider tends to be unique, but this complexity has been abstracted by CoreOS. You can view each platform's instructions on their documentation pages. The most universal way to provide cloud-config is [via config-drive](https://github.com/coreos/coreos-cloudinit/blob/master/Documentation/config-drive.md), which attaches a read-only device to the machine, that contains your cloud-config file.
## Configuration Parameters
### coreos
#### etcd
The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
We can use the templating feature of coreos-cloudinit to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
```
@@ -19,7 +48,9 @@ We can use the templating feature of coreos-cloudinit to automate etcd configura
coreos:
etcd:
name: node001
discovery: https://discovery.etcd.io/3445fa65423d8b04df07f59fb40218f8
# generate a new token for each unique cluster from https://discovery.etcd.io/new
discovery: https://discovery.etcd.io/<token>
# multi-region and multi-cloud deployments need to use $public_ipv4
addr: $public_ipv4:4001
peer-addr: $private_ipv4:7001
```
@@ -29,62 +60,63 @@ coreos:
```
[Service]
Environment="ETCD_NAME=node001"
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/3445fa65423d8b04df07f59fb40218f8"
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
Environment="ETCD_ADDR=203.0.113.29:4001"
Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
```
For more information about the available configuration options, see the [etcd documentation][etcd-config].
For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
[etcd-config]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md
### coreos.oem
#### fleet
These fields are borrowed from the [os-release spec][os-release] and repurposed
as a way for coreos-cloudinit to know about the OEM partition on this machine:
The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allow for the configuration of fleet through environment variables. For example, the following cloud-config document...
```
#cloud-config
- **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:
fleet:
public-ip: $public_ipv4
metadata: region=us-west
```
coreos-cloudinit renders these fields to `/etc/oem-release`.
If no **id** field is provided, coreos-cloudinit will ignore this section.
...will generate a systemd unit drop-in like this:
```
[Service]
Environment="FLEET_PUBLIC_IP=203.0.113.29"
Environment="FLEET_METADATA=region=us-west"
```
For example, the following cloud-config document...
For more information on fleet configuration, see the [fleet documentation][fleet-config].
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/configuration.md
#### update
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
- **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:
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
update:
reboot-strategy: etcd-lock
```
...would be rendered to the following `/etc/oem-release`:
#### units
```
ID="rackspace"
NAME="Rackspace Cloud Servers"
VERSION_ID="168.0.0"
HOME_URL="https://www.rackspace.com/cloud/servers/"
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
```
[os-release]: http://www.freedesktop.org/software/systemd/man/os-release.html
### coreos.units
Arbitrary systemd units may be provided in the `coreos.units` attribute.
`coreos.units` is a list of objects with the following fields:
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.
- **runtime**: Boolean indicating whether or not to persist the unit across reboots. This is analogous 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.
@@ -100,6 +132,7 @@ Write a unit to disk, automatically starting it.
coreos:
units:
- name: docker-redis.service
command: start
content: |
[Unit]
Description=Redis container
@@ -115,7 +148,7 @@ coreos:
WantedBy=local.target
```
Start the builtin `etcd` and `fleet` services:
Start the built-in `etcd` and `fleet` services:
```
# cloud-config
@@ -128,14 +161,12 @@ coreos:
command: start
```
## Cloud-Config Parameters
### ssh_authorized_keys
Provided public SSH keys will be authorized for the `core` user.
The `ssh_authorized_keys` parameter adds public SSH keys which will be authorized for the `core` user.
The keys will be named "coreos-cloudinit" by default.
Override this with the `--ssh-key-name` flag when calling `coreos-cloudinit`.
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
```
#cloud-config
@@ -146,7 +177,7 @@ ssh_authorized_keys:
### hostname
The provided value will be used to set the system's hostname.
The `hostname` parameter defines the system's hostname.
This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
```
@@ -157,8 +188,7 @@ hostname: coreos1
### users
Add or modify users with the `users` directive by providing a list of user objects, each consisting of the following fields.
Each field is optional and of type string unless otherwise noted.
The `users` parameter adds or modifies the specified list of users. Each user is an object which consists of the following fields. Each field is optional and of type string unless otherwise noted.
All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the user already exists.
- **name**: Required. Login name of user
@@ -171,6 +201,7 @@ All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the use
- **no-user-group**: Boolean. Skip default group creation.
- **ssh-authorized-keys**: List of public SSH keys to authorize for this user
- **coreos-ssh-import-github**: Authorize SSH keys from Github user
- **coreos-ssh-import-url**: Authorize SSH keys imported from a url endpoint.
- **system**: Create the user as a system user. No home directory will be created.
- **no-log-init**: Boolean. Skip initialization of lastlog and faillog databases.
@@ -189,7 +220,7 @@ users:
- name: elroy
passwd: $6$5s2u6/jR$un0AvWnqilcgaNB3Mkxd5yYv6mTlWfOoCYHZmfi3LDKVltj.E8XNKEcwWm...
groups:
- staff
- sudo
- docker
ssh-authorized-keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
@@ -215,12 +246,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.
#### Retrieving SSH Authorized Keys
##### From a GitHub User
Using the `coreos-ssh-import-github` field, we can import public SSH keys from a GitHub user to use as authorized keys to a server.
```
#cloud-config
users:
- name: elroy
coreos-ssh-import-github: elroy
```
##### From an HTTP Endpoint
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
```
#cloud-config
users:
- name: elroy
coreos-ssh-import-url: https://token:<OAUTH-TOKEN>@github-enterprise.example.com/users/elroy/keys
```
You can also specify any URL whose response matches the JSON format for public keys:
```
#cloud-config
users:
- name: elroy
coreos-ssh-import-url: https://example.com/public-keys
```
### write_files
Inject an arbitrary set of files to the local filesystem.
Provide a list of objects with the following attributes:
The `write-file` parameter defines a list of files to create on the local filesystem. Each file is represented as an associative array which has the following keys:
- **path**: Absolute location on disk where contents should be written
- **content**: Data to write at the provided `path`
- **permissions**: String representing file permissions in octal notation (i.e. '0644')
- **owner**: User and group that should own the file written to disk. This is equivalent to the `<user>:<group>` argument to `chown <user>:<group> <path>`.
Explicitly not implemented is the **encoding** attribute.
The **content** field must represent exactly what should be written to disk.
```
#cloud-config
write_files:
- 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,4 +1,4 @@
# coreos-cloudinit
# coreos-cloudinit [![Build Status](https://travis-ci.org/coreos/coreos-cloudinit.png?branch=master)](https://travis-ci.org/coreos/coreos-cloudinit)
coreos-cloudinit enables a user to customize CoreOS machines by providing either a cloud-config document or an executable script through user-data.

5
build
View File

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

View File

@@ -5,14 +5,13 @@ import (
"fmt"
"log"
"os"
"strings"
"github.com/coreos/coreos-cloudinit/datasource"
"github.com/coreos/coreos-cloudinit/initialize"
"github.com/coreos/coreos-cloudinit/system"
)
const version = "0.3.2"
const version = "0.7.1"
func main() {
var printVersion bool
@@ -27,6 +26,9 @@ func main() {
var url string
flag.StringVar(&url, "from-url", "", "Download user-data from provided url")
var useProcCmdline bool
flag.BoolVar(&useProcCmdline, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=<url>', using the cloud-config served by an HTTP GET to <url>", datasource.ProcCmdlineLocation, datasource.ProcCmdlineCloudConfigFlag))
var workspace string
flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
@@ -40,18 +42,15 @@ func main() {
os.Exit(0)
}
if file != "" && url != "" {
fmt.Println("Provide one of --from-file or --from-url")
os.Exit(1)
}
var ds datasource.Datasource
if file != "" {
ds = datasource.NewLocalFile(file)
} else if url != "" {
ds = datasource.NewMetadataService(url)
} else if useProcCmdline {
ds = datasource.NewProcCmdline()
} else {
fmt.Println("Provide one of --from-file or --from-url")
fmt.Println("Provide one of --from-file, --from-url or --from-proc-cmdline")
os.Exit(1)
}
@@ -76,7 +75,7 @@ func main() {
userdata := string(userdataBytes)
userdata = env.Apply(userdata)
parsed, err := ParseUserData(userdata)
parsed, err := initialize.ParseUserData(userdata)
if err != nil {
log.Printf("Failed parsing user-data: %v", err)
if ignoreFailure {
@@ -108,22 +107,3 @@ func main() {
log.Fatalf("Failed resolving user-data: %v", err)
}
}
func ParseUserData(contents string) (interface{}, error) {
header := strings.SplitN(contents, "\n", 2)[0]
if strings.HasPrefix(header, "#!") {
log.Printf("Parsing user-data as script")
return system.Script(contents), nil
} else if header == "#cloud-config" {
log.Printf("Parsing user-data as cloud-config")
cfg, err := initialize.NewCloudConfig(contents)
if err != nil {
log.Fatal(err.Error())
}
return *cfg, nil
} else {
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
}
}

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,104 @@
package datasource
import (
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"net"
"net/http"
neturl "net/url"
"strings"
"time"
)
const (
HTTP_2xx = 2
HTTP_4xx = 4
maxTimeout = time.Second * 5
maxRetries = 15
)
type Datasource interface {
Fetch() ([]byte, error)
Type() string
}
// HTTP client timeout
// This one is low since exponential backoff will kick off too.
var timeout = time.Duration(2) * time.Second
func dialTimeout(network, addr string) (net.Conn, error) {
deadline := time.Now().Add(timeout)
c, err := net.DialTimeout(network, addr, timeout)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
}
// Fetches user-data url with support for exponential backoff and maximum retries
func fetchURL(rawurl string) ([]byte, error) {
if rawurl == "" {
return nil, errors.New("user-data URL is empty. Skipping.")
}
url, err := neturl.Parse(rawurl)
if err != nil {
return nil, err
}
// Unfortunately, url.Parse is too generic to throw errors if a URL does not
// have a valid HTTP scheme. So, we have to do this extra validation
if !strings.HasPrefix(url.Scheme, "http") {
return nil, fmt.Errorf("user-data URL %s does not have a valid HTTP scheme. Skipping.", rawurl)
}
userdataURL := url.String()
// We need to create our own client in order to add timeout support.
// TODO(c4milo) Replace it once Go 1.3 is officially used by CoreOS
// More info: https://code.google.com/p/go/source/detail?r=ada6f2d5f99f
transport := &http.Transport{
Dial: dialTimeout,
}
client := &http.Client{
Transport: transport,
}
for retry := 1; retry <= maxRetries; retry++ {
log.Printf("Fetching user-data from %s. Attempt #%d", userdataURL, retry)
resp, err := client.Get(userdataURL)
if err == nil {
defer resp.Body.Close()
status := resp.StatusCode / 100
if status == HTTP_2xx {
return ioutil.ReadAll(resp.Body)
}
if status == HTTP_4xx {
return nil, fmt.Errorf("user-data not found. HTTP status code: %d", resp.StatusCode)
}
log.Printf("user-data not found. HTTP status code: %d", resp.StatusCode)
} else {
log.Printf("unable to fetch user-data: %s", err.Error())
}
duration := time.Millisecond * time.Duration((math.Pow(float64(2), float64(retry)) * 100))
if duration > maxTimeout {
duration = maxTimeout
}
time.Sleep(duration)
}
return nil, fmt.Errorf("unable to fetch user-data. Maximum retries reached: %d", maxRetries)
}

View File

@@ -0,0 +1,119 @@
package datasource
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
)
var expBackoffTests = []struct {
count int
body string
}{
{0, "number of attempts: 0"},
{1, "number of attempts: 1"},
{2, "number of attempts: 2"},
}
// Test exponential backoff and that it continues retrying if a 5xx response is
// received
func TestFetchURLExpBackOff(t *testing.T) {
for i, tt := range expBackoffTests {
mux := http.NewServeMux()
count := 0
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if count == tt.count {
io.WriteString(w, fmt.Sprintf("number of attempts: %d", count))
return
}
count++
http.Error(w, "", 500)
})
ts := httptest.NewServer(mux)
defer ts.Close()
data, err := fetchURL(ts.URL)
if err != nil {
t.Errorf("Test case %d produced error: %v", i, err)
}
if count != tt.count {
t.Errorf("Test case %d failed: %d != %d", i, count, tt.count)
}
if string(data) != tt.body {
t.Errorf("Test case %d failed: %s != %s", i, tt.body, data)
}
}
}
// Test that it stops retrying if a 4xx response comes back
func TestFetchURL4xx(t *testing.T) {
retries := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
retries++
http.Error(w, "", 404)
}))
defer ts.Close()
_, err := fetchURL(ts.URL)
if err == nil {
t.Errorf("Incorrect result\ngot: %s\nwant: %s", err.Error(), "user-data not found. HTTP status code: 404")
}
if retries > 1 {
t.Errorf("Number of retries:\n%d\nExpected number of retries:\n%s", retries, 1)
}
}
// Test that it fetches and returns user-data just fine
func TestFetchURL2xx(t *testing.T) {
var cloudcfg = `
#cloud-config
coreos:
oem:
id: test
name: CoreOS.box for Test
version-id: %VERSION_ID%+%BUILD_ID%
home-url: https://github.com/coreos/coreos-cloudinit
bug-report-url: https://github.com/coreos/coreos-cloudinit
update:
reboot-strategy: best-effort
`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, cloudcfg)
}))
defer ts.Close()
data, err := fetchURL(ts.URL)
if err != nil {
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, nil)
}
if string(data) != cloudcfg {
t.Errorf("Incorrect result\ngot: %s\nwant: %s", string(data), cloudcfg)
}
}
// Test attempt to fetching using malformed URL
func TestFetchURLMalformed(t *testing.T) {
var tests = []struct {
url string
want string
}{
{"boo", "user-data URL boo does not have a valid HTTP scheme. Skipping."},
{"mailto://boo", "user-data URL mailto://boo does not have a valid HTTP scheme. Skipping."},
{"ftp://boo", "user-data URL ftp://boo does not have a valid HTTP scheme. Skipping."},
{"", "user-data URL is empty. Skipping."},
}
for _, test := range tests {
_, err := fetchURL(test.url)
if err == nil || err.Error() != test.want {
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, test.want)
}
}
}

View File

@@ -1,36 +1,15 @@
package datasource
import (
"io/ioutil"
"net/http"
)
type metadataService struct {
url string
client http.Client
}
func NewMetadataService(url string) *metadataService {
return &metadataService{url, http.Client{}}
return &metadataService{url}
}
func (ms *metadataService) Fetch() ([]byte, error) {
resp, err := ms.client.Get(ms.url)
if err != nil {
return []byte{}, err
}
defer resp.Body.Close()
if resp.StatusCode / 100 != 2 {
return []byte{}, nil
}
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return respBytes, nil
return fetchURL(ms.url)
}
func (ms *metadataService) Type() string {

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package initialize
import (
"errors"
"fmt"
"log"
"path"
@@ -10,23 +11,123 @@ import (
"github.com/coreos/coreos-cloudinit/system"
)
// CloudConfigFile represents a CoreOS specific configuration option that can generate
// an associated system.File to be written to disk
type CloudConfigFile interface {
// File should either return (*system.File, error), or (nil, nil) if nothing
// needs to be done for this configuration option.
File(root string) (*system.File, error)
}
// CloudConfigUnit represents a CoreOS specific configuration option that can generate
// an associated system.Unit to be created/enabled appropriately
type CloudConfigUnit interface {
// Unit should either return (*system.Unit, error), or (nil, nil) if nothing
// needs to be done for this configuration option.
Unit(root string) (*system.Unit, error)
}
// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct {
Etcd EtcdEnvironment
Units []system.Unit
Fleet FleetEnvironment
OEM OEMRelease
Update UpdateConfig
Units []system.Unit
}
WriteFiles []system.File `yaml:"write_files"`
Hostname string
Users []system.User
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
}
type warner func(format string, v ...interface{})
// warnOnUnrecognizedKeys parses the contents of a cloud-config file and calls
// warn(msg, key) for every unrecognized key (i.e. those not present in CloudConfig)
func warnOnUnrecognizedKeys(contents string, warn warner) {
// Generate a map of all understood cloud config options
var cc map[string]interface{}
b, _ := goyaml.Marshal(&CloudConfig{})
goyaml.Unmarshal(b, &cc)
// Now unmarshal the entire provided contents
var c map[string]interface{}
goyaml.Unmarshal([]byte(contents), &c)
// Check that every key in the contents exists in the cloud config
for k, _ := range c {
if _, ok := cc[k]; !ok {
warn("Warning: unrecognized key %q in provided cloud config - ignoring section", k)
}
}
// Check for unrecognized coreos options, if any are set
coreos, ok := c["coreos"]
if ok {
set := coreos.(map[interface{}]interface{})
known := cc["coreos"].(map[interface{}]interface{})
for k, _ := range set {
key := k.(string)
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", key)
}
}
}
// Check for any badly-specified users, if any are set
users, ok := c["users"]
if ok {
var known map[string]interface{}
b, _ := goyaml.Marshal(&system.User{})
goyaml.Unmarshal(b, &known)
set := users.([]interface{})
for _, u := range set {
user := u.(map[interface{}]interface{})
for k, _ := range user {
key := k.(string)
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", key)
}
}
}
}
// Check for any badly-specified files, if any are set
files, ok := c["write_files"]
if ok {
var known map[string]interface{}
b, _ := goyaml.Marshal(&system.File{})
goyaml.Unmarshal(b, &known)
set := files.([]interface{})
for _, f := range set {
file := f.(map[interface{}]interface{})
for k, _ := range file {
key := k.(string)
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", key)
}
}
}
}
}
// NewCloudConfig instantiates a new CloudConfig from the given contents (a
// string of YAML), returning any error encountered. It will ignore unknown
// fields but log encountering them.
func NewCloudConfig(contents string) (*CloudConfig, error) {
var cfg CloudConfig
err := goyaml.Unmarshal([]byte(contents), &cfg)
if err != nil {
return &cfg, err
}
warnOnUnrecognizedKeys(contents, log.Printf)
return &cfg, nil
}
func (cc CloudConfig) String() string {
bytes, err := goyaml.Marshal(cc)
@@ -40,6 +141,9 @@ func (cc CloudConfig) String() string {
return stringified
}
// Apply renders a CloudConfig to an Environment. This can involve things like
// configuring the hostname, adding new users, writing various configuration
// files to disk, and manipulating systemd services.
func Apply(cfg CloudConfig, env *Environment) error {
if cfg.Hostname != "" {
if err := system.SetHostname(cfg.Hostname); err != nil {
@@ -48,14 +152,6 @@ func Apply(cfg CloudConfig, env *Environment) error {
log.Printf("Set hostname to %s", cfg.Hostname)
}
if cfg.Coreos.OEM.ID != "" {
if err := WriteOEMRelease(&cfg.Coreos.OEM, env.Root()); err != nil {
return err
}
log.Printf("Wrote /etc/oem-release to filesystem")
}
if len(cfg.Users) > 0 {
for _, user := range cfg.Users {
if user.Name == "" {
log.Printf("User object has no 'name' field, skipping")
@@ -91,6 +187,11 @@ func Apply(cfg CloudConfig, env *Environment) error {
return err
}
}
if user.SSHImportURL != "" {
log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL)
if err := SSHImportKeysFromURL(user.Name, user.SSHImportURL); err != nil {
return err
}
}
}
@@ -103,7 +204,26 @@ func Apply(cfg CloudConfig, env *Environment) error {
}
}
if len(cfg.WriteFiles) > 0 {
for _, ccf := range []CloudConfigFile{cfg.Coreos.OEM, cfg.Coreos.Update, cfg.ManageEtcHosts} {
f, err := ccf.File(env.Root())
if err != nil {
return err
}
if f != nil {
cfg.WriteFiles = append(cfg.WriteFiles, *f)
}
}
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
u, err := ccu.Unit(env.Root())
if err != nil {
return err
}
if u != nil {
cfg.Coreos.Units = append(cfg.Coreos.Units, *u)
}
}
for _, file := range cfg.WriteFiles {
file.Path = path.Join(env.Root(), file.Path)
if err := system.WriteFile(&file); err != nil {
@@ -111,28 +231,28 @@ func Apply(cfg CloudConfig, env *Environment) error {
}
log.Printf("Wrote file %s to filesystem", file.Path)
}
}
if len(cfg.Coreos.Etcd) > 0 {
if err := WriteEtcdEnvironment(cfg.Coreos.Etcd, env.Root()); err != nil {
log.Fatalf("Failed to write etcd config to filesystem: %v", err)
}
log.Printf("Wrote etcd config file to filesystem")
}
if len(cfg.Coreos.Units) > 0 {
commands := make(map[string]string, 0)
reload := false
for _, unit := range cfg.Coreos.Units {
dst := system.UnitDestination(&unit, env.Root())
if unit.Content != "" {
log.Printf("Writing unit %s to filesystem", unit.Name)
dst, err := system.PlaceUnit(&unit, env.Root())
if err != nil {
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
if err := system.PlaceUnit(&unit, dst); err != nil {
return err
}
log.Printf("Placed unit %s at %s", unit.Name, dst)
reload = true
}
if unit.Mask {
log.Printf("Masking unit file %s", unit.Name)
if err := system.MaskUnit(unit.Name, env.Root()); err != nil {
return err
}
}
if unit.Enable {
if unit.Group() != "network" {
log.Printf("Enabling unit file %s", dst)
if err := system.EnableUnitFile(dst, unit.Runtime); err != nil {
@@ -146,11 +266,15 @@ func Apply(cfg CloudConfig, env *Environment) error {
if unit.Group() == "network" {
commands["systemd-networkd.service"] = "restart"
} else {
if unit.Command != "" {
} else if unit.Command != "" {
commands[unit.Name] = unit.Command
}
}
if reload {
if err := system.DaemonReload(); err != nil {
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
}
}
for unit, command := range commands {
@@ -161,7 +285,6 @@ func Apply(cfg CloudConfig, env *Environment) error {
}
log.Printf("Result of '%s %s': %s", command, unit, res)
}
}
return nil
}

View File

@@ -1,10 +1,75 @@
package initialize
import (
"fmt"
"strings"
"testing"
)
func TestCloudConfigUnknownKeys(t *testing.T) {
contents := `
coreos:
etcd:
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
coreos_unknown:
foo: "bar"
section_unknown:
dunno:
something
bare_unknown:
bar
write_files:
- content: fun
path: /var/party
file_unknown: nofun
users:
- name: fry
passwd: somehash
user_unknown: philip
hostname:
foo
`
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("error instantiating CloudConfig with unknown keys: %v", err)
}
if cfg.Hostname != "foo" {
t.Fatalf("hostname not correctly set when invalid keys are present")
}
if len(cfg.Coreos.Etcd) < 1 {
t.Fatalf("etcd section not correctly set when invalid keys are present")
}
if len(cfg.WriteFiles) < 1 || cfg.WriteFiles[0].Content != "fun" || cfg.WriteFiles[0].Path != "/var/party" {
t.Fatalf("write_files section not correctly set when invalid keys are present")
}
if len(cfg.Users) < 1 || cfg.Users[0].Name != "fry" || cfg.Users[0].PasswordHash != "somehash" {
t.Fatalf("users section not correctly set when invalid keys are present")
}
var warnings string
catchWarn := func(f string, v ...interface{}) {
warnings += fmt.Sprintf(f, v...)
}
warnOnUnrecognizedKeys(contents, catchWarn)
if !strings.Contains(warnings, "coreos_unknown") {
t.Errorf("warnings did not catch unrecognized coreos option coreos_unknown")
}
if !strings.Contains(warnings, "bare_unknown") {
t.Errorf("warnings did not catch unrecognized key bare_unknown")
}
if !strings.Contains(warnings, "section_unknown") {
t.Errorf("warnings did not catch unrecognized key section_unknown")
}
if !strings.Contains(warnings, "user_unknown") {
t.Errorf("warnings did not catch unrecognized user key user_unknown")
}
if !strings.Contains(warnings, "file_unknown") {
t.Errorf("warnings did not catch unrecognized file key file_unknown")
}
}
// Assert that the parsing of a cloud config file "generally works"
func TestCloudConfigEmpty(t *testing.T) {
cfg, err := NewCloudConfig("")
@@ -32,6 +97,8 @@ func TestCloudConfig(t *testing.T) {
coreos:
etcd:
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
update:
reboot-strategy: reboot
units:
- name: 50-eth0.network
runtime: yes
@@ -129,6 +196,9 @@ Address=10.209.171.177/19
if cfg.Hostname != "trontastic" {
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
@@ -157,6 +227,26 @@ func TestCloudConfigSerializationHeader(t *testing.T) {
}
}
// TestDropInIgnored asserts that users are unable to set DropIn=True on units
func TestDropInIgnored(t *testing.T) {
contents := `
coreos:
units:
- name: test
dropin: true
`
cfg, err := NewCloudConfig(contents)
if err != nil || len(cfg.Coreos.Units) != 1 {
t.Fatalf("Encountered unexpected error: %v", err)
}
if len(cfg.Coreos.Units) != 1 || cfg.Coreos.Units[0].Name != "test" {
t.Fatalf("Expected 1 unit, but got %d: %v", len(cfg.Coreos.Units), cfg.Coreos.Units)
}
if cfg.Coreos.Units[0].DropIn {
t.Errorf("dropin option on unit in cloud-config was not ignored!")
}
}
func TestCloudConfigUsers(t *testing.T) {
contents := `
users:

View File

@@ -45,3 +45,16 @@ func (self *Environment) Apply(data string) string {
}
return data
}
// normalizeSvcEnv standardizes the keys of the map (environment variables for a service)
// by replacing any dashes with underscores and ensuring they are entirely upper case.
// For example, "some-env" --> "SOME_ENV"
func normalizeSvcEnv(m map[string]string) map[string]string {
out := make(map[string]string, len(m))
for key, val := range m {
key = strings.ToUpper(key)
key = strings.Replace(key, "-", "_", -1)
out[key] = val
}
return out
}

View File

@@ -3,26 +3,14 @@ package initialize
import (
"errors"
"fmt"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
type EtcdEnvironment map[string]string
func (ec EtcdEnvironment) normalized() map[string]string {
out := make(map[string]string, len(ec))
for key, val := range ec {
key = strings.ToUpper(key)
key = strings.Replace(key, "-", "_", -1)
out[key] = val
}
return out
}
func (ec EtcdEnvironment) String() (out string) {
norm := ec.normalized()
func (ee EtcdEnvironment) String() (out string) {
norm := normalizeSvcEnv(ee)
if val, ok := norm["DISCOVERY_URL"]; ok {
delete(norm, "DISCOVERY_URL")
@@ -40,23 +28,27 @@ func (ec EtcdEnvironment) String() (out string) {
return
}
// Write an EtcdEnvironment to the appropriate path on disk for etcd.service
func WriteEtcdEnvironment(env EtcdEnvironment, root string) error {
if _, ok := env["name"]; !ok {
// Unit creates a Unit file drop-in for etcd, using any configured
// options and adding a default MachineID if unset.
func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) {
if ee == nil {
return nil, nil
}
if _, ok := ee["name"]; !ok {
if machineID := system.MachineID(root); machineID != "" {
env["name"] = machineID
ee["name"] = machineID
} else if hostname, err := system.Hostname(); err == nil {
env["name"] = hostname
ee["name"] = hostname
} else {
return errors.New("Unable to determine default etcd name")
return nil, errors.New("Unable to determine default etcd name")
}
}
file := system.File{
Path: path.Join(root, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf"),
RawFilePermissions: "0644",
Content: env.String(),
}
return system.WriteFile(&file)
return &system.Unit{
Name: "etcd.service",
Runtime: true,
DropIn: true,
Content: ee.String(),
}, nil
}

View File

@@ -3,10 +3,10 @@ package initialize
import (
"io/ioutil"
"os"
"os/exec"
"path"
"syscall"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestEtcdEnvironment(t *testing.T) {
@@ -68,10 +68,20 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
defer os.RemoveAll(dir)
if err := WriteEtcdEnvironment(ec, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
u, err := ec.Unit(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
}
if u == nil {
t.Fatalf("Returned nil etcd unit unexpectedly")
}
dst := system.UnitDestination(u, dir)
os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
@@ -101,12 +111,12 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
}
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
ec := EtcdEnvironment{}
ee := EtcdEnvironment{}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
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))
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
@@ -114,8 +124,18 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
t.Fatalf("Failed writing out /etc/machine-id: %v", err)
}
if err := WriteEtcdEnvironment(ec, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
u, err := ee.Unit(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
}
if u == nil {
t.Fatalf("Returned nil etcd unit unexpectedly")
}
dst := system.UnitDestination(u, dir)
os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
@@ -133,7 +153,14 @@ Environment="ETCD_NAME=node007"
}
}
func rmdir(path string) error {
cmd := exec.Command("rm", "-rf", path)
return cmd.Run()
func TestEtcdEnvironmentWhenNil(t *testing.T) {
// EtcdEnvironment will be a nil map if it wasn't in the yaml
var ee EtcdEnvironment
if ee != nil {
t.Fatalf("EtcdEnvironment is not nil")
}
u, err := ee.Unit("")
if u != nil || err != nil {
t.Fatalf("Unit returned a non-nil value for nil input")
}
}

34
initialize/fleet.go Normal file
View File

@@ -0,0 +1,34 @@
package initialize
import (
"fmt"
"github.com/coreos/coreos-cloudinit/system"
)
type FleetEnvironment map[string]string
func (fe FleetEnvironment) String() (out string) {
norm := normalizeSvcEnv(fe)
out += "[Service]\n"
for key, val := range norm {
out += fmt.Sprintf("Environment=\"FLEET_%s=%s\"\n", key, val)
}
return
}
// Unit generates a Unit file drop-in for fleet, if any fleet options were
// configured in cloud-config
func (fe FleetEnvironment) Unit(root string) (*system.Unit, error) {
if len(fe) < 1 {
return nil, nil
}
return &system.Unit{
Name: "fleet.service",
Runtime: true,
DropIn: true,
Content: fe.String(),
}, nil
}

42
initialize/fleet_test.go Normal file
View File

@@ -0,0 +1,42 @@
package initialize
import "testing"
func TestFleetEnvironment(t *testing.T) {
cfg := make(FleetEnvironment, 0)
cfg["public-ip"] = "12.34.56.78"
env := cfg.String()
expect := `[Service]
Environment="FLEET_PUBLIC_IP=12.34.56.78"
`
if env != expect {
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
}
}
func TestFleetUnit(t *testing.T) {
cfg := make(FleetEnvironment, 0)
u, err := cfg.Unit("/")
if u != nil {
t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
}
cfg["public-ip"] = "12.34.56.78"
u, err = cfg.Unit("/")
if err != nil {
t.Errorf("error generating fleet unit: %v", err)
}
if u == nil {
t.Fatalf("unexpectedly got nil unit generating fleet unit!")
}
if !u.Runtime {
t.Errorf("bad Runtime for generated fleet unit!")
}
if !u.DropIn {
t.Errorf("bad DropIn for generated fleet unit!")
}
}

View File

@@ -1,52 +1,18 @@
package initialize
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/coreos/coreos-cloudinit/system"
)
type GithubUserKey struct {
Id int `json:"id"`
Key string `json:"key"`
}
func fetchGithubKeys(github_url string) ([]string, error) {
res, err := http.Get(github_url)
defer res.Body.Close()
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
var data []GithubUserKey
err = json.Unmarshal(body, &data)
if err != nil {
return nil, err
}
keys := make([]string, 0)
for _, key := range data {
keys = append(keys, key.Key)
}
return keys, err
}
func SSHImportGithubUser(system_user string, github_user string) error {
url := fmt.Sprintf("https://api.github.com/users/%s/keys", github_user)
keys, err := fetchGithubKeys(url)
keys, err := fetchUserKeys(url)
if err != nil {
return err
}
key_name := fmt.Sprintf("github-%s", github_user)
err = system.AuthorizeSSHKeys(system_user, key_name, keys)
if err != nil {
return err
}
return nil
return system.AuthorizeSSHKeys(system_user, key_name, keys)
}

View File

@@ -1,48 +1,9 @@
package initialize
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestCloudConfigUsersGithubMarshal(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gh_res := `
[
{
"id": 67057,
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ=="
},
{
"id": 3340477,
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBANxpzIbTzKTeBRaOIdUxwwGwvDasTfU/PonhbNIuhYjc+xFGvBRTumox2F+luVAKKs4WdvA4nJXaY1OFi6DZftk5Bp4E2JaSzp8ulAzHsMexDdv6LGHGEJj/qdHAL1vHk2K89PpwRFSRZI8XRBLjvkr4ZgBKLG5ZILXPJEPP2j3lAAAAFQCtxoTnV8wy0c4grcGrQ+1sCsD7WQAAAIAqZsW2GviMe1RQrbZT0xAZmI64XRPrnLsoLxycHWlS7r6uUln2c6Ae2MB/YF0d4Kd1XZii9GHj7rrypqEo7MW8uSabhu70nmu1J8m2O3Dsr+4oJLeat9vwPsJV92IKO0jQwjKnAOHOiB9JKGeCw+NfXfogbti9/q38Q6XcS+SI5wAAAIEA1803Y2h+tOOpZXAsNIwl9mRfExWzLQ3L7knwJdznQu/6SW1H/1oyoYLebuk187Qj2UFI5qQ6AZNc49DvohWx0Cg6ABcyubNyoaCjZKWIdxVnItHWNbLe//+tyTu0I2eQwJOORsEPK5gMpf599C7wXQ//DzZOWbTWiHEX52gCTmk="
},
{
"id": 5224438,
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O"
}
]
`
fmt.Fprintln(w, gh_res)
}))
defer ts.Close()
keys, err := fetchGithubKeys(ts.URL)
if err != nil {
t.Fatalf("Encountered unexpected error: %v", err)
}
expected := "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ=="
if keys[0] != expected {
t.Fatalf("expected %s, got %s", expected, keys[0])
}
expected = "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O"
if keys[2] != expected {
t.Fatalf("expected %s, got %s", expected, keys[2])
}
}
func TestCloudConfigUsersGithubUser(t *testing.T) {
contents := `

View File

@@ -0,0 +1,46 @@
package initialize
import (
"errors"
"fmt"
"os"
"path"
"github.com/coreos/coreos-cloudinit/system"
)
const DefaultIpv4Address = "127.0.0.1"
type EtcHosts string
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
if eh != "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
}
func (eh EtcHosts) File(root string) (*system.File, error) {
if eh == "" {
return nil, nil
}
etcHosts, err := eh.generateEtcHosts()
if err != nil {
return nil, err
}
return &system.File{
Path: path.Join("etc", "hosts"),
RawFilePermissions: "0644",
Content: etcHosts,
}, nil
}

View File

@@ -0,0 +1,85 @@
package initialize
import (
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
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) {
eh := EtcHosts("invalid")
if f, err := eh.File(""); err == nil || f != nil {
t.Fatalf("EtcHosts File succeeded with invalid value!")
}
}
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 os.RemoveAll(dir)
eh := EtcHosts("localhost")
f, err := eh.File(dir)
if err != nil {
t.Fatalf("Error calling File on EtcHosts: %v", err)
}
if f == nil {
t.Fatalf("manageEtcHosts returned nil file unexpectedly")
}
f.Path = path.Join(dir, f.Path)
if err := system.WriteFile(f); err != nil {
t.Fatalf("Error writing EtcHosts: %v", err)
}
fullPath := path.Join(dir, "etc", "hosts")
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
hostname, err := os.Hostname()
if err != nil {
t.Fatalf("Unable to read OS hostname: %v", err)
}
expect := fmt.Sprintf("%s %s\n", DefaultIpv4Address, hostname)
if string(contents) != expect {
t.Fatalf("File has incorrect contents")
}
}

View File

@@ -16,11 +16,11 @@ type OEMRelease struct {
BugReportURL string `yaml:"bug-report-url"`
}
func (oem *OEMRelease) String() string {
func (oem OEMRelease) String() string {
fields := []string{
fmt.Sprintf("ID=%q", oem.ID),
fmt.Sprintf("ID=%s", oem.ID),
fmt.Sprintf("VERSION_ID=%s", oem.VersionID),
fmt.Sprintf("NAME=%q", oem.Name),
fmt.Sprintf("VERSION_ID=%q", oem.VersionID),
fmt.Sprintf("HOME_URL=%q", oem.HomeURL),
fmt.Sprintf("BUG_REPORT_URL=%q", oem.BugReportURL),
}
@@ -28,12 +28,14 @@ func (oem *OEMRelease) String() string {
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(),
func (oem OEMRelease) File(root string) (*system.File, error) {
if oem.ID == "" {
return nil, nil
}
return system.WriteFile(&file)
return &system.File{
Path: path.Join("etc", "oem-release"),
RawFilePermissions: "0644",
Content: oem.String(),
}, nil
}

View File

@@ -4,8 +4,9 @@ import (
"io/ioutil"
"os"
"path"
"syscall"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestOEMReleaseWrittenToDisk(t *testing.T) {
@@ -20,10 +21,19 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
defer os.RemoveAll(dir)
if err := WriteOEMRelease(&oem, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
f, err := oem.File(dir)
if err != nil {
t.Fatalf("Processing of OEMRelease failed: %v", err)
}
if f == nil {
t.Fatalf("OEMRelease returned nil file unexpectedly")
}
f.Path = path.Join(dir, f.Path)
if err := system.WriteFile(f); err != nil {
t.Fatalf("Writing of OEMRelease failed: %v", err)
}
fullPath := path.Join(dir, "etc", "oem-release")
@@ -42,9 +52,9 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `ID="rackspace"
expect := `ID=rackspace
VERSION_ID=168.0.0
NAME="Rackspace Cloud Servers"
VERSION_ID="168.0.0"
HOME_URL="https://www.rackspace.com/cloud/servers/"
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
`

47
initialize/ssh_keys.go Normal file
View File

@@ -0,0 +1,47 @@
package initialize
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/coreos/coreos-cloudinit/system"
)
type UserKey struct {
ID int `json:"id,omitempty"`
Key string `json:"key"`
}
func SSHImportKeysFromURL(system_user string, url string) error {
keys, err := fetchUserKeys(url)
if err != nil {
return err
}
key_name := fmt.Sprintf("coreos-cloudinit-%s", system_user)
return system.AuthorizeSSHKeys(system_user, key_name, keys)
}
func fetchUserKeys(url string) ([]string, error) {
res, err := http.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
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)
}
}

151
initialize/update.go Normal file
View File

@@ -0,0 +1,151 @@
package initialize
import (
"bufio"
"errors"
"fmt"
"os"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
const (
locksmithUnit = "locksmithd.service"
)
// updateOption represents a configurable update option, which, if set, will be
// written into update.conf, replacing any existing value for the option
type updateOption struct {
key string // key used to configure this option in cloud-config
valid []string // valid values for the option
prefix string // prefix for the option in the update.conf file
value string // used to store the new value in update.conf (including prefix)
seen bool // whether the option has been seen in any existing update.conf
}
// updateOptions defines the update options understood by cloud-config.
// The keys represent the string used in cloud-config to configure the option.
var updateOptions = []*updateOption{
&updateOption{
key: "reboot-strategy",
prefix: "REBOOT_STRATEGY=",
valid: []string{"best-effort", "etcd-lock", "reboot", "off"},
},
&updateOption{
key: "group",
prefix: "GROUP=",
valid: []string{"master", "beta", "alpha", "stable"},
},
&updateOption{
key: "server",
prefix: "SERVER=",
},
}
// isValid checks whether a supplied value is valid for this option
func (uo updateOption) isValid(val string) bool {
if len(uo.valid) == 0 {
return true
}
for _, v := range uo.valid {
if val == v {
return true
}
}
return false
}
type UpdateConfig map[string]string
// File generates an `/etc/coreos/update.conf` file (if any update
// configuration options are set in cloud-config) by either rewriting the
// existing file on disk, or starting from `/usr/share/coreos/update.conf`
func (uc UpdateConfig) File(root string) (*system.File, error) {
if len(uc) < 1 {
return nil, nil
}
var out string
// Generate the list of possible substitutions to be performed based on the options that are configured
subs := make([]*updateOption, 0)
for _, uo := range updateOptions {
val, ok := uc[uo.key]
if !ok {
continue
}
if !uo.isValid(val) {
return nil, errors.New(fmt.Sprintf("invalid value %v for option %v (valid options: %v)", val, uo.key, uo.valid))
}
uo.value = uo.prefix + val
subs = append(subs, uo)
}
etcUpdate := path.Join(root, "etc", "coreos", "update.conf")
usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf")
conf, err := os.Open(etcUpdate)
if os.IsNotExist(err) {
conf, err = os.Open(usrUpdate)
}
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(conf)
for scanner.Scan() {
line := scanner.Text()
for _, s := range subs {
if strings.HasPrefix(line, s.prefix) {
line = s.value
s.seen = true
break
}
}
out += line
out += "\n"
if err := scanner.Err(); err != nil {
return nil, err
}
}
for _, s := range subs {
if !s.seen {
out += s.value
out += "\n"
}
}
return &system.File{
Path: path.Join("etc", "coreos", "update.conf"),
RawFilePermissions: "0644",
Content: out,
}, nil
}
// GetUnit generates a locksmith system.Unit, if reboot-strategy was set in
// cloud-config, for the cloud-init initializer to act on appropriately
func (uc UpdateConfig) Unit(root string) (*system.Unit, error) {
strategy, ok := uc["reboot-strategy"]
if !ok {
return nil, nil
}
u := &system.Unit{
Name: locksmithUnit,
Enable: true,
Command: "restart",
Mask: false,
}
if strategy == "off" {
u.Enable = false
u.Command = "stop"
u.Mask = true
}
return u, nil
}

217
initialize/update_test.go Normal file
View File

@@ -0,0 +1,217 @@
package initialize
import (
"io/ioutil"
"os"
"path"
"sort"
"strings"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
const (
base = `SERVER=https://example.com
GROUP=thegroupc`
configured = base + `
REBOOT_STRATEGY=awesome
`
expected = base + `
REBOOT_STRATEGY=etcd-lock
`
)
func setupFixtures(dir string) {
os.MkdirAll(path.Join(dir, "usr", "share", "coreos"), 0755)
os.MkdirAll(path.Join(dir, "run", "systemd", "system"), 0755)
ioutil.WriteFile(path.Join(dir, "usr", "share", "coreos", "update.conf"), []byte(base), 0644)
}
func TestEmptyUpdateConfig(t *testing.T) {
uc := &UpdateConfig{}
f, err := uc.File("")
if err != nil {
t.Error("unexpected error getting file from empty UpdateConfig")
}
if f != nil {
t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f)
}
u, err := uc.Unit("")
if err != nil {
t.Error("unexpected error getting unit from empty UpdateConfig")
}
if u != nil {
t.Errorf("getting unit from empty UpdateConfig should have returned nil, got %v", u)
}
}
func TestInvalidUpdateOptions(t *testing.T) {
uon := &updateOption{
key: "numbers",
prefix: "numero_",
valid: []string{"one", "two"},
}
uoa := &updateOption{
key: "any_will_do",
prefix: "any_",
}
if !uon.isValid("one") {
t.Error("update option did not accept valid option \"one\"")
}
if uon.isValid("three") {
t.Error("update option accepted invalid option \"three\"")
}
for _, s := range []string{"one", "asdf", "foobarbaz"} {
if !uoa.isValid(s) {
t.Errorf("update option with no \"valid\" field did not accept %q", s)
}
}
uc := &UpdateConfig{"reboot-strategy": "wizzlewazzle"}
f, err := uc.File("")
if err == nil {
t.Errorf("File did not give an error on invalid UpdateOption")
}
if f != nil {
t.Errorf("File did not return a nil file on invalid UpdateOption")
}
}
func TestServerGroupOptions(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
setupFixtures(dir)
u := &UpdateConfig{"group": "master", "server": "http://foo.com"}
want := `
GROUP=master
SERVER=http://foo.com`
f, err := u.File(dir)
if err != nil {
t.Errorf("unexpected error getting file from UpdateConfig: %v", err)
} else if f == nil {
t.Error("unexpectedly got empty file from UpdateConfig")
} else {
out := strings.Split(f.Content, "\n")
sort.Strings(out)
got := strings.Join(out, "\n")
if got != want {
t.Errorf("File has incorrect contents, got %v, want %v", got, want)
}
}
}
func TestRebootStrategies(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
setupFixtures(dir)
strategies := []struct {
name string
line string
uMask bool
uCommand string
}{
{"best-effort", "REBOOT_STRATEGY=best-effort", false, "restart"},
{"etcd-lock", "REBOOT_STRATEGY=etcd-lock", false, "restart"},
{"reboot", "REBOOT_STRATEGY=reboot", false, "restart"},
{"off", "REBOOT_STRATEGY=off", true, "stop"},
}
for _, s := range strategies {
uc := &UpdateConfig{"reboot-strategy": s.name}
f, err := uc.File(dir)
if err != nil {
t.Errorf("update failed to generate file for reboot-strategy=%v: %v", s.name, err)
} else if f == nil {
t.Errorf("generated empty file for reboot-strategy=%v", s.name)
} else {
seen := false
for _, line := range strings.Split(f.Content, "\n") {
if line == s.line {
seen = true
break
}
}
if !seen {
t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line)
}
}
u, err := uc.Unit(dir)
if err != nil {
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
} else if u == nil {
t.Errorf("generated empty unit for reboot-strategy=%v", s.name)
} else {
if u.Name != locksmithUnit {
t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name)
}
if u.Mask != s.uMask {
t.Errorf("unit generated for reboot strategy=%v had bad mask: %t", s.name, u.Mask)
}
if u.Command != s.uCommand {
t.Errorf("unit generated for reboot strategy=%v had bad command: %v", s.name, u.Command)
}
}
}
}
func TestUpdateConfWrittenToDisk(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
setupFixtures(dir)
for i := 0; i < 2; i++ {
if i == 1 {
err = ioutil.WriteFile(path.Join(dir, "etc", "coreos", "update.conf"), []byte(configured), 0644)
if err != nil {
t.Fatal(err)
}
}
uc := &UpdateConfig{"reboot-strategy": "etcd-lock"}
f, err := uc.File(dir)
if err != nil {
t.Fatalf("Processing UpdateConfig failed: %v", err)
} else if f == nil {
t.Fatal("Unexpectedly got nil updateconfig file")
}
f.Path = path.Join(dir, f.Path)
if err := system.WriteFile(f); err != nil {
t.Fatalf("Error writing update config: %v", err)
}
fullPath := path.Join(dir, "etc", "coreos", "update.conf")
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
if string(contents) != expected {
t.Fatalf("File has incorrect contents, got %v, wanted %v", string(contents), expected)
}
}
}

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

@@ -31,7 +31,6 @@ func (f *File) Permissions() (os.FileMode, error) {
return os.FileMode(perm), nil
}
func WriteFile(f *File) error {
if f.Encoding != "" {
return fmt.Errorf("Unable to write file with encoding %s", f.Encoding)

View File

@@ -17,11 +17,21 @@ import (
// never be used as a true MachineID
const fakeMachineID = "42000000000000000000000000000042"
// Name for drop-in service configuration files created by cloudconfig
const cloudConfigDropIn = "20-cloudinit.conf"
type Unit struct {
Name string
Mask bool
Enable bool
Runtime bool
Content string
Command string
// For drop-in units, a cloudinit.conf is generated.
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
// until the correct behaviour for multiple drop-in units is determined.
DropIn bool `yaml:"-"`
}
func (u *Unit) Type() string {
@@ -41,20 +51,31 @@ func (u *Unit) Group() (group string) {
type Script []byte
func PlaceUnit(u *Unit, root string) (string, error) {
// UnitDestination builds the appropriate absolute file path for
// the given Unit. The root argument indicates the effective base
// directory of the system (similar to a chroot).
func UnitDestination(u *Unit, root string) string {
dir := "etc"
if u.Runtime {
dir = "run"
}
dst := path.Join(root, dir, "systemd", u.Group())
if _, err := os.Stat(dst); os.IsNotExist(err) {
if err := os.MkdirAll(dst, os.FileMode(0755)); err != nil {
return "", err
if u.DropIn {
return path.Join(root, dir, "systemd", u.Group(), fmt.Sprintf("%s.d", u.Name), cloudConfigDropIn)
} else {
return path.Join(root, dir, "systemd", u.Group(), u.Name)
}
}
dst = path.Join(dst, u.Name)
// PlaceUnit writes a unit file at the provided destination, creating
// parent directories as necessary.
func PlaceUnit(u *Unit, dst string) error {
dir := filepath.Dir(dst)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
return err
}
}
file := File{
Path: dst,
@@ -64,10 +85,10 @@ func PlaceUnit(u *Unit, root string) (string, error) {
err := WriteFile(&file)
if err != nil {
return "", err
return err
}
return dst, nil
return nil
}
func EnableUnitFile(file string, runtime bool) error {
@@ -157,3 +178,11 @@ func MachineID(root string) string {
return id
}
func MaskUnit(unit string, root string) error {
masked := path.Join(root, "etc", "systemd", "system", unit)
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
return err
}
return os.Symlink("/dev/null", masked)
}

View File

@@ -4,7 +4,6 @@ import (
"io/ioutil"
"os"
"path"
"syscall"
"testing"
)
@@ -24,14 +23,19 @@ Address=10.209.171.177/19
if err != nil {
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)
}
fullPath := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
fi, err := os.Stat(fullPath)
fi, err := os.Stat(dst)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
@@ -40,19 +44,43 @@ Address=10.209.171.177/19
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
contents, err := ioutil.ReadFile(dst)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `[Match]
expectContents := `[Match]
Name=eth47
[Network]
Address=10.209.171.177/19
`
if string(contents) != expect {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expect)
if string(contents) != expectContents {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
}
}
func TestUnitDestination(t *testing.T) {
dir := "/some/dir"
name := "foobar.service"
u := Unit{
Name: name,
DropIn: false,
}
dst := UnitDestination(&u, dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst)
}
u.DropIn = true
dst = UnitDestination(&u, dir)
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst)
}
}
@@ -70,14 +98,19 @@ Where=/media/state
if err != nil {
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)
}
fullPath := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
fi, err := os.Stat(fullPath)
fi, err := os.Stat(dst)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
@@ -86,17 +119,17 @@ Where=/media/state
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
contents, err := ioutil.ReadFile(dst)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `[Mount]
expectContents := `[Mount]
What=/dev/sdb1
Where=/media/state
`
if string(contents) != expect {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expect)
if string(contents) != expectContents {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
}
}
@@ -105,7 +138,7 @@ func TestMachineID(t *testing.T) {
if err != nil {
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))
ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007\n"), os.FileMode(0444))
@@ -114,3 +147,23 @@ func TestMachineID(t *testing.T) {
t.Fatalf("File has incorrect contents")
}
}
func TestMaskUnit(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
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"`
SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"`
SSHImportGithubUser string `yaml:"coreos-ssh-import-github"`
SSHImportURL string `yaml:"coreos-ssh-import-url"`
GECOS string `yaml:"gecos"`
Homedir string `yaml:"homedir"`
NoCreateHome bool `yaml:"no-create-home"`
@@ -33,6 +34,8 @@ func CreateUser(u *User) error {
if u.PasswordHash != "" {
args = append(args, "--password", u.PasswordHash)
} else {
args = append(args, "--password", "*")
}
if u.GECOS != "" {
@@ -50,7 +53,7 @@ func CreateUser(u *User) error {
}
if u.PrimaryGroup != "" {
args = append(args, "--primary-group", u.PrimaryGroup)
args = append(args, "--gid", u.PrimaryGroup)
}
if len(u.Groups) > 0 {

39
test
View File

@@ -1,10 +1,37 @@
#!/bin/bash -e
#
# Run all coreos-cloudinit tests
# ./test
# ./test -v
#
# Run tests for one package
# PKG=initialize ./test
#
echo "Building bin/coreos-cloudinit"
. build
# Invoke ./cover for HTML output
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..."
for pkg in "./initialize ./system"; do
go test -i $pkg
go test -v $pkg
done
go test -i ${TESTPKGS}
go test ${COVER} $@ ${TESTPKGS}
echo "Checking gofmt..."
fmtRes=$(gofmt -l $GOFMTPATH)
echo "Success"

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,11 @@
[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
[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,14 @@
[Unit]
Description=Mount config drive from virtfs
Conflicts=configdrive-block.service umount.target
ConditionPathIsMountPoint=!/media/configdrive
ConditionVirtualization=vm
# 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,5 @@
[Unit]
Description=Watch for a cloud-config at %f
[Path]
PathExists=%f

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

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

@@ -0,0 +1,13 @@
[Unit]
Description=Load user-provided cloud configs
Requires=system-config.target
After=system-config.target
# Watch for configs at a couple common paths
Requires=user-cloudinit@media-configdrive-openstack-latest-user_data.path
After=user-cloudinit@media-configdrive-openstack-latest-user_data.path
Requires=user-cloudinit@var-lib-coreos\x2dinstall-user_data.path
After=user-cloudinit@var-lib-coreos\x2dinstall-user_data.path
Requires=user-cloudinit-proc-cmdline.service
After=user-cloudinit-proc-cmdline.service