Compare commits

..

1 Commits

Author SHA1 Message Date
Brandon Philips
68202f3c06 feat(Documentation/cloud-config): add section about oem-release
We need to have cloud-config render a file about itself that can be
easily parsed and used by the rest of the system. Essentially just copy
the os-release format.
2014-03-16 16:47:44 -07:00
85 changed files with 932 additions and 6732 deletions

3
.gitignore vendored
View File

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

View File

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

View File

@@ -1,87 +0,0 @@
# 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
View File

@@ -1,36 +0,0 @@
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

@@ -1,37 +0,0 @@
## 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,216 +1,38 @@
# Using Cloud-Config
# Customize CoreOS with Cloud-Config
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.
CoreOS allows you to configure machine parameters, launch systemd units on startup and more. 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.
## 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
### File Format
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
A cloud-config file should contain an associative array which has zero or more of the following keys:
- `coreos`
- `ssh_authorized_keys`
- `hostname`
- `users`
- `write_files`
- `manage_etc_hosts`
The expected values for these keys are defined in the rest of this document.
[yaml]: https://en.wikipedia.org/wiki/YAML
### Providing Cloud-Config with Config-Drive
CoreOS tries to conform to each platform's native method to provide user data. Each cloud provider tends to be unique, but this complexity has been abstracted by CoreOS. You can view each platform's instructions on their documentation pages. The most universal way to provide cloud-config is [via config-drive](https://github.com/coreos/coreos-cloudinit/blob/master/Documentation/config-drive.md), which attaches a read-only device to the machine, that contains your cloud-config file.
## Configuration Parameters
### coreos
#### etcd
The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
We can use the templating feature of coreos-cloudinit to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
```
#cloud-config
coreos:
etcd:
name: node001
# generate a new token for each unique cluster from https://discovery.etcd.io/new
discovery: https://discovery.etcd.io/<token>
# multi-region and multi-cloud deployments need to use $public_ipv4
addr: $public_ipv4:4001
peer-addr: $private_ipv4:7001
```
...will generate a systemd unit drop-in like this:
```
[Service]
Environment="ETCD_NAME=node001"
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
Environment="ETCD_ADDR=203.0.113.29:4001"
Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
```
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
#### fleet
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
coreos:
fleet:
public-ip: $public_ipv4
metadata: region=us-west
```
...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 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.
These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field.
The `reboot-strategy` parameter also affects the behaviour of [locksmith](https://github.com/coreos/locksmith).
- **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).
- **server**: is the omaha endpoint URL which will be queried for updates.
- **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable")
*Note: cloudinit will only manipulate the locksmith unit file in the systemd runtime directory (`/run/systemd/system/locksmithd.service`). If any manual modifications are made to an overriding unit configuration file (e.g. `/etc/systemd/system/locksmithd.service`), cloudinit will no longer be able to control the locksmith service unit.*
##### Example
```
#cloud-config
coreos:
update:
reboot-strategy: etcd-lock
```
#### units
The `coreos.units.*` parameters define a list of arbitrary systemd units to start. Each item is an object with the following fields:
- **name**: String representing unit's name. Required.
- **runtime**: Boolean indicating whether or not to persist the unit across reboots. This is analogous to the `--runtime` argument to `systemctl 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.
- **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. Default value is false.
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
##### Examples
Write a unit to disk, automatically starting it.
```
#cloud-config
coreos:
units:
- name: docker-redis.service
command: start
content: |
[Unit]
Description=Redis container
Author=Me
After=docker.service
[Service]
Restart=always
ExecStart=/usr/bin/docker start -a redis_server
ExecStop=/usr/bin/docker stop -t 2 redis_server
```
Start the built-in `etcd` and `fleet` services:
```
#cloud-config
coreos:
units:
- name: etcd.service
command: start
- name: fleet.service
command: start
```
## Supported cloud-config Parameters
### ssh_authorized_keys
The `ssh_authorized_keys` parameter adds public SSH keys which will be authorized for the `core` user.
Provided public SSH keys will be authorized for the `core` user.
The keys will be named "coreos-cloudinit" by default.
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
```
#cloud-config
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
```
Override this with the `--ssh-key-name` flag when calling `coreos-cloudinit`.
### hostname
The `hostname` parameter defines the system's hostname.
The provided value will be used to set the system's hostname.
This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
```
#cloud-config
hostname: coreos1
```
### users
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.
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.
All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the user already exists.
- **name**: Required. Login name of user
- **gecos**: GECOS comment of user
- **passwd**: Hash of the password to use for this user
- **homedir**: User's home directory. Defaults to /home/<name>
- **no-create-home**: Boolean. Skip home directory creation.
- **no-create-home**: Boolean. Skip home directory createion.
- **primary-group**: Default group for the user. Defaults to a new group created named after the user.
- **groups**: Add user to these additional groups
- **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.
@@ -222,107 +44,153 @@ The following fields are not yet implemented:
- **selinux-user**: Corresponding SELinux user
- **ssh-import-id**: Import SSH keys by ID from Launchpad.
```
#cloud-config
##### Generating a password hash
users:
- name: elroy
passwd: $6$5s2u6/jR$un0AvWnqilcgaNB3Mkxd5yYv6mTlWfOoCYHZmfi3LDKVltj.E8XNKEcwWm...
groups:
- sudo
- docker
ssh-authorized-keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
```
Generating a safe hash is important to the security of your system. Currently with updated tools like [oclhashcat](http://hashcat.net/oclhashcat/) simplified hashes like md5crypt are trivial to crack on modern GPU hardware. You can generate a "safer" hash (read: not safe, never publish your hashes publicly) via:
#### Generating a password hash
###### On Debian/Ubuntu (via the package "whois")
mkpasswd --method=SHA-512 --rounds=4096
If you choose to use a password instead of an SSH key, generating a safe hash is extremely important to the security of your system. Simplified hashes like md5crypt are trivial to crack on modern GPU hardware. Here are a few ways to generate secure hashes:
###### With OpenSSL (note: this will only make md5crypt. While better than plantext it should not be considered fully secure)
openssl passwd -1
```
# On Debian/Ubuntu (via the package "whois")
mkpasswd --method=SHA-512 --rounds=4096
###### With Python (change password and salt values)
python -c "import crypt, getpass, pwd; print crypt.crypt('password', '\$6\$SALT\$')"
# OpenSSL (note: this will only make md5crypt. While better than plantext it should not be considered fully secure)
openssl passwd -1
# Python (change password and salt values)
python -c "import crypt, getpass, pwd; print crypt.crypt('password', '\$6\$SALT\$')"
# Perl (change password and salt values)
perl -e 'print crypt("password","\$6\$SALT\$") . "\n"'
```
###### With Perl (change password and salt values)
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://github-enterprise.example.com/api/v3/users/elroy/keys?access_token=<TOKEN>
```
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
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:
Inject an arbitrary set of files to the local filesystem.
Provide a list of objects with the following attributes:
- **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.
## Custom cloud-config Parameters
### coreos.oem
These fields are borrowed from the [os-release spec][os-release] and repurposed
as a way for cloud-init to know about the OEM partition on this machine.
- **id**: A lower case string identifying the oem.
- **version-id**: A lower case string identifying the version of the OEM. Example: `168.0.0`
- **name**: A name without the version that is suitable for presentation to the user.
- **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 partition.
cloudinit must render these fields down to an /etc/oem-release file on disk in the following format:
```
NAME=Rackspace
ID=rackspace
VERSION_ID=168.0.0
PRETTY_NAME="Rackspace Cloud Servers"
HOME_URL="http://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.etcd.discovery_url
The value of `coreos.etcd.discovery_url` will be used to discover the instance's etcd peers using the [etcd discovery protocol][disco-proto]. Usage of the [public discovery service][disco-service] is encouraged.
[disco-proto]: https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md
[disco-service]: http://discovery.etcd.io
### coreos.units
Arbitrary systemd units may be provided in the `coreos.units` attribute.
`coreos.units` is a list of objects with the following fields:
- **name**: string representing unit's name
- **runtime**: boolean indicating whether or not to persist the unit across reboots. This is analagous to the `--runtime` flag to `systemd enable`.
- **content**: plaintext string representing entire unit file
See docker example below.
## user-data Script
Simply set your user-data to a script where the first line is a shebang:
```
#!/bin/bash
echo 'Hello, world!'
```
## Examples
### Inject an SSH key, bootstrap etcd, and start fleet
```
#cloud-config
coreos:
etcd:
discovery_url: https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877
fleet:
autostart: yes
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
```
### Start a docker container on boot
```
#cloud-config
coreos:
units:
- name: docker-redis.service
content: |
[Unit]
Description=Redis container
Author=Me
After=docker.service
[Service]
Restart=always
ExecStart=/usr/bin/docker start -a redis_server
ExecStop=/usr/bin/docker stop -t 2 redis_server
[Install]
WantedBy=local.target
```
### Add a user
```
#cloud-config
users:
- name: elroy
passwd: $6$5s2u6/jR$un0AvWnqilcgaNB3Mkxd5yYv6mTlWfOoCYHZmfi3LDKVltj.E8XNKEcwWm...
groups:
- staff
- docker
ssh-authorized-keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
```
### Inject configuration files
```
#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
- path: /etc/hosts
contents: |
127.0.0.1 localhost
192.0.2.211 buildbox
- path: /etc/resolv.conf
contents: |
nameserver 192.0.2.13
nameserver 192.0.2.14
```

View File

@@ -1,30 +0,0 @@
# 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
View File

@@ -1,202 +0,0 @@
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
View File

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

View File

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

12
build
View File

@@ -1,14 +1,6 @@
#!/bin/bash -e
ORG_PATH="github.com/coreos"
REPO_PATH="${ORG_PATH}/coreos-cloudinit"
if [ ! -h gopath/src/${REPO_PATH} ]; then
mkdir -p gopath/src/${ORG_PATH}
ln -s ../../../.. gopath/src/${REPO_PATH} || exit 255
fi
export GOBIN=${PWD}/bin
export GOPATH=${PWD}/gopath
export GOPATH=${PWD}
go build -o bin/coreos-cloudinit ${REPO_PATH}
go build -o bin/coreos-cloudinit github.com/coreos/coreos-cloudinit

143
cloudinit/cloud_config.go Normal file
View File

@@ -0,0 +1,143 @@
package cloudinit
import (
"fmt"
"log"
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
)
const DefaultSSHKeyName = "coreos-cloudinit"
type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct {
Etcd struct{ Discovery_URL string }
Fleet struct{ Autostart bool }
Units []Unit
}
WriteFiles []WriteFile `yaml:"write_files"`
Hostname string
Users []User
}
func NewCloudConfig(contents []byte) (*CloudConfig, error) {
var cfg CloudConfig
err := goyaml.Unmarshal(contents, &cfg)
return &cfg, err
}
func (cc CloudConfig) String() string {
bytes, err := goyaml.Marshal(cc)
if err != nil {
return ""
}
stringified := string(bytes)
stringified = fmt.Sprintf("#cloud-config\n%s", stringified)
return stringified
}
func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
if cfg.Hostname != "" {
if err := SetHostname(cfg.Hostname); err != nil {
return err
}
log.Printf("Set hostname to %s", cfg.Hostname)
}
if len(cfg.Users) > 0 {
for _, user := range cfg.Users {
if user.Name == "" {
log.Printf("User object has no 'name' field, skipping")
continue
}
if UserExists(&user) {
log.Printf("User '%s' exists, ignoring creation-time fields", user.Name)
if user.PasswordHash != "" {
log.Printf("Setting '%s' user's password", user.Name)
if err := SetUserPassword(user.Name, user.PasswordHash); err != nil {
log.Printf("Failed setting '%s' user's password: %v", user.Name, err)
return err
}
}
} else {
log.Printf("Creating user '%s'", user.Name)
if err := CreateUser(&user); err != nil {
log.Printf("Failed creating user '%s': %v", user.Name, err)
return err
}
}
if len(user.SSHAuthorizedKeys) > 0 {
log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name)
if err := AuthorizeSSHKeys(user.Name, sshKeyName, user.SSHAuthorizedKeys); err != nil {
return err
}
}
}
}
if len(cfg.SSHAuthorizedKeys) > 0 {
err := AuthorizeSSHKeys("core", sshKeyName, cfg.SSHAuthorizedKeys)
if err == nil {
log.Printf("Authorized SSH keys for core user")
} else {
return err
}
}
if len(cfg.WriteFiles) > 0 {
for _, file := range cfg.WriteFiles {
if err := ProcessWriteFile("/", &file); err != nil {
return err
}
log.Printf("Wrote file %s to filesystem", file.Path)
}
}
if cfg.Coreos.Etcd.Discovery_URL != "" {
err := PersistEtcdDiscoveryURL(cfg.Coreos.Etcd.Discovery_URL)
if err == nil {
log.Printf("Consumed etcd discovery url")
} else {
log.Fatalf("Failed to persist etcd discovery url to filesystem: %v", err)
}
}
if len(cfg.Coreos.Units) > 0 {
for _, unit := range cfg.Coreos.Units {
log.Printf("Placing unit %s on filesystem", unit.Name)
dst, err := PlaceUnit("/", &unit)
if err != nil {
return err
}
log.Printf("Placed unit %s at %s", unit.Name, dst)
if unit.Group() != "network" {
log.Printf("Enabling unit file %s", dst)
if err := EnableUnitFile(dst, unit.Runtime); err != nil {
return err
}
log.Printf("Enabled unit %s", unit.Name)
} else {
log.Printf("Skipping enable for network-like unit %s", unit.Name)
}
}
DaemonReload()
StartUnits(cfg.Coreos.Units)
}
if cfg.Coreos.Fleet.Autostart {
err := StartUnitByName("fleet.service")
if err == nil {
log.Printf("Started fleet service.")
} else {
return err
}
}
return nil
}

View File

@@ -1,78 +1,13 @@
package initialize
package cloudinit
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("")
cfg, err := NewCloudConfig([]byte{})
if err != nil {
t.Fatalf("Encountered unexpected error :%v", err)
}
@@ -82,6 +17,14 @@ func TestCloudConfigEmpty(t *testing.T) {
t.Error("Parsed incorrect number of SSH keys")
}
if cfg.Coreos.Etcd.Discovery_URL != "" {
t.Error("Parsed incorrect value of discovery url")
}
if cfg.Coreos.Fleet.Autostart {
t.Error("Expected AutostartFleet not to be defined")
}
if len(cfg.WriteFiles) != 0 {
t.Error("Expected zero WriteFiles")
}
@@ -93,12 +36,12 @@ func TestCloudConfigEmpty(t *testing.T) {
// Assert that the parsing of a cloud config file "generally works"
func TestCloudConfig(t *testing.T) {
contents := `
contents := []byte(`
coreos:
etcd:
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
update:
reboot-strategy: reboot
discovery_url: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
fleet:
autostart: Yes
units:
- name: 50-eth0.network
runtime: yes
@@ -112,12 +55,6 @@ coreos:
Address=10.209.171.177/19
'
oem:
id: rackspace
name: Rackspace Cloud Servers
version-id: 168.0.0
home-url: https://www.rackspace.com/cloud/servers/
bug-report-url: https://github.com/coreos/coreos-overlay
ssh_authorized_keys:
- foobar
- foobaz
@@ -129,7 +66,7 @@ write_files:
permissions: '0644'
owner: root:dogepack
hostname: trontastic
`
`)
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error :%v", err)
@@ -144,6 +81,14 @@ hostname: trontastic
t.Error("Expected first SSH key to be 'foobaz'")
}
if cfg.Coreos.Etcd.Discovery_URL != "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" {
t.Error("Failed to parse etcd discovery url")
}
if !cfg.Coreos.Fleet.Autostart {
t.Error("Expected AutostartFleet to be true")
}
if len(cfg.WriteFiles) != 1 {
t.Error("Failed to parse correct number of write_files")
} else {
@@ -154,8 +99,8 @@ hostname: trontastic
if wf.Encoding != "" {
t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding)
}
if perm, _ := wf.Permissions(); perm != 0644 {
t.Errorf("WriteFile has incorrect permissions %s", perm)
if wf.Permissions != "0644" {
t.Errorf("WriteFile has incorrect permissions %s", wf.Permissions)
}
if wf.Path != "/etc/dogepack.conf" {
t.Errorf("WriteFile has incorrect path %s", wf.Path)
@@ -189,27 +134,20 @@ Address=10.209.171.177/19
}
}
if cfg.Coreos.OEM.ID != "rackspace" {
t.Errorf("Failed parsing coreos.oem. Expected ID 'rackspace', got %q.", cfg.Coreos.OEM.ID)
}
if cfg.Hostname != "trontastic" {
t.Errorf("Failed to parse hostname")
}
if cfg.Coreos.Update["reboot-strategy"] != "reboot" {
t.Errorf("Failed to parse locksmith strategy")
}
}
// Assert that our interface conversion doesn't panic
func TestCloudConfigKeysNotList(t *testing.T) {
contents := `
contents := []byte(`
ssh_authorized_keys:
- foo: bar
`
`)
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error: %v", err)
t.Fatalf("Encountered unexpected error :%v", err)
}
keys := cfg.SSHAuthorizedKeys
@@ -219,7 +157,7 @@ ssh_authorized_keys:
}
func TestCloudConfigSerializationHeader(t *testing.T) {
cfg, _ := NewCloudConfig("")
cfg, _ := NewCloudConfig([]byte{})
contents := cfg.String()
header := strings.SplitN(contents, "\n", 2)[0]
if header != "#cloud-config" {
@@ -227,28 +165,8 @@ 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 := `
contents := []byte(`
users:
- name: elroy
passwd: somehash
@@ -264,7 +182,7 @@ users:
no-user-group: true
system: y
no-log-init: True
`
`)
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error: %v", err)
@@ -305,7 +223,7 @@ users:
t.Errorf("Failed to parse no-create-home field")
}
if user.PrimaryGroup != "things" {
if user.PrimaryGroup != "things"{
t.Errorf("Failed to parse primary-group field, got %q", user.PrimaryGroup)
}

25
cloudinit/etcd.go Normal file
View File

@@ -0,0 +1,25 @@
package cloudinit
import (
"io/ioutil"
"log"
"os"
"path"
)
const (
etcdDiscoveryPath = "/var/run/etcd/bootstrap.disco"
)
func PersistEtcdDiscoveryURL(url string) error {
dir := path.Dir(etcdDiscoveryPath)
if _, err := os.Stat(dir); err != nil {
log.Printf("Creating directory /var/run/etcd")
err := os.MkdirAll(dir, os.FileMode(0644))
if err != nil {
return err
}
}
return ioutil.WriteFile(etcdDiscoveryPath, []byte(url), os.FileMode(0644))
}

View File

@@ -0,0 +1,36 @@
package cloudinit
import (
"io/ioutil"
"net/http"
)
type metadataService struct {
url string
client http.Client
}
func NewMetadataService(url string) *metadataService {
return &metadataService{url, http.Client{}}
}
func (ms *metadataService) UserData() ([]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
}

View File

@@ -1,4 +1,4 @@
package system
package cloudinit
import (
"fmt"

157
cloudinit/systemd.go Normal file
View File

@@ -0,0 +1,157 @@
package cloudinit
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
)
type Unit struct {
Name string
Runtime bool
Content string
}
func (u *Unit) Type() string {
ext := filepath.Ext(u.Name)
return strings.TrimLeft(ext, ".")
}
func (u *Unit) Group() (group string) {
t := u.Type()
if t == "network" || t == "netdev" || t == "link" {
group = "network"
} else {
group = "system"
}
return
}
type Script []byte
func PlaceUnit(root string, u *Unit) (string, error) {
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
}
}
dst = path.Join(dst, u.Name)
err := ioutil.WriteFile(dst, []byte(u.Content), os.FileMode(0644))
if err != nil {
return "", err
}
return dst, nil
}
func EnableUnitFile(file string, runtime bool) error {
conn, err := dbus.New()
if err != nil {
return err
}
files := []string{file}
_, _, err = conn.EnableUnitFiles(files, runtime, true)
return err
}
func separateNetworkUnits(units []Unit) ([]Unit, []Unit) {
networkUnits := make([]Unit, 0)
nonNetworkUnits := make([]Unit, 0)
for _, unit := range units {
if unit.Group() == "network" {
networkUnits = append(networkUnits, unit)
} else {
nonNetworkUnits = append(nonNetworkUnits, unit)
}
}
return networkUnits, nonNetworkUnits
}
func StartUnits(units []Unit) error {
networkUnits, nonNetworkUnits := separateNetworkUnits(units)
if len(networkUnits) > 0 {
if err := RestartUnitByName("systemd-networkd.service"); err != nil {
return err
}
}
for _, unit := range nonNetworkUnits {
if err := RestartUnitByName(unit.Name); err != nil {
return err
}
}
return nil
}
func DaemonReload() error {
conn, err := dbus.New()
if err != nil {
return err
}
_, err = conn.Reload()
return err
}
func RestartUnitByName(name string) error {
log.Printf("Restarting unit %s", name)
conn, err := dbus.New()
if err != nil {
return err
}
output, err := conn.RestartUnit(name, "replace")
log.Printf("Restart completed with '%s'", output)
return err
}
func StartUnitByName(name string) error {
conn, err := dbus.New()
if err != nil {
return err
}
_, err = conn.StartUnit(name, "replace")
return err
}
func ExecuteScript(scriptPath string) (string, error) {
props := []dbus.Property{
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
dbus.PropExecStart([]string{"/bin/bash", scriptPath}, false),
}
base := path.Base(scriptPath)
name := fmt.Sprintf("coreos-cloudinit-%s.service", base)
log.Printf("Creating transient systemd unit '%s'", name)
conn, err := dbus.New()
if err != nil {
return "", err
}
_, err = conn.StartTransientUnit(name, "replace", props...)
return name, err
}
func SetHostname(hostname string) error {
return exec.Command("hostnamectl", "set-hostname", hostname).Run()
}

102
cloudinit/systemd_test.go Normal file
View File

@@ -0,0 +1,102 @@
package cloudinit
import (
"io/ioutil"
"os"
"path"
"syscall"
"testing"
)
func TestPlaceNetworkUnit(t *testing.T) {
u := Unit{
Name: "50-eth0.network",
Runtime: true,
Content: `[Match]
Name=eth47
[Network]
Address=10.209.171.177/19
`,
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
if _, err := PlaceUnit(dir, &u); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
fullPath := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `[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)
}
}
func TestPlaceMountUnit(t *testing.T) {
u := Unit{
Name: "media-state.mount",
Runtime: false,
Content: `[Mount]
What=/dev/sdb1
Where=/media/state
`,
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
if _, err := PlaceUnit(dir, &u); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
fullPath := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `[Mount]
What=/dev/sdb1
Where=/media/state
`
if string(contents) != expect {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expect)
}
}

View File

@@ -1,4 +1,4 @@
package system
package cloudinit
import (
"fmt"
@@ -9,19 +9,17 @@ import (
)
type User struct {
Name string `yaml:"name"`
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"`
PrimaryGroup string `yaml:"primary-group"`
Groups []string `yaml:"groups"`
NoUserGroup bool `yaml:"no-user-group"`
System bool `yaml:"system"`
NoLogInit bool `yaml:"no-log-init"`
Name string `yaml:"name"`
PasswordHash string `yaml:"passwd"`
SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"`
GECOS string `yaml:"gecos"`
Homedir string `yaml:"homedir"`
NoCreateHome bool `yaml:"no-create-home"`
PrimaryGroup string `yaml:"primary-group"`
Groups []string `yaml:"groups"`
NoUserGroup bool `yaml:"no-user-group"`
System bool `yaml:"system"`
NoLogInit bool `yaml:"no-log-init"`
}
func UserExists(u *User) bool {
@@ -34,8 +32,6 @@ func CreateUser(u *User) error {
if u.PasswordHash != "" {
args = append(args, "--password", u.PasswordHash)
} else {
args = append(args, "--password", "*")
}
if u.GECOS != "" {
@@ -53,7 +49,7 @@ func CreateUser(u *User) error {
}
if u.PrimaryGroup != "" {
args = append(args, "--gid", u.PrimaryGroup)
args = append(args, "--primary-group", u.PrimaryGroup)
}
if len(u.Groups) > 0 {

30
cloudinit/user_data.go Normal file
View File

@@ -0,0 +1,30 @@
package cloudinit
import (
"bufio"
"bytes"
"fmt"
"log"
"strings"
)
func ParseUserData(contents []byte) (interface{}, error) {
bytereader := bytes.NewReader(contents)
bufreader := bufio.NewReader(bytereader)
header, _ := bufreader.ReadString('\n')
if strings.HasPrefix(header, "#!") {
log.Printf("Parsing user-data as script")
return Script(contents), nil
} else if header == "#cloud-config\n" {
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)
}
}

66
cloudinit/workspace.go Normal file
View File

@@ -0,0 +1,66 @@
package cloudinit
import (
"fmt"
"io/ioutil"
"os"
"path"
)
func PrepWorkspace(workspace string) error {
// Ensure workspace exists and is a directory
info, err := os.Stat(workspace)
if err == nil {
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", workspace)
}
} else {
err = os.MkdirAll(workspace, 0755)
if err != nil {
return err
}
}
// Ensure scripts dir in workspace exists and is a directory
scripts := path.Join(workspace, "scripts")
info, err = os.Stat(scripts)
if err == nil {
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", scripts)
}
} else {
err = os.Mkdir(scripts, 0755)
if err != nil {
return err
}
}
return nil
}
func PersistScriptInWorkspace(script Script, workspace string) (string, error) {
scriptsDir := path.Join(workspace, "scripts")
f, err := ioutil.TempFile(scriptsDir, "")
if err != nil {
return "", err
}
defer f.Close()
f.Chmod(0744)
_, err = f.Write(script)
if err != nil {
return "", err
}
// Ensure script has been written to disk before returning, as the
// next natural thing to do is execute it
f.Sync()
return f.Name(), nil
}
func PersistScriptUnitNameInWorkspace(name string, workspace string) error {
unitPath := path.Join(workspace, "scripts", "unit-name")
return ioutil.WriteFile(unitPath, []byte(name), 0644)
}

46
cloudinit/write_file.go Normal file
View File

@@ -0,0 +1,46 @@
package cloudinit
import (
"errors"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
)
type WriteFile struct {
Encoding string
Content string
Owner string
Path string
Permissions string
}
func ProcessWriteFile(base string, wf *WriteFile) error {
fullPath := path.Join(base, wf.Path)
if err := os.MkdirAll(path.Dir(fullPath), os.FileMode(0744)); err != nil {
return err
}
// Parse string representation of file mode as octal
perm, err := strconv.ParseInt(wf.Permissions, 8, 32)
if err != nil {
return errors.New("Unable to parse file permissions as octal integer")
}
if err := ioutil.WriteFile(fullPath, []byte(wf.Content), os.FileMode(perm)); err != nil {
return err
}
if wf.Owner != "" {
// We shell out since we don't have a way to look up unix groups natively
cmd := exec.Command("chown", wf.Owner, fullPath)
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,81 @@
package cloudinit
import (
"io/ioutil"
"os"
"path"
"syscall"
"testing"
)
func TestWriteFileUnencodedContent(t *testing.T) {
wf := WriteFile{
Path: "/tmp/foo",
Content: "bar",
Permissions: "0644",
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
if err := ProcessWriteFile(dir, &wf); err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
}
fullPath := path.Join(dir, "tmp", "foo")
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) != "bar" {
t.Fatalf("File has incorrect contents")
}
}
func TestWriteFileInvalidPermission(t *testing.T) {
wf := WriteFile{
Path: "/tmp/foo",
Content: "bar",
Permissions: "pants",
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
if err := ProcessWriteFile(dir, &wf); err == nil {
t.Fatalf("Expected error to be raised when writing file with invalid permission")
}
}
func TestWriteFileEncodedContent(t *testing.T) {
wf := WriteFile{
Path: "/tmp/foo",
Content: "",
Encoding: "base64",
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
if err := ProcessWriteFile(dir, &wf); err == nil {
t.Fatalf("Expected error to be raised when writing file with encoding")
}
}

View File

@@ -1,48 +1,35 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"flag"
"io/ioutil"
"os"
"path"
"log"
"github.com/coreos/coreos-cloudinit/datasource"
"github.com/coreos/coreos-cloudinit/initialize"
"github.com/coreos/coreos-cloudinit/network"
"github.com/coreos/coreos-cloudinit/system"
"github.com/coreos/coreos-cloudinit/cloudinit"
)
const version = "0.7.5"
const version = "0.1.2+git"
func main() {
var userdata []byte
var err error
var printVersion bool
flag.BoolVar(&printVersion, "version", false, "Print the version and exit")
var ignoreFailure bool
flag.BoolVar(&ignoreFailure, "ignore-failure", false, "Exits with 0 status in the event of malformed input from user-data")
var file string
flag.StringVar(&file, "from-file", "", "Read user-data from provided file")
var configdrive string
flag.StringVar(&configdrive, "from-configdrive", "", "Read user-data from provided cloud-drive directory")
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 convertNetconf string
flag.StringVar(&convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files (requires the -from-configdrive flag)")
var workspace string
flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
var sshKeyName string
flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
flag.StringVar(&sshKeyName, "ssh-key-name", cloudinit.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
flag.Parse()
@@ -51,138 +38,58 @@ func main() {
os.Exit(0)
}
var ds datasource.Datasource
if file != "" && url != "" {
fmt.Println("Provide one of --from-file or --from-url")
os.Exit(1)
}
if file != "" {
ds = datasource.NewLocalFile(file)
log.Printf("Reading user-data from file: %s", file)
userdata, err = ioutil.ReadFile(file)
if err != nil {
log.Fatal(err.Error())
}
} else if url != "" {
ds = datasource.NewMetadataService(url)
} else if configdrive != "" {
ds = datasource.NewConfigDrive(configdrive)
} else if useProcCmdline {
ds = datasource.NewProcCmdline()
} else {
fmt.Println("Provide one of --from-file, --from-configdrive, --from-url or --from-proc-cmdline")
os.Exit(1)
}
if convertNetconf != "" && configdrive == "" {
fmt.Println("-convert-netconf flag requires -from-configdrive")
os.Exit(1)
}
switch convertNetconf {
case "":
case "debian":
default:
fmt.Printf("Invalid option to -convert-netconf: '%s'. Supported options: 'debian'\n", convertNetconf)
os.Exit(1)
}
fmt.Printf("Fetching user-data from datasource of type %q\n", ds.Type())
userdataBytes, err := ds.Fetch()
if err != nil {
fmt.Printf("Failed fetching user-data from datasource: %v\n", err)
if ignoreFailure {
os.Exit(0)
} else {
os.Exit(1)
}
}
env := initialize.NewEnvironment("/", workspace)
if len(userdataBytes) > 0 {
if err := processUserdata(string(userdataBytes), env); err != nil {
fmt.Printf("Failed resolving user-data: %v\n", err)
if !ignoreFailure {
os.Exit(1)
}
log.Printf("Reading user-data from metadata service")
svc := cloudinit.NewMetadataService(url)
userdata, err = svc.UserData()
if err != nil {
log.Fatal(err.Error())
}
} else {
fmt.Println("No user data to handle.")
fmt.Println("Provide one of --from-file or --from-url")
os.Exit(1)
}
if convertNetconf != "" {
if err := processNetconf(convertNetconf, configdrive); err != nil {
fmt.Printf("Failed to process network config: %v\n", err)
if !ignoreFailure {
os.Exit(1)
}
}
if len(userdata) == 0 {
log.Printf("No user data to handle, exiting.")
os.Exit(0)
}
}
func processUserdata(userdata string, env *initialize.Environment) error {
userdata = env.Apply(userdata)
parsed, err := initialize.ParseUserData(userdata)
parsed, err := cloudinit.ParseUserData(userdata)
if err != nil {
fmt.Printf("Failed parsing user-data: %v\n", err)
return err
log.Fatalf("Failed parsing user-data: %v", err)
}
err = initialize.PrepWorkspace(env.Workspace())
err = cloudinit.PrepWorkspace(workspace)
if err != nil {
fmt.Printf("Failed preparing workspace: %v\n", err)
return err
log.Fatalf("Failed preparing workspace: %v", err)
}
switch t := parsed.(type) {
case initialize.CloudConfig:
err = initialize.Apply(t, env)
case system.Script:
case cloudinit.CloudConfig:
err = cloudinit.ApplyCloudConfig(t, sshKeyName)
case cloudinit.Script:
var path string
path, err = initialize.PersistScriptInWorkspace(t, env.Workspace())
path, err = cloudinit.PersistScriptInWorkspace(t, workspace)
if err == nil {
var name string
name, err = system.ExecuteScript(path)
initialize.PersistUnitNameInWorkspace(name, env.Workspace())
name, err = cloudinit.ExecuteScript(path)
cloudinit.PersistScriptUnitNameInWorkspace(name, workspace)
}
}
return err
}
func processNetconf(convertNetconf, configdrive string) error {
openstackRoot := path.Join(configdrive, "openstack")
metadataFilename := path.Join(openstackRoot, "latest", "meta_data.json")
metadataBytes, err := ioutil.ReadFile(metadataFilename)
if err != nil {
return err
}
var metadata struct {
NetworkConfig struct {
ContentPath string `json:"content_path"`
} `json:"network_config"`
}
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
return err
}
configPath := metadata.NetworkConfig.ContentPath
if configPath == "" {
fmt.Printf("No network config specified in %q.\n", metadataFilename)
return nil
}
netconfBytes, err := ioutil.ReadFile(path.Join(openstackRoot, configPath))
if err != nil {
return err
}
var interfaces []network.InterfaceGenerator
switch convertNetconf {
case "debian":
interfaces, err = network.ProcessDebianNetconf(string(netconfBytes))
default:
return fmt.Errorf("Unsupported network config format %q", convertNetconf)
}
if err != nil {
return err
}
if err := system.WriteNetworkdConfigs(interfaces); err != nil {
return err
}
return system.RestartNetwork(interfaces)
if err != nil {
log.Fatalf("Failed resolving user-data: %v", err)
}
}

27
cover
View File

@@ -1,27 +0,0 @@
#!/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,27 +0,0 @@
package datasource
import (
"io/ioutil"
"os"
"path"
)
type configDrive struct {
path string
}
func NewConfigDrive(path string) *configDrive {
return &configDrive{path}
}
func (self *configDrive) Fetch() ([]byte, error) {
data, err := ioutil.ReadFile(path.Join(self.path, "openstack", "latest", "user_data"))
if os.IsNotExist(err) {
err = nil
}
return data, err
}
func (self *configDrive) Type() string {
return "cloud-drive"
}

View File

@@ -1,6 +0,0 @@
package datasource
type Datasource interface {
Fetch() ([]byte, error)
Type() string
}

View File

@@ -1,21 +0,0 @@
package datasource
import (
"io/ioutil"
)
type localFile struct {
path string
}
func NewLocalFile(path string) *localFile {
return &localFile{path}
}
func (self *localFile) Fetch() ([]byte, error) {
return ioutil.ReadFile(self.path)
}
func (self *localFile) Type() string {
return "local-file"
}

View File

@@ -1,20 +0,0 @@
package datasource
import "github.com/coreos/coreos-cloudinit/pkg"
type metadataService struct {
url string
}
func NewMetadataService(url string) *metadataService {
return &metadataService{url}
}
func (ms *metadataService) Fetch() ([]byte, error) {
client := pkg.NewHttpClient()
return client.Get(ms.url)
}
func (ms *metadataService) Type() string {
return "metadata-service"
}

View File

@@ -1,72 +0,0 @@
package datasource
import (
"errors"
"io/ioutil"
"log"
"strings"
"github.com/coreos/coreos-cloudinit/pkg"
)
const (
ProcCmdlineLocation = "/proc/cmdline"
ProcCmdlineCloudConfigFlag = "cloud-config-url"
)
type procCmdline struct{
Location string
}
func NewProcCmdline() *procCmdline {
return &procCmdline{Location: ProcCmdlineLocation}
}
func (self *procCmdline) Fetch() ([]byte, error) {
contents, err := ioutil.ReadFile(self.Location)
if err != nil {
return nil, err
}
cmdline := strings.TrimSpace(string(contents))
url, err := findCloudConfigURL(cmdline)
if err != nil {
return nil, err
}
client := pkg.NewHttpClient()
cfg, err := client.Get(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

@@ -1,88 +0,0 @@
package datasource
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"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)
}
}
}
func TestProcCmdlineAndFetchConfig(t *testing.T) {
var (
ProcCmdlineTmpl = "foo=bar cloud-config-url=%s/config\n"
CloudConfigContent = "#cloud-config\n"
)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && r.RequestURI == "/config" {
fmt.Fprint(w, CloudConfigContent)
}
}))
defer ts.Close()
file, err := ioutil.TempFile(os.TempDir(), "test_proc_cmdline")
defer os.Remove(file.Name())
if err != nil {
t.Errorf("Test produced error: %v", err)
}
_, err = file.Write([]byte(fmt.Sprintf(ProcCmdlineTmpl, ts.URL)))
if err != nil {
t.Errorf("Test produced error: %v", err)
}
p := NewProcCmdline()
p.Location = file.Name()
cfg, err := p.Fetch()
if err != nil {
t.Errorf("Test produced error: %v", err)
}
if string(cfg) != CloudConfigContent {
t.Errorf("Test failed, response body: %s != %s", cfg, CloudConfigContent)
}
}

View File

@@ -1,290 +0,0 @@
package initialize
import (
"errors"
"fmt"
"log"
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
"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
// associated system.Units to be created/enabled appropriately
type CloudConfigUnit interface {
Units(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
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)
if err != nil {
return ""
}
stringified := string(bytes)
stringified = fmt.Sprintf("#cloud-config\n%s", stringified)
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 {
return err
}
log.Printf("Set hostname to %s", cfg.Hostname)
}
for _, user := range cfg.Users {
if user.Name == "" {
log.Printf("User object has no 'name' field, skipping")
continue
}
if system.UserExists(&user) {
log.Printf("User '%s' exists, ignoring creation-time fields", user.Name)
if user.PasswordHash != "" {
log.Printf("Setting '%s' user's password", user.Name)
if err := system.SetUserPassword(user.Name, user.PasswordHash); err != nil {
log.Printf("Failed setting '%s' user's password: %v", user.Name, err)
return err
}
}
} else {
log.Printf("Creating user '%s'", user.Name)
if err := system.CreateUser(&user); err != nil {
log.Printf("Failed creating user '%s': %v", user.Name, err)
return err
}
}
if len(user.SSHAuthorizedKeys) > 0 {
log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name)
if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil {
return err
}
}
if user.SSHImportGithubUser != "" {
log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name)
if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil {
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
}
}
}
if len(cfg.SSHAuthorizedKeys) > 0 {
err := system.AuthorizeSSHKeys("core", env.SSHKeyName(), cfg.SSHAuthorizedKeys)
if err == nil {
log.Printf("Authorized SSH keys for core user")
} else {
return err
}
}
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.Units(env.Root())
if err != nil {
return err
}
cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
}
for _, file := range cfg.WriteFiles {
path, err := system.WriteFile(&file, env.Root())
if err != nil {
return err
}
log.Printf("Wrote file %s to filesystem", path)
}
commands := make(map[string]string, 0)
reload := false
for _, unit := range cfg.Coreos.Units {
dst := unit.Destination(env.Root())
if unit.Content != "" {
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
if err := system.PlaceUnit(&unit, dst); err != nil {
return err
}
log.Printf("Placed unit %s at %s", unit.Name, dst)
reload = true
}
if unit.Mask {
log.Printf("Masking unit file %s", unit.Name)
if err := system.MaskUnit(&unit, env.Root()); err != nil {
return err
}
} else if unit.Runtime {
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
if err := system.UnmaskUnit(&unit, env.Root()); err != nil {
return err
}
}
if unit.Enable {
if unit.Group() != "network" {
log.Printf("Enabling unit file %s", unit.Name)
if err := system.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
return err
}
log.Printf("Enabled unit %s", unit.Name)
} else {
log.Printf("Skipping enable for network-like unit %s", unit.Name)
}
}
if unit.Group() == "network" {
commands["systemd-networkd.service"] = "restart"
} else if unit.Command != "" {
commands[unit.Name] = unit.Command
}
}
if reload {
if err := system.DaemonReload(); err != nil {
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
}
}
for unit, command := range commands {
log.Printf("Calling unit command '%s %s'", command, unit)
res, err := system.RunUnitCommand(command, unit)
if err != nil {
return err
}
log.Printf("Result of '%s %s': %s", command, unit, res)
}
return nil
}

View File

@@ -1,60 +0,0 @@
package initialize
import (
"os"
"path"
"strings"
)
const DefaultSSHKeyName = "coreos-cloudinit"
type Environment struct {
root string
workspace string
sshKeyName string
substitutions map[string]string
}
func NewEnvironment(root, workspace string) *Environment {
substitutions := map[string]string{
"$public_ipv4": os.Getenv("COREOS_PUBLIC_IPV4"),
"$private_ipv4": os.Getenv("COREOS_PRIVATE_IPV4"),
}
return &Environment{root, workspace, DefaultSSHKeyName, substitutions}
}
func (self *Environment) Workspace() string {
return path.Join(self.root, self.workspace)
}
func (self *Environment) Root() string {
return self.root
}
func (self *Environment) SSHKeyName() string {
return self.sshKeyName
}
func (self *Environment) SetSSHKeyName(name string) {
self.sshKeyName = name
}
func (self *Environment) Apply(data string) string {
for key, val := range self.substitutions {
data = strings.Replace(data, key, val, -1)
}
return data
}
// 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

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

View File

@@ -1,55 +0,0 @@
package initialize
import (
"errors"
"fmt"
"github.com/coreos/coreos-cloudinit/system"
)
type EtcdEnvironment map[string]string
func (ee EtcdEnvironment) String() (out string) {
norm := normalizeSvcEnv(ee)
if val, ok := norm["DISCOVERY_URL"]; ok {
delete(norm, "DISCOVERY_URL")
if _, ok := norm["DISCOVERY"]; !ok {
norm["DISCOVERY"] = val
}
}
out += "[Service]\n"
for key, val := range norm {
out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val)
}
return
}
// Units creates a Unit file drop-in for etcd, using any configured
// options and adding a default MachineID if unset.
func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
if ee == nil {
return nil, nil
}
if _, ok := ee["name"]; !ok {
if machineID := system.MachineID(root); machineID != "" {
ee["name"] = machineID
} else if hostname, err := system.Hostname(); err == nil {
ee["name"] = hostname
} else {
return nil, errors.New("Unable to determine default etcd name")
}
}
etcd := system.Unit{
Name: "etcd.service",
Runtime: true,
DropIn: true,
Content: ee.String(),
}
return []system.Unit{etcd}, nil
}

View File

@@ -1,168 +0,0 @@
package initialize
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestEtcdEnvironment(t *testing.T) {
cfg := make(EtcdEnvironment, 0)
cfg["discovery"] = "http://disco.example.com/foobar"
cfg["peer-bind-addr"] = "127.0.0.1:7002"
env := cfg.String()
expect := `[Service]
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`
if env != expect {
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
}
}
func TestEtcdEnvironmentDiscoveryURLTranslated(t *testing.T) {
cfg := make(EtcdEnvironment, 0)
cfg["discovery_url"] = "http://disco.example.com/foobar"
cfg["peer-bind-addr"] = "127.0.0.1:7002"
env := cfg.String()
expect := `[Service]
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`
if env != expect {
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
}
}
func TestEtcdEnvironmentDiscoveryOverridesDiscoveryURL(t *testing.T) {
cfg := make(EtcdEnvironment, 0)
cfg["discovery_url"] = "ping"
cfg["discovery"] = "pong"
cfg["peer-bind-addr"] = "127.0.0.1:7002"
env := cfg.String()
expect := `[Service]
Environment="ETCD_DISCOVERY=pong"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`
if env != expect {
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
}
}
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
ee := EtcdEnvironment{
"name": "node001",
"discovery": "http://disco.example.com/foobar",
"peer-bind-addr": "127.0.0.1:7002",
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
uu, err := ee.Units(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
}
if len(uu) != 1 {
t.Fatalf("Expected 1 unit to be returned, got %d", len(uu))
}
u := uu[0]
dst := u.Destination(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")
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `[Service]
Environment="ETCD_NAME=node001"
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`
if string(contents) != expect {
t.Fatalf("File has incorrect contents")
}
}
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
ee := EtcdEnvironment{}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
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))
if err != nil {
t.Fatalf("Failed writing out /etc/machine-id: %v", err)
}
uu, err := ee.Units(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
}
if len(uu) == 0 {
t.Fatalf("Returned empty etcd units unexpectedly")
}
u := uu[0]
dst := u.Destination(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")
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `[Service]
Environment="ETCD_NAME=node007"
`
if string(contents) != expect {
t.Fatalf("File has incorrect contents")
}
}
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")
}
uu, err := ee.Units("")
if len(uu) != 0 || err != nil {
t.Fatalf("Units returned value for nil input")
}
}

View File

@@ -1,35 +0,0 @@
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
}
// Units generates a Unit file drop-in for fleet, if any fleet options were
// configured in cloud-config
func (fe FleetEnvironment) Units(root string) ([]system.Unit, error) {
if len(fe) < 1 {
return nil, nil
}
fleet := system.Unit{
Name: "fleet.service",
Runtime: true,
DropIn: true,
Content: fe.String(),
}
return []system.Unit{fleet}, nil
}

View File

@@ -1,43 +0,0 @@
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)
uu, err := cfg.Units("/")
if len(uu) != 0 {
t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
}
cfg["public-ip"] = "12.34.56.78"
uu, err = cfg.Units("/")
if err != nil {
t.Errorf("error generating fleet unit: %v", err)
}
if len(uu) != 1 {
t.Fatalf("expected 1 unit generated, got %d", len(uu))
}
u := uu[0]
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,18 +0,0 @@
package initialize
import (
"fmt"
"github.com/coreos/coreos-cloudinit/system"
)
func SSHImportGithubUser(system_user string, github_user string) error {
url := fmt.Sprintf("https://api.github.com/users/%s/keys", github_user)
keys, err := fetchUserKeys(url)
if err != nil {
return err
}
key_name := fmt.Sprintf("github-%s", github_user)
return system.AuthorizeSSHKeys(system_user, key_name, keys)
}

View File

@@ -1,32 +0,0 @@
package initialize
import (
"testing"
)
func TestCloudConfigUsersGithubUser(t *testing.T) {
contents := `
users:
- name: elroy
coreos-ssh-import-github: bcwaldon
`
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.SSHImportGithubUser != "bcwaldon" {
t.Errorf("github user is %q, expected 'bcwaldon'", user.SSHImportGithubUser)
}
}

View File

@@ -1,46 +0,0 @@
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

@@ -1,83 +0,0 @@
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")
}
if _, err := system.WriteFile(f, dir); 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

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

View File

@@ -1,63 +0,0 @@
package initialize
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestOEMReleaseWrittenToDisk(t *testing.T) {
oem := OEMRelease{
ID: "rackspace",
Name: "Rackspace Cloud Servers",
VersionID: "168.0.0",
HomeURL: "https://www.rackspace.com/cloud/servers/",
BugReportURL: "https://github.com/coreos/coreos-overlay",
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
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")
}
if _, err := system.WriteFile(f, dir); err != nil {
t.Fatalf("Writing of OEMRelease failed: %v", err)
}
fullPath := path.Join(dir, "etc", "oem-release")
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0644) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
contents, err := ioutil.ReadFile(fullPath)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expect := `ID=rackspace
VERSION_ID=168.0.0
NAME="Rackspace Cloud Servers"
HOME_URL="https://www.rackspace.com/cloud/servers/"
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
`
if string(contents) != expect {
t.Fatalf("File has incorrect contents")
}
}

View File

@@ -1,43 +0,0 @@
package initialize
import (
"encoding/json"
"fmt"
"github.com/coreos/coreos-cloudinit/pkg"
"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) {
client := pkg.NewHttpClient()
data, err := client.Get(url)
if err != nil {
return nil, err
}
var userKeys []UserKey
err = json.Unmarshal(data, &userKeys)
if err != nil {
return nil, err
}
keys := make([]string, 0)
for _, key := range userKeys {
keys = append(keys, key.Key)
}
return keys, err
}

View File

@@ -1,69 +0,0 @@
package initialize
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestCloudConfigUsersUrlMarshal(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gh_res := `
[
{
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ=="
},
{
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBANxpzIbTzKTeBRaOIdUxwwGwvDasTfU/PonhbNIuhYjc+xFGvBRTumox2F+luVAKKs4WdvA4nJXaY1OFi6DZftk5Bp4E2JaSzp8ulAzHsMexDdv6LGHGEJj/qdHAL1vHk2K89PpwRFSRZI8XRBLjvkr4ZgBKLG5ZILXPJEPP2j3lAAAAFQCtxoTnV8wy0c4grcGrQ+1sCsD7WQAAAIAqZsW2GviMe1RQrbZT0xAZmI64XRPrnLsoLxycHWlS7r6uUln2c6Ae2MB/YF0d4Kd1XZii9GHj7rrypqEo7MW8uSabhu70nmu1J8m2O3Dsr+4oJLeat9vwPsJV92IKO0jQwjKnAOHOiB9JKGeCw+NfXfogbti9/q38Q6XcS+SI5wAAAIEA1803Y2h+tOOpZXAsNIwl9mRfExWzLQ3L7knwJdznQu/6SW1H/1oyoYLebuk187Qj2UFI5qQ6AZNc49DvohWx0Cg6ABcyubNyoaCjZKWIdxVnItHWNbLe//+tyTu0I2eQwJOORsEPK5gMpf599C7wXQ//DzZOWbTWiHEX52gCTmk="
},
{
"id": 5224438,
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O"
}
]
`
fmt.Fprintln(w, gh_res)
}))
defer ts.Close()
keys, err := fetchUserKeys(ts.URL)
if err != nil {
t.Fatalf("Encountered unexpected error: %v", err)
}
expected := "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ=="
if keys[0] != expected {
t.Fatalf("expected %s, got %s", expected, keys[0])
}
expected = "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O"
if keys[2] != expected {
t.Fatalf("expected %s, got %s", expected, keys[2])
}
}
func TestCloudConfigUsersSSHImportURL(t *testing.T) {
contents := `
users:
- name: elroy
coreos-ssh-import-url: https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys
`
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error: %v", err)
}
if len(cfg.Users) != 1 {
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
}
user := cfg.Users[0]
if user.Name != "elroy" {
t.Errorf("User name is %q, expected 'elroy'", user.Name)
}
if user.SSHImportURL != "https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys" {
t.Errorf("ssh import url is %q, expected 'https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys'", user.SSHImportURL)
}
}

View File

@@ -1,165 +0,0 @@
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
}
// Units generates units for the cloud-init initializer to act on:
// - a locksmith system.Unit, if "reboot-strategy" was set in cloud-config
// - an update_engine system.Unit, if "group" was set in cloud-config
func (uc UpdateConfig) Units(root string) ([]system.Unit, error) {
var units []system.Unit
if strategy, ok := uc["reboot-strategy"]; ok {
ls := &system.Unit{
Name: locksmithUnit,
Command: "restart",
Mask: false,
Runtime: true,
}
if strategy == "off" {
ls.Command = "stop"
ls.Mask = true
}
units = append(units, *ls)
}
rue := false
if _, ok := uc["group"]; ok {
rue = true
}
if _, ok := uc["server"]; ok {
rue = true
}
if rue {
ue := system.Unit{
Name: "update-engine",
Command: "restart",
}
units = append(units, ue)
}
return units, nil
}

View File

@@ -1,232 +0,0 @@
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)
}
uu, err := uc.Units("")
if err != nil {
t.Error("unexpected error getting unit from empty UpdateConfig")
}
if len(uu) != 0 {
t.Errorf("getting unit from empty UpdateConfig should have returned zero units, got %d", len(uu))
}
}
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)
}
}
uu, err := u.Units(dir)
if err != nil {
t.Errorf("unexpected error getting units from UpdateConfig: %v", err)
} else if len(uu) != 1 {
t.Errorf("unexpected number of files returned from UpdateConfig: want 1, got %d", len(uu))
} else {
unit := uu[0]
if unit.Name != "update-engine" {
t.Errorf("bad name for generated unit: want update-engine, got %s", unit.Name)
}
if unit.Command != "restart" {
t.Errorf("bad command for generated unit: want restart, got %s", unit.Command)
}
}
}
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)
}
}
uu, err := uc.Units(dir)
if err != nil {
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
} else if len(uu) != 1 {
t.Errorf("unexpected number of units for reboot-strategy=%v: %d", s.name, len(uu))
} else {
u := uu[0]
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")
}
if _, err := system.WriteFile(f, dir); 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)
}
}
}

View File

@@ -1,33 +0,0 @@
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

@@ -1,49 +0,0 @@
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

@@ -1,51 +0,0 @@
package initialize
import (
"io/ioutil"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
func PrepWorkspace(workspace string) error {
if err := system.EnsureDirectoryExists(workspace); err != nil {
return err
}
scripts := path.Join(workspace, "scripts")
if err := system.EnsureDirectoryExists(scripts); err != nil {
return err
}
return nil
}
func PersistScriptInWorkspace(script system.Script, workspace string) (string, error) {
scriptsPath := path.Join(workspace, "scripts")
tmp, err := ioutil.TempFile(scriptsPath, "")
if err != nil {
return "", err
}
tmp.Close()
relpath := strings.TrimPrefix(tmp.Name(), workspace)
file := system.File{
Path: relpath,
RawFilePermissions: "0744",
Content: string(script),
}
return system.WriteFile(&file, workspace)
}
func PersistUnitNameInWorkspace(name string, workspace string) error {
file := system.File{
Path: path.Join("scripts", "unit-name"),
RawFilePermissions: "0644",
Content: name,
}
_, err := system.WriteFile(&file, workspace)
return err
}

View File

@@ -1,193 +0,0 @@
package network
import (
"fmt"
"strconv"
)
type InterfaceGenerator interface {
Name() string
Netdev() string
Link() string
Network() string
}
type logicalInterface struct {
name string
config configMethod
children []InterfaceGenerator
}
func (i *logicalInterface) Network() string {
config := fmt.Sprintf("[Match]\nName=%s\n\n[Network]\n", i.name)
for _, child := range i.children {
switch iface := child.(type) {
case *vlanInterface:
config += fmt.Sprintf("VLAN=%s\n", iface.name)
case *bondInterface:
config += fmt.Sprintf("Bond=%s\n", iface.name)
}
}
switch conf := i.config.(type) {
case configMethodStatic:
for _, nameserver := range conf.nameservers {
config += fmt.Sprintf("DNS=%s\n", nameserver)
}
if conf.address.IP != nil {
config += fmt.Sprintf("\n[Address]\nAddress=%s\n", conf.address.String())
}
for _, route := range conf.routes {
config += fmt.Sprintf("\n[Route]\nDestination=%s\nGateway=%s\n", route.destination.String(), route.gateway)
}
case configMethodDHCP:
config += "DHCP=true\n"
}
return config
}
type physicalInterface struct {
logicalInterface
}
func (p *physicalInterface) Name() string {
return p.name
}
func (p *physicalInterface) Netdev() string {
return ""
}
func (p *physicalInterface) Link() string {
return ""
}
type bondInterface struct {
logicalInterface
slaves []string
}
func (b *bondInterface) Name() string {
return b.name
}
func (b *bondInterface) Netdev() string {
return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name)
}
func (b *bondInterface) Link() string {
return ""
}
type vlanInterface struct {
logicalInterface
id int
rawDevice string
}
func (v *vlanInterface) Name() string {
return v.name
}
func (v *vlanInterface) Netdev() string {
return fmt.Sprintf("[NetDev]\nKind=vlan\nName=%s\n\n[VLAN]\nId=%d\n", v.name, v.id)
}
func (v *vlanInterface) Link() string {
return ""
}
func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator {
bondStanzas := make(map[string]*stanzaInterface)
physicalStanzas := make(map[string]*stanzaInterface)
vlanStanzas := make(map[string]*stanzaInterface)
for _, iface := range stanzas {
switch iface.kind {
case interfaceBond:
bondStanzas[iface.name] = iface
case interfacePhysical:
physicalStanzas[iface.name] = iface
case interfaceVLAN:
vlanStanzas[iface.name] = iface
}
}
physicals := make(map[string]*physicalInterface)
for _, p := range physicalStanzas {
if _, ok := p.configMethod.(configMethodLoopback); ok {
continue
}
physicals[p.name] = &physicalInterface{
logicalInterface{
name: p.name,
config: p.configMethod,
children: []InterfaceGenerator{},
},
}
}
bonds := make(map[string]*bondInterface)
for _, b := range bondStanzas {
bonds[b.name] = &bondInterface{
logicalInterface{
name: b.name,
config: b.configMethod,
children: []InterfaceGenerator{},
},
b.options["slaves"],
}
}
vlans := make(map[string]*vlanInterface)
for _, v := range vlanStanzas {
var rawDevice string
id, _ := strconv.Atoi(v.options["id"][0])
if device := v.options["raw_device"]; len(device) == 1 {
rawDevice = device[0]
}
vlans[v.name] = &vlanInterface{
logicalInterface{
name: v.name,
config: v.configMethod,
children: []InterfaceGenerator{},
},
id,
rawDevice,
}
}
for _, vlan := range vlans {
if physical, ok := physicals[vlan.rawDevice]; ok {
physical.children = append(physical.children, vlan)
}
if bond, ok := bonds[vlan.rawDevice]; ok {
bond.children = append(bond.children, vlan)
}
}
for _, bond := range bonds {
for _, slave := range bond.slaves {
if physical, ok := physicals[slave]; ok {
physical.children = append(physical.children, bond)
}
if pBond, ok := bonds[slave]; ok {
pBond.children = append(pBond.children, bond)
}
}
}
interfaces := make([]InterfaceGenerator, 0, len(physicals)+len(bonds)+len(vlans))
for _, physical := range physicals {
interfaces = append(interfaces, physical)
}
for _, bond := range bonds {
interfaces = append(interfaces, bond)
}
for _, vlan := range vlans {
interfaces = append(interfaces, vlan)
}
return interfaces
}

View File

@@ -1,321 +0,0 @@
package network
import (
"net"
"reflect"
"testing"
)
func TestPhysicalInterfaceName(t *testing.T) {
p := physicalInterface{logicalInterface{name: "testname"}}
if p.Name() != "testname" {
t.FailNow()
}
}
func TestPhysicalInterfaceNetdev(t *testing.T) {
p := physicalInterface{}
if p.Netdev() != "" {
t.FailNow()
}
}
func TestPhysicalInterfaceLink(t *testing.T) {
p := physicalInterface{}
if p.Link() != "" {
t.FailNow()
}
}
func TestPhysicalInterfaceNetwork(t *testing.T) {
p := physicalInterface{logicalInterface{
name: "testname",
children: []InterfaceGenerator{
&bondInterface{
logicalInterface{
name: "testbond1",
},
nil,
},
&vlanInterface{
logicalInterface{
name: "testvlan1",
},
1,
"",
},
&vlanInterface{
logicalInterface{
name: "testvlan2",
},
1,
"",
},
},
}}
network := `[Match]
Name=testname
[Network]
Bond=testbond1
VLAN=testvlan1
VLAN=testvlan2
`
if p.Network() != network {
t.FailNow()
}
}
func TestBondInterfaceName(t *testing.T) {
b := bondInterface{logicalInterface{name: "testname"}, nil}
if b.Name() != "testname" {
t.FailNow()
}
}
func TestBondInterfaceNetdev(t *testing.T) {
b := bondInterface{logicalInterface{name: "testname"}, nil}
netdev := `[NetDev]
Kind=bond
Name=testname
`
if b.Netdev() != netdev {
t.FailNow()
}
}
func TestBondInterfaceLink(t *testing.T) {
b := bondInterface{}
if b.Link() != "" {
t.FailNow()
}
}
func TestBondInterfaceNetwork(t *testing.T) {
b := bondInterface{
logicalInterface{
name: "testname",
config: configMethodDHCP{},
children: []InterfaceGenerator{
&bondInterface{
logicalInterface{
name: "testbond1",
},
nil,
},
&vlanInterface{
logicalInterface{
name: "testvlan1",
},
1,
"",
},
&vlanInterface{
logicalInterface{
name: "testvlan2",
},
1,
"",
},
},
},
nil,
}
network := `[Match]
Name=testname
[Network]
Bond=testbond1
VLAN=testvlan1
VLAN=testvlan2
DHCP=true
`
if b.Network() != network {
t.FailNow()
}
}
func TestVLANInterfaceName(t *testing.T) {
v := vlanInterface{logicalInterface{name: "testname"}, 1, ""}
if v.Name() != "testname" {
t.FailNow()
}
}
func TestVLANInterfaceNetdev(t *testing.T) {
v := vlanInterface{logicalInterface{name: "testname"}, 1, ""}
netdev := `[NetDev]
Kind=vlan
Name=testname
[VLAN]
Id=1
`
if v.Netdev() != netdev {
t.FailNow()
}
}
func TestVLANInterfaceLink(t *testing.T) {
v := vlanInterface{}
if v.Link() != "" {
t.FailNow()
}
}
func TestVLANInterfaceNetwork(t *testing.T) {
v := vlanInterface{
logicalInterface{
name: "testname",
config: configMethodStatic{
address: net.IPNet{
IP: []byte{192, 168, 1, 100},
Mask: []byte{255, 255, 255, 0},
},
nameservers: []net.IP{
[]byte{8, 8, 8, 8},
},
routes: []route{
route{
destination: net.IPNet{
IP: []byte{0, 0, 0, 0},
Mask: []byte{0, 0, 0, 0},
},
gateway: []byte{1, 2, 3, 4},
},
},
},
},
0,
"",
}
network := `[Match]
Name=testname
[Network]
DNS=8.8.8.8
[Address]
Address=192.168.1.100/24
[Route]
Destination=0.0.0.0/0
Gateway=1.2.3.4
`
if v.Network() != network {
t.Log(v.Network())
t.FailNow()
}
}
func TestBuildInterfacesLo(t *testing.T) {
stanzas := []*stanzaInterface{
&stanzaInterface{
name: "lo",
kind: interfacePhysical,
auto: false,
configMethod: configMethodLoopback{},
options: map[string][]string{},
},
}
interfaces := buildInterfaces(stanzas)
if len(interfaces) != 0 {
t.FailNow()
}
}
func TestBuildInterfaces(t *testing.T) {
stanzas := []*stanzaInterface{
&stanzaInterface{
name: "eth0",
kind: interfacePhysical,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{},
},
&stanzaInterface{
name: "bond0",
kind: interfaceBond,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"slaves": []string{"eth0"},
},
},
&stanzaInterface{
name: "bond1",
kind: interfaceBond,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"slaves": []string{"bond0"},
},
},
&stanzaInterface{
name: "vlan0",
kind: interfaceVLAN,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"id": []string{"0"},
"raw_device": []string{"eth0"},
},
},
&stanzaInterface{
name: "vlan1",
kind: interfaceVLAN,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{
"id": []string{"1"},
"raw_device": []string{"bond0"},
},
},
}
interfaces := buildInterfaces(stanzas)
vlan1 := &vlanInterface{
logicalInterface{
name: "vlan1",
config: configMethodManual{},
children: []InterfaceGenerator{},
},
1,
"bond0",
}
vlan0 := &vlanInterface{
logicalInterface{
name: "vlan0",
config: configMethodManual{},
children: []InterfaceGenerator{},
},
0,
"eth0",
}
bond1 := &bondInterface{
logicalInterface{
name: "bond1",
config: configMethodManual{},
children: []InterfaceGenerator{},
},
[]string{"bond0"},
}
bond0 := &bondInterface{
logicalInterface{
name: "bond0",
config: configMethodManual{},
children: []InterfaceGenerator{vlan1, bond1},
},
[]string{"eth0"},
}
eth0 := &physicalInterface{
logicalInterface{
name: "eth0",
config: configMethodManual{},
children: []InterfaceGenerator{vlan0, bond0},
},
}
expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1}
if !reflect.DeepEqual(interfaces, expect) {
t.FailNow()
}
}

View File

@@ -1,45 +0,0 @@
package network
import (
"strings"
)
func ProcessDebianNetconf(config string) ([]InterfaceGenerator, error) {
lines := formatConfig(config)
stanzas, err := parseStanzas(lines)
if err != nil {
return nil, err
}
interfaces := make([]*stanzaInterface, 0, len(stanzas))
for _, stanza := range stanzas {
switch s := stanza.(type) {
case *stanzaInterface:
interfaces = append(interfaces, s)
}
}
return buildInterfaces(interfaces), nil
}
func formatConfig(config string) []string {
lines := []string{}
config = strings.Replace(config, "\\\n", "", -1)
for config != "" {
split := strings.SplitN(config, "\n", 2)
line := strings.TrimSpace(split[0])
if len(split) == 2 {
config = split[1]
} else {
config = ""
}
if strings.HasPrefix(line, "#") || line == "" {
continue
}
lines = append(lines, line)
}
return lines
}

View File

@@ -1,42 +0,0 @@
package network
import (
"testing"
)
func TestFormatConfigs(t *testing.T) {
for in, n := range map[string]int{
"": 0,
"line1\\\nis long": 1,
"#comment": 0,
"#comment\\\ncomment": 0,
" #comment \\\n comment\nline 1\nline 2\\\n is long": 2,
} {
lines := formatConfig(in)
if len(lines) != n {
t.Fatalf("bad number of lines for config %q: got %d, want %d", in, len(lines), n)
}
}
}
func TestProcessDebianNetconf(t *testing.T) {
for _, tt := range []struct {
in string
fail bool
n int
}{
{"", false, 0},
{"iface", true, -1},
{"auto eth1\nauto eth2", false, 0},
{"iface eth1 inet manual", false, 1},
} {
interfaces, err := ProcessDebianNetconf(tt.in)
failed := err != nil
if tt.fail != failed {
t.Fatalf("bad failure state for %q: got %b, want %b", failed, tt.fail)
}
if tt.n != -1 && tt.n != len(interfaces) {
t.Fatalf("bad number of interfaces for %q: got %d, want %q", tt.in, len(interfaces), tt.n)
}
}
}

View File

@@ -1,295 +0,0 @@
package network
import (
"fmt"
"net"
"strconv"
"strings"
)
type stanza interface{}
type stanzaAuto struct {
interfaces []string
}
type stanzaInterface struct {
name string
kind interfaceKind
auto bool
configMethod configMethod
options map[string][]string
}
type interfaceKind int
const (
interfaceBond = interfaceKind(iota)
interfacePhysical
interfaceVLAN
)
type route struct {
destination net.IPNet
gateway net.IP
}
type configMethod interface{}
type configMethodStatic struct {
address net.IPNet
nameservers []net.IP
routes []route
}
type configMethodLoopback struct{}
type configMethodManual struct{}
type configMethodDHCP struct{}
func parseStanzas(lines []string) (stanzas []stanza, err error) {
rawStanzas, err := splitStanzas(lines)
if err != nil {
return nil, err
}
stanzas = make([]stanza, 0, len(rawStanzas))
for _, rawStanza := range rawStanzas {
if stanza, err := parseStanza(rawStanza); err == nil {
stanzas = append(stanzas, stanza)
} else {
return nil, err
}
}
autos := make([]string, 0)
interfaceMap := make(map[string]*stanzaInterface)
for _, stanza := range stanzas {
switch c := stanza.(type) {
case *stanzaAuto:
autos = append(autos, c.interfaces...)
case *stanzaInterface:
interfaceMap[c.name] = c
}
}
// Apply the auto attribute
for _, auto := range autos {
if iface, ok := interfaceMap[auto]; ok {
iface.auto = true
}
}
return stanzas, nil
}
func splitStanzas(lines []string) ([][]string, error) {
var curStanza []string
stanzas := make([][]string, 0)
for _, line := range lines {
if isStanzaStart(line) {
if curStanza != nil {
stanzas = append(stanzas, curStanza)
}
curStanza = []string{line}
} else if curStanza != nil {
curStanza = append(curStanza, line)
} else {
return nil, fmt.Errorf("missing stanza start '%s'", line)
}
}
if curStanza != nil {
stanzas = append(stanzas, curStanza)
}
return stanzas, nil
}
func isStanzaStart(line string) bool {
switch strings.Split(line, " ")[0] {
case "auto":
fallthrough
case "iface":
fallthrough
case "mapping":
return true
}
if strings.HasPrefix(line, "allow-") {
return true
}
return false
}
func parseStanza(rawStanza []string) (stanza, error) {
if len(rawStanza) == 0 {
panic("empty stanza")
}
tokens := strings.Fields(rawStanza[0])
if len(tokens) < 2 {
return nil, fmt.Errorf("malformed stanza start %q", rawStanza[0])
}
kind := tokens[0]
attributes := tokens[1:]
switch kind {
case "auto":
return parseAutoStanza(attributes, rawStanza[1:])
case "iface":
return parseInterfaceStanza(attributes, rawStanza[1:])
default:
return nil, fmt.Errorf("unknown stanza '%s'", kind)
}
}
func parseAutoStanza(attributes []string, options []string) (*stanzaAuto, error) {
return &stanzaAuto{interfaces: attributes}, nil
}
func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterface, error) {
if len(attributes) != 3 {
return nil, fmt.Errorf("incorrect number of attributes")
}
iface := attributes[0]
confMethod := attributes[2]
optionMap := make(map[string][]string, 0)
for _, option := range options {
if strings.HasPrefix(option, "post-up") {
tokens := strings.SplitAfterN(option, " ", 2)
if len(tokens) != 2 {
continue
}
if v, ok := optionMap["post-up"]; ok {
optionMap["post-up"] = append(v, tokens[1])
} else {
optionMap["post-up"] = []string{tokens[1]}
}
} else if strings.HasPrefix(option, "pre-down") {
tokens := strings.SplitAfterN(option, " ", 2)
if len(tokens) != 2 {
continue
}
if v, ok := optionMap["pre-down"]; ok {
optionMap["pre-down"] = append(v, tokens[1])
} else {
optionMap["pre-down"] = []string{tokens[1]}
}
} else {
tokens := strings.Fields(option)
optionMap[tokens[0]] = tokens[1:]
}
}
var conf configMethod
switch confMethod {
case "static":
config := configMethodStatic{
routes: make([]route, 0),
nameservers: make([]net.IP, 0),
}
if addresses, ok := optionMap["address"]; ok {
if len(addresses) == 1 {
config.address.IP = net.ParseIP(addresses[0])
}
}
if netmasks, ok := optionMap["netmask"]; ok {
if len(netmasks) == 1 {
config.address.Mask = net.IPMask(net.ParseIP(netmasks[0]).To4())
}
}
if config.address.IP == nil || config.address.Mask == nil {
return nil, fmt.Errorf("malformed static network config for '%s'", iface)
}
if gateways, ok := optionMap["gateway"]; ok {
if len(gateways) == 1 {
config.routes = append(config.routes, route{
destination: net.IPNet{
IP: net.IPv4(0, 0, 0, 0),
Mask: net.IPv4Mask(0, 0, 0, 0),
},
gateway: net.ParseIP(gateways[0]),
})
}
}
for _, nameserver := range optionMap["dns-nameservers"] {
config.nameservers = append(config.nameservers, net.ParseIP(nameserver))
}
for _, postup := range optionMap["post-up"] {
if strings.HasPrefix(postup, "route add") {
route := route{}
fields := strings.Fields(postup)
for i, field := range fields[:len(fields)-1] {
switch field {
case "-net":
route.destination.IP = net.ParseIP(fields[i+1])
case "netmask":
route.destination.Mask = net.IPMask(net.ParseIP(fields[i+1]).To4())
case "gw":
route.gateway = net.ParseIP(fields[i+1])
}
}
if route.destination.IP != nil && route.destination.Mask != nil && route.gateway != nil {
config.routes = append(config.routes, route)
}
}
}
conf = config
case "loopback":
conf = configMethodLoopback{}
case "manual":
conf = configMethodManual{}
case "dhcp":
conf = configMethodDHCP{}
default:
return nil, fmt.Errorf("invalid config method '%s'", confMethod)
}
if _, ok := optionMap["vlan_raw_device"]; ok {
return parseVLANStanza(iface, conf, attributes, optionMap)
}
if strings.Contains(iface, ".") {
return parseVLANStanza(iface, conf, attributes, optionMap)
}
if _, ok := optionMap["bond-slaves"]; ok {
return parseBondStanza(iface, conf, attributes, optionMap)
}
return parsePhysicalStanza(iface, conf, attributes, optionMap)
}
func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
options["slaves"] = options["bond-slaves"]
return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil
}
func parsePhysicalStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
return &stanzaInterface{name: iface, kind: interfacePhysical, configMethod: conf, options: options}, nil
}
func parseVLANStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) {
var id string
if strings.Contains(iface, ".") {
tokens := strings.Split(iface, ".")
id = tokens[len(tokens)-1]
} else if strings.HasPrefix(iface, "vlan") {
id = strings.TrimPrefix(iface, "vlan")
} else {
return nil, fmt.Errorf("malformed vlan name %s", iface)
}
if _, err := strconv.Atoi(id); err != nil {
return nil, fmt.Errorf("malformed vlan name %s", iface)
}
options["id"] = []string{id}
options["raw_device"] = options["vlan_raw_device"]
return &stanzaInterface{name: iface, kind: interfaceVLAN, configMethod: conf, options: options}, nil
}

View File

@@ -1,502 +0,0 @@
package network
import (
"net"
"reflect"
"strings"
"testing"
)
func TestSplitStanzasNoParent(t *testing.T) {
in := []string{"test"}
e := "missing stanza start"
_, err := splitStanzas(in)
if err == nil || !strings.HasPrefix(err.Error(), e) {
t.Fatalf("bad error for splitStanzas(%q): got %q, want %q", in, err, e)
}
}
func TestBadParseStanzas(t *testing.T) {
for in, e := range map[string]string{
"": "missing stanza start",
"iface": "malformed stanza start",
"allow-?? unknown": "unknown stanza",
} {
_, err := parseStanzas([]string{in})
if err == nil || !strings.HasPrefix(err.Error(), e) {
t.Fatalf("bad error for parseStanzas(%q): got %q, want %q", in, err, e)
}
}
}
func TestBadParseInterfaceStanza(t *testing.T) {
for _, tt := range []struct {
in []string
opts []string
e string
}{
{[]string{}, nil, "incorrect number of attributes"},
{[]string{"eth", "inet", "invalid"}, nil, "invalid config method"},
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"netmask 255.255.255.0"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"address invalid", "netmask 255.255.255.0"}, "malformed static network config"},
{[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100", "netmask invalid"}, "malformed static network config"},
} {
_, err := parseInterfaceStanza(tt.in, tt.opts)
if err == nil || !strings.HasPrefix(err.Error(), tt.e) {
t.Fatalf("bad error parsing interface stanza %q: got %q, want %q", tt.in, err.Error(), tt.e)
}
}
}
func TestBadParseVLANStanzas(t *testing.T) {
conf := configMethodManual{}
options := map[string][]string{}
for _, in := range []string{"myvlan", "eth.vlan"} {
_, err := parseVLANStanza(in, conf, nil, options)
if err == nil || !strings.HasPrefix(err.Error(), "malformed vlan name") {
t.Fatalf("did not error on bad vlan %q", in)
}
}
}
func TestSplitStanzas(t *testing.T) {
expect := [][]string{
{"auto lo"},
{"iface eth1", "option: 1"},
{"mapping"},
{"allow-"},
}
lines := make([]string, 0, 5)
for _, stanza := range expect {
for _, line := range stanza {
lines = append(lines, line)
}
}
stanzas, err := splitStanzas(lines)
if err != nil {
t.FailNow()
}
for i, stanza := range stanzas {
if len(stanza) != len(expect[i]) {
t.FailNow()
}
for j, line := range stanza {
if line != expect[i][j] {
t.FailNow()
}
}
}
}
func TestParseStanzaNil(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatal("parseStanza(nil) did not panic")
}
}()
parseStanza(nil)
}
func TestParseStanzaSuccess(t *testing.T) {
for _, in := range []string{
"auto a",
"iface a inet manual",
} {
if _, err := parseStanza([]string{in}); err != nil {
t.Fatalf("unexpected error parsing stanza %q: %s", in, err)
}
}
}
func TestParseAutoStanza(t *testing.T) {
interfaces := []string{"test", "attribute"}
stanza, err := parseAutoStanza(interfaces, nil)
if err != nil {
t.Fatalf("unexpected error parsing auto stanza %q: %s", interfaces, err)
}
if !reflect.DeepEqual(stanza.interfaces, interfaces) {
t.FailNow()
}
}
func TestParseBondStanzaNoSlaves(t *testing.T) {
bond, err := parseBondStanza("", nil, nil, map[string][]string{})
if err != nil {
t.FailNow()
}
if bond.options["slaves"] != nil {
t.FailNow()
}
}
func TestParseBondStanza(t *testing.T) {
conf := configMethodManual{}
options := map[string][]string{
"bond-slaves": []string{"1", "2"},
}
bond, err := parseBondStanza("test", conf, nil, options)
if err != nil {
t.FailNow()
}
if bond.name != "test" {
t.FailNow()
}
if bond.kind != interfaceBond {
t.FailNow()
}
if bond.configMethod != conf {
t.FailNow()
}
if !reflect.DeepEqual(bond.options["slaves"], options["bond-slaves"]) {
t.FailNow()
}
}
func TestParsePhysicalStanza(t *testing.T) {
conf := configMethodManual{}
options := map[string][]string{
"a": []string{"1", "2"},
"b": []string{"1"},
}
physical, err := parsePhysicalStanza("test", conf, nil, options)
if err != nil {
t.FailNow()
}
if physical.name != "test" {
t.FailNow()
}
if physical.kind != interfacePhysical {
t.FailNow()
}
if physical.configMethod != conf {
t.FailNow()
}
if !reflect.DeepEqual(physical.options, options) {
t.FailNow()
}
}
func TestParseVLANStanzas(t *testing.T) {
conf := configMethodManual{}
options := map[string][]string{}
for _, in := range []string{"vlan25", "eth.25"} {
vlan, err := parseVLANStanza(in, conf, nil, options)
if err != nil {
t.Fatalf("unexpected error from parseVLANStanza(%q): %s", in, err)
}
if !reflect.DeepEqual(vlan.options["id"], []string{"25"}) {
t.FailNow()
}
}
}
func TestParseInterfaceStanzaStaticAddress(t *testing.T) {
options := []string{"address 192.168.1.100", "netmask 255.255.255.0"}
expect := net.IPNet{
IP: net.IPv4(192, 168, 1, 100),
Mask: net.IPv4Mask(255, 255, 255, 0),
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
if err != nil {
t.FailNow()
}
static, ok := iface.configMethod.(configMethodStatic)
if !ok {
t.FailNow()
}
if !reflect.DeepEqual(static.address, expect) {
t.FailNow()
}
}
func TestParseInterfaceStanzaStaticGateway(t *testing.T) {
options := []string{"address 192.168.1.100", "netmask 255.255.255.0", "gateway 192.168.1.1"}
expect := []route{
{
destination: net.IPNet{
IP: net.IPv4(0, 0, 0, 0),
Mask: net.IPv4Mask(0, 0, 0, 0),
},
gateway: net.IPv4(192, 168, 1, 1),
},
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
if err != nil {
t.FailNow()
}
static, ok := iface.configMethod.(configMethodStatic)
if !ok {
t.FailNow()
}
if !reflect.DeepEqual(static.routes, expect) {
t.FailNow()
}
}
func TestParseInterfaceStanzaStaticDNS(t *testing.T) {
options := []string{"address 192.168.1.100", "netmask 255.255.255.0", "dns-nameservers 192.168.1.10 192.168.1.11 192.168.1.12"}
expect := []net.IP{
net.IPv4(192, 168, 1, 10),
net.IPv4(192, 168, 1, 11),
net.IPv4(192, 168, 1, 12),
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
if err != nil {
t.FailNow()
}
static, ok := iface.configMethod.(configMethodStatic)
if !ok {
t.FailNow()
}
if !reflect.DeepEqual(static.nameservers, expect) {
t.FailNow()
}
}
func TestBadParseInterfaceStanzasStaticPostUp(t *testing.T) {
for _, in := range []string{
"post-up invalid",
"post-up route add",
"post-up route add -net",
"post-up route add gw",
"post-up route add netmask",
"gateway",
"gateway 192.168.1.1 192.168.1.2",
} {
options := []string{"address 192.168.1.100", "netmask 255.255.255.0", in}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
if err != nil {
t.Fatalf("parseInterfaceStanza with options %s got unexpected error", options)
}
static, ok := iface.configMethod.(configMethodStatic)
if !ok {
t.Fatalf("parseInterfaceStanza with options %s did not return configMethodStatic", options)
}
if len(static.routes) != 0 {
t.Fatalf("parseInterfaceStanza with options %s did not return zero-length static routes", options)
}
}
}
func TestParseInterfaceStanzaStaticPostUp(t *testing.T) {
options := []string{
"address 192.168.1.100",
"netmask 255.255.255.0",
"post-up route add gw 192.168.1.1 -net 192.168.1.0 netmask 255.255.255.0",
}
expect := []route{
{
destination: net.IPNet{
IP: net.IPv4(192, 168, 1, 0),
Mask: net.IPv4Mask(255, 255, 255, 0),
},
gateway: net.IPv4(192, 168, 1, 1),
},
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "static"}, options)
if err != nil {
t.FailNow()
}
static, ok := iface.configMethod.(configMethodStatic)
if !ok {
t.FailNow()
}
if !reflect.DeepEqual(static.routes, expect) {
t.FailNow()
}
}
func TestParseInterfaceStanzaLoopback(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"eth", "inet", "loopback"}, nil)
if err != nil {
t.FailNow()
}
if _, ok := iface.configMethod.(configMethodLoopback); !ok {
t.FailNow()
}
}
func TestParseInterfaceStanzaManual(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, nil)
if err != nil {
t.FailNow()
}
if _, ok := iface.configMethod.(configMethodManual); !ok {
t.FailNow()
}
}
func TestParseInterfaceStanzaDHCP(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"eth", "inet", "dhcp"}, nil)
if err != nil {
t.FailNow()
}
if _, ok := iface.configMethod.(configMethodDHCP); !ok {
t.FailNow()
}
}
func TestParseInterfaceStanzaPostUpOption(t *testing.T) {
options := []string{
"post-up",
"post-up 1 2",
"post-up 3 4",
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
if err != nil {
t.FailNow()
}
if !reflect.DeepEqual(iface.options["post-up"], []string{"1 2", "3 4"}) {
t.Log(iface.options["post-up"])
t.FailNow()
}
}
func TestParseInterfaceStanzaPreDownOption(t *testing.T) {
options := []string{
"pre-down",
"pre-down 3",
"pre-down 4",
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
if err != nil {
t.FailNow()
}
if !reflect.DeepEqual(iface.options["pre-down"], []string{"3", "4"}) {
t.Log(iface.options["pre-down"])
t.FailNow()
}
}
func TestParseInterfaceStanzaEmptyOption(t *testing.T) {
options := []string{
"test",
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
if err != nil {
t.FailNow()
}
if !reflect.DeepEqual(iface.options["test"], []string{}) {
t.FailNow()
}
}
func TestParseInterfaceStanzaOptions(t *testing.T) {
options := []string{
"test1 1",
"test2 2 3",
"test1 5 6",
}
iface, err := parseInterfaceStanza([]string{"eth", "inet", "manual"}, options)
if err != nil {
t.FailNow()
}
if !reflect.DeepEqual(iface.options["test1"], []string{"5", "6"}) {
t.Log(iface.options["test1"])
t.FailNow()
}
if !reflect.DeepEqual(iface.options["test2"], []string{"2", "3"}) {
t.Log(iface.options["test2"])
t.FailNow()
}
}
func TestParseInterfaceStazaBond(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"mybond", "inet", "manual"}, []string{"bond-slaves eth"})
if err != nil {
t.FailNow()
}
if iface.kind != interfaceBond {
t.FailNow()
}
}
func TestParseInterfaceStazaVLANName(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"eth0.1", "inet", "manual"}, nil)
if err != nil {
t.FailNow()
}
if iface.kind != interfaceVLAN {
t.FailNow()
}
}
func TestParseInterfaceStazaVLANOption(t *testing.T) {
iface, err := parseInterfaceStanza([]string{"vlan1", "inet", "manual"}, []string{"vlan_raw_device eth"})
if err != nil {
t.FailNow()
}
if iface.kind != interfaceVLAN {
t.FailNow()
}
}
func TestParseStanzasNone(t *testing.T) {
stanzas, err := parseStanzas(nil)
if err != nil {
t.FailNow()
}
if len(stanzas) != 0 {
t.FailNow()
}
}
func TestParseStanzas(t *testing.T) {
lines := []string{
"auto lo",
"iface lo inet loopback",
"iface eth1 inet manual",
"iface eth2 inet manual",
"iface eth3 inet manual",
"auto eth1 eth3",
}
expect := []stanza{
&stanzaAuto{
interfaces: []string{"lo"},
},
&stanzaInterface{
name: "lo",
kind: interfacePhysical,
auto: true,
configMethod: configMethodLoopback{},
options: map[string][]string{},
},
&stanzaInterface{
name: "eth1",
kind: interfacePhysical,
auto: true,
configMethod: configMethodManual{},
options: map[string][]string{},
},
&stanzaInterface{
name: "eth2",
kind: interfacePhysical,
auto: false,
configMethod: configMethodManual{},
options: map[string][]string{},
},
&stanzaInterface{
name: "eth3",
kind: interfacePhysical,
auto: true,
configMethod: configMethodManual{},
options: map[string][]string{},
},
&stanzaAuto{
interfaces: []string{"eth1", "eth3"},
},
}
stanzas, err := parseStanzas(lines)
if err != err {
t.FailNow()
}
if !reflect.DeepEqual(stanzas, expect) {
t.FailNow()
}
}

View File

@@ -1,123 +0,0 @@
package pkg
import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
neturl "net/url"
"strings"
"time"
)
const (
HTTP_2xx = 2
HTTP_4xx = 4
)
type HttpClient struct {
// Maximum exp backoff duration. Defaults to 5 seconds
MaxBackoff time.Duration
// Maximum number of connection retries. Defaults to 15
MaxRetries int
// HTTP client timeout, this is suggested to be low since exponential
// backoff will kick off too. Defaults to 2 seconds
Timeout time.Duration
// Whether or not to skip TLS verification. Defaults to false
SkipTLS bool
}
func NewHttpClient() *HttpClient {
return &HttpClient{
MaxBackoff: time.Second * 5,
MaxRetries: 15,
Timeout: time.Duration(2) * time.Second,
SkipTLS: false,
}
}
func expBackoff(interval, max time.Duration) time.Duration {
interval = interval * 2
if interval > max {
interval = max
}
return interval
}
// Fetches a given URL with support for exponential backoff and maximum retries
func (h *HttpClient) Get(rawurl string) ([]byte, error) {
if rawurl == "" {
return nil, errors.New("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("URL %s does not have a valid HTTP scheme. Skipping.", rawurl)
}
dataURL := 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{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: h.SkipTLS,
},
Dial: func(network, addr string) (net.Conn, error) {
deadline := time.Now().Add(h.Timeout)
c, err := net.DialTimeout(network, addr, h.Timeout)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
},
}
client := &http.Client{
Transport: transport,
}
duration := 50 * time.Millisecond
for retry := 1; retry <= h.MaxRetries; retry++ {
log.Printf("Fetching data from %s. Attempt #%d", dataURL, retry)
resp, err := client.Get(dataURL)
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("Not found. HTTP status code: %d", resp.StatusCode)
}
log.Printf("Server error. HTTP status code: %d", resp.StatusCode)
} else {
log.Printf("Unable to fetch data: %s", err.Error())
}
duration = expBackoff(duration, h.MaxBackoff)
log.Printf("Sleeping for %v...", duration)
time.Sleep(duration)
}
return nil, fmt.Errorf("Unable to fetch data. Maximum retries reached: %d", h.MaxRetries)
}

View File

@@ -1,140 +0,0 @@
package pkg
import (
"fmt"
"io"
"math"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestExpBackoff(t *testing.T) {
duration := time.Millisecond
max := time.Hour
for i := 0; i < math.MaxUint16; i++ {
duration = expBackoff(duration, max)
if duration < 0 {
t.Fatalf("duration too small: %v %v", duration, i)
}
if duration > max {
t.Fatalf("duration too large: %v %v", duration, i)
}
}
}
// Test exponential backoff and that it continues retrying if a 5xx response is
// received
func TestGetURLExpBackOff(t *testing.T) {
var expBackoffTests = []struct {
count int
body string
}{
{0, "number of attempts: 0"},
{1, "number of attempts: 1"},
{2, "number of attempts: 2"},
}
client := NewHttpClient()
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 := client.Get(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 TestGetURL4xx(t *testing.T) {
client := NewHttpClient()
retries := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
retries++
http.Error(w, "", 404)
}))
defer ts.Close()
_, err := client.Get(ts.URL)
if err == nil {
t.Errorf("Incorrect result\ngot: %s\nwant: %s", err.Error(), "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 TestGetURL2xx(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
`
client := NewHttpClient()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, cloudcfg)
}))
defer ts.Close()
data, err := client.Get(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 TestGetMalformedURL(t *testing.T) {
client := NewHttpClient()
var tests = []struct {
url string
want string
}{
{"boo", "URL boo does not have a valid HTTP scheme. Skipping."},
{"mailto://boo", "URL mailto://boo does not have a valid HTTP scheme. Skipping."},
{"ftp://boo", "URL ftp://boo does not have a valid HTTP scheme. Skipping."},
{"", "URL is empty. Skipping."},
}
for _, test := range tests {
_, err := client.Get(test.url)
if err == nil || err.Error() != test.want {
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, test.want)
}
}
}

View File

@@ -0,0 +1 @@
../../../

View File

@@ -1,98 +0,0 @@
package system
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
)
type File struct {
Encoding string
Content string
Owner string
Path string
RawFilePermissions string `yaml:"permissions"`
}
func (f *File) Permissions() (os.FileMode, error) {
if f.RawFilePermissions == "" {
return os.FileMode(0644), nil
}
// Parse string representation of file mode as octal
perm, err := strconv.ParseInt(f.RawFilePermissions, 8, 32)
if err != nil {
return 0, errors.New("Unable to parse file permissions as octal integer")
}
return os.FileMode(perm), nil
}
func WriteFile(f *File, root string) (string, error) {
if f.Encoding != "" {
return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
}
fullpath := path.Join(root, f.Path)
dir := path.Dir(fullpath)
if err := EnsureDirectoryExists(dir); err != nil {
return "", err
}
perm, err := f.Permissions()
if err != nil {
return "", err
}
var tmp *os.File
// Create a temporary file in the same directory to ensure it's on the same filesystem
if tmp, err = ioutil.TempFile(dir, "cloudinit-temp"); err != nil {
return "", err
}
if err := ioutil.WriteFile(tmp.Name(), []byte(f.Content), perm); err != nil {
return "", err
}
if err := tmp.Close(); err != nil {
return "", err
}
// Ensure the permissions are as requested (since WriteFile can be affected by sticky bit)
if err := os.Chmod(tmp.Name(), perm); err != nil {
return "", err
}
if f.Owner != "" {
// We shell out since we don't have a way to look up unix groups natively
cmd := exec.Command("chown", f.Owner, tmp.Name())
if err := cmd.Run(); err != nil {
return "", err
}
}
if err := os.Rename(tmp.Name(), fullpath); err != nil {
return "", err
}
return fullpath, nil
}
func EnsureDirectoryExists(dir string) error {
info, err := os.Stat(dir)
if err == nil {
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", dir)
}
} else {
err = os.MkdirAll(dir, 0755)
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,118 +0,0 @@
package system
import (
"io/ioutil"
"os"
"path"
"testing"
)
func TestWriteFileUnencodedContent(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)
fn := "foo"
fullPath := path.Join(dir, fn)
wf := File{
Path: fn,
Content: "bar",
RawFilePermissions: "0644",
}
path, err := WriteFile(&wf, dir)
if err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
} else if path != fullPath {
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
}
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) != "bar" {
t.Fatalf("File has incorrect contents")
}
}
func TestWriteFileInvalidPermission(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)
wf := File{
Path: path.Join(dir, "tmp", "foo"),
Content: "bar",
RawFilePermissions: "pants",
}
if _, err := WriteFile(&wf, dir); err == nil {
t.Fatalf("Expected error to be raised when writing file with invalid permission")
}
}
func TestWriteFilePermissions(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)
fn := "foo"
fullPath := path.Join(dir, fn)
wf := File{
Path: fn,
RawFilePermissions: "0755",
}
path, err := WriteFile(&wf, dir)
if err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
} else if path != fullPath {
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
}
fi, err := os.Stat(fullPath)
if err != nil {
t.Fatalf("Unable to stat file: %v", err)
}
if fi.Mode() != os.FileMode(0755) {
t.Errorf("File has incorrect mode: %v", fi.Mode())
}
}
func TestWriteFileEncodedContent(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)
wf := File{
Path: path.Join(dir, "tmp", "foo"),
Content: "",
Encoding: "base64",
}
if _, err := WriteFile(&wf, dir); err == nil {
t.Fatalf("Expected error to be raised when writing file with encoding")
}
}

View File

@@ -1,89 +0,0 @@
package system
import (
"fmt"
"io/ioutil"
"net"
"os/exec"
"path"
"github.com/coreos/coreos-cloudinit/network"
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
)
const (
runtimeNetworkPath = "/run/systemd/network"
)
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
defer func() {
if e := restartNetworkd(); e != nil {
err = e
}
}()
if err = downNetworkInterfaces(interfaces); err != nil {
return
}
if err = probe8012q(); err != nil {
return
}
return
}
func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error {
sysInterfaceMap := make(map[string]*net.Interface)
if systemInterfaces, err := net.Interfaces(); err == nil {
for _, iface := range systemInterfaces {
sysInterfaceMap[iface.Name] = &iface
}
} else {
return err
}
for _, iface := range interfaces {
if systemInterface, ok := sysInterfaceMap[iface.Name()]; ok {
if err := netlink.NetworkLinkDown(systemInterface); err != nil {
fmt.Printf("Error while downing interface %q (%s). Continuing...\n", systemInterface.Name, err)
}
}
}
return nil
}
func probe8012q() error {
return exec.Command("modprobe", "8021q").Run()
}
func restartNetworkd() error {
_, err := RunUnitCommand("restart", "systemd-networkd.service")
return err
}
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
for _, iface := range interfaces {
filename := path.Join(runtimeNetworkPath, fmt.Sprintf("%s.netdev", iface.Name()))
if err := writeConfig(filename, iface.Netdev()); err != nil {
return err
}
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.link", iface.Name()))
if err := writeConfig(filename, iface.Link()); err != nil {
return err
}
filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.network", iface.Name()))
if err := writeConfig(filename, iface.Network()); err != nil {
return err
}
}
return nil
}
func writeConfig(filename string, config string) error {
if config == "" {
return nil
}
return ioutil.WriteFile(filename, []byte(config), 0444)
}

View File

@@ -1,231 +0,0 @@
package system
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
)
// fakeMachineID is placed on non-usr CoreOS images and should
// 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 {
ext := filepath.Ext(u.Name)
return strings.TrimLeft(ext, ".")
}
func (u *Unit) Group() (group string) {
t := u.Type()
if t == "network" || t == "netdev" || t == "link" {
group = "network"
} else {
group = "system"
}
return
}
type Script []byte
// Destination builds the appropriate absolute file path for
// the Unit. The root argument indicates the effective base
// directory of the system (similar to a chroot).
func (u *Unit) Destination(root string) string {
dir := "etc"
if u.Runtime {
dir = "run"
}
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)
}
}
// 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: filepath.Base(dst),
Content: u.Content,
RawFilePermissions: "0644",
}
_, err := WriteFile(&file, dir)
if err != nil {
return err
}
return nil
}
func EnableUnitFile(unit string, runtime bool) error {
conn, err := dbus.New()
if err != nil {
return err
}
units := []string{unit}
_, _, err = conn.EnableUnitFiles(units, runtime, true)
return err
}
func RunUnitCommand(command, unit string) (string, error) {
conn, err := dbus.New()
if err != nil {
return "", err
}
var fn func(string, string) (string, error)
switch command {
case "start":
fn = conn.StartUnit
case "stop":
fn = conn.StopUnit
case "restart":
fn = conn.RestartUnit
case "reload":
fn = conn.ReloadUnit
case "try-restart":
fn = conn.TryRestartUnit
case "reload-or-restart":
fn = conn.ReloadOrRestartUnit
case "reload-or-try-restart":
fn = conn.ReloadOrTryRestartUnit
default:
return "", fmt.Errorf("Unsupported systemd command %q", command)
}
return fn(unit, "replace")
}
func DaemonReload() error {
conn, err := dbus.New()
if err != nil {
return err
}
return conn.Reload()
}
func ExecuteScript(scriptPath string) (string, error) {
props := []dbus.Property{
dbus.PropDescription("Unit generated and executed by coreos-cloudinit on behalf of user"),
dbus.PropExecStart([]string{"/bin/bash", scriptPath}, false),
}
base := path.Base(scriptPath)
name := fmt.Sprintf("coreos-cloudinit-%s.service", base)
log.Printf("Creating transient systemd unit '%s'", name)
conn, err := dbus.New()
if err != nil {
return "", err
}
_, err = conn.StartTransientUnit(name, "replace", props...)
return name, err
}
func SetHostname(hostname string) error {
return exec.Command("hostnamectl", "set-hostname", hostname).Run()
}
func Hostname() (string, error) {
return os.Hostname()
}
func MachineID(root string) string {
contents, _ := ioutil.ReadFile(path.Join(root, "etc", "machine-id"))
id := strings.TrimSpace(string(contents))
if id == fakeMachineID {
id = ""
}
return id
}
// MaskUnit masks the given Unit by symlinking its unit file to
// /dev/null, analogous to `systemctl mask`.
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
// file at the location*, to ensure that the mask will succeed.
func MaskUnit(unit *Unit, root string) error {
masked := unit.Destination(root)
if _, err := os.Stat(masked); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
return err
}
} else if err := os.Remove(masked); err != nil {
return err
}
return os.Symlink("/dev/null", masked)
}
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
// associated with the given Unit is empty or appears to be a symlink to
// /dev/null, it is removed.
func UnmaskUnit(unit *Unit, root string) error {
masked := unit.Destination(root)
ne, err := nullOrEmpty(masked)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return err
}
if !ne {
log.Printf("%s is not null or empty, refusing to unmask", masked)
return nil
}
return os.Remove(masked)
}
// nullOrEmpty checks whether a given path appears to be an empty regular file
// or a symlink to /dev/null
func nullOrEmpty(path string) (bool, error) {
fi, err := os.Stat(path)
if err != nil {
return false, err
}
m := fi.Mode()
if m.IsRegular() && fi.Size() <= 0 {
return true, nil
}
if m&os.ModeCharDevice > 0 {
return true, nil
}
return false, nil
}

View File

@@ -1,279 +0,0 @@
package system
import (
"io/ioutil"
"os"
"path"
"testing"
)
func TestPlaceNetworkUnit(t *testing.T) {
u := Unit{
Name: "50-eth0.network",
Runtime: true,
Content: `[Match]
Name=eth47
[Network]
Address=10.209.171.177/19
`,
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
dst := u.Destination(dir)
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
if dst != expectDst {
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
}
if err := PlaceUnit(&u, dst); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
fi, err := os.Stat(dst)
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(dst)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expectContents := `[Match]
Name=eth47
[Network]
Address=10.209.171.177/19
`
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 := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
if dst != expectDst {
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
}
u.DropIn = true
dst = u.Destination(dir)
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
if dst != expectDst {
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
}
}
func TestPlaceMountUnit(t *testing.T) {
u := Unit{
Name: "media-state.mount",
Runtime: false,
Content: `[Mount]
What=/dev/sdb1
Where=/media/state
`,
}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
if dst != expectDst {
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
}
if err := PlaceUnit(&u, dst); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
fi, err := os.Stat(dst)
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(dst)
if err != nil {
t.Fatalf("Unable to read expected file: %v", err)
}
expectContents := `[Mount]
What=/dev/sdb1
Where=/media/state
`
if string(contents) != expectContents {
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
}
}
func TestMachineID(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)
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007\n"), os.FileMode(0444))
if MachineID(dir) != "node007" {
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)
// Ensure mask works with units that do not currently exist
uf := &Unit{Name: "foo.service"}
if err := MaskUnit(uf, dir); err != nil {
t.Fatalf("Unable to mask new unit: %v", err)
}
fooPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
fooTgt, err := os.Readlink(fooPath)
if err != nil {
t.Fatalf("Unable to read link", err)
}
if fooTgt != "/dev/null" {
t.Fatalf("unit not masked, got unit target", fooTgt)
}
// Ensure mask works with unit files that already exist
ub := &Unit{Name: "bar.service"}
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
if _, err := os.Create(barPath); err != nil {
t.Fatalf("Error creating new unit file: %v", err)
}
if err := MaskUnit(ub, dir); err != nil {
t.Fatalf("Unable to mask existing unit: %v", err)
}
barTgt, err := os.Readlink(barPath)
if err != nil {
t.Fatalf("Unable to read link", err)
}
if barTgt != "/dev/null" {
t.Fatalf("unit not masked, got unit target", barTgt)
}
}
func TestUnmaskUnit(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)
nilUnit := &Unit{Name: "null.service"}
if err := UnmaskUnit(nilUnit, dir); err != nil {
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
}
uf := &Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}
dst := uf.Destination(dir)
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
t.Fatalf("Unable to create unit directory: %v", err)
}
if _, err := os.Create(dst); err != nil {
t.Fatalf("Unable to write unit file: %v", err)
}
if err := ioutil.WriteFile(dst, []byte(uf.Content), 700); err != nil {
t.Fatalf("Unable to write unit file: %v", err)
}
if err := UnmaskUnit(uf, dir); err != nil {
t.Errorf("unmask of non-empty unit returned unexpected error: %v", err)
}
got, _ := ioutil.ReadFile(dst)
if string(got) != uf.Content {
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
}
ub := &Unit{Name: "bar.service"}
dst = ub.Destination(dir)
if err := os.Symlink("/dev/null", dst); err != nil {
t.Fatalf("Unable to create masked unit: %v", err)
}
if err := UnmaskUnit(ub, dir); err != nil {
t.Errorf("unmask of unit returned unexpected error: %v", err)
}
if _, err := os.Stat(dst); !os.IsNotExist(err) {
t.Errorf("expected %s to not exist after unmask, but got err: %s", err)
}
}
func TestNullOrEmpty(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)
non := path.Join(dir, "does_not_exist")
ne, err := nullOrEmpty(non)
if !os.IsNotExist(err) {
t.Errorf("nullOrEmpty on nonexistent file returned bad error: %v", err)
}
if ne {
t.Errorf("nullOrEmpty returned true unxpectedly")
}
regEmpty := path.Join(dir, "regular_empty_file")
_, err = os.Create(regEmpty)
if err != nil {
t.Fatalf("Unable to create tempfile: %v", err)
}
gotNe, gotErr := nullOrEmpty(regEmpty)
if !gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of regular empty file returned %t, %v - want true, nil", gotNe, gotErr)
}
reg := path.Join(dir, "regular_file")
if err := ioutil.WriteFile(reg, []byte("asdf"), 700); err != nil {
t.Fatalf("Unable to create tempfile: %v", err)
}
gotNe, gotErr = nullOrEmpty(reg)
if gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of regular file returned %t, %v - want false, nil", gotNe, gotErr)
}
null := path.Join(dir, "null")
if err := os.Symlink(os.DevNull, null); err != nil {
t.Fatalf("Unable to create /dev/null link: %s", err)
}
gotNe, gotErr = nullOrEmpty(null)
if !gotNe || gotErr != nil {
t.Errorf("nullOrEmpty of null symlink returned %t, %v - want true, nil", gotNe, gotErr)
}
}

37
test
View File

@@ -1,37 +1,8 @@
#!/bin/bash -e
#
# Run all coreos-cloudinit tests
# ./test
# ./test -v
#
# Run tests for one package
# PKG=initialize ./test
#
# Invoke ./cover for HTML output
COVER=${COVER:-"-cover"}
source ./build
declare -a TESTPKGS=(initialize system datasource pkg network)
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 "Building bin/coreos-cloudinit"
. build
echo "Running tests..."
go test -i ${TESTPKGS}
go test ${COVER} $@ ${TESTPKGS}
echo "Checking gofmt..."
fmtRes=$(gofmt -l $GOFMTPATH)
echo "Success"
go test -i github.com/coreos/coreos-cloudinit/cloudinit
go test -v github.com/coreos/coreos-cloudinit/cloudinit

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
Michael Crosby <michael@crosbymichael.com> (@crosbymichael)
Guillaume J. Charmes <guillaume@docker.com> (@creack)

View File

@@ -1,23 +0,0 @@
// Packet netlink provide access to low level Netlink sockets and messages.
//
// Actual implementations are in:
// netlink_linux.go
// netlink_darwin.go
package netlink
import (
"errors"
"net"
)
var (
ErrWrongSockType = errors.New("Wrong socket type")
ErrShortResponse = errors.New("Got short response from netlink")
)
// A Route is a subnet associated with the interface to reach it.
type Route struct {
*net.IPNet
Iface *net.Interface
Default bool
}

View File

@@ -1,891 +0,0 @@
// +build amd64
package netlink
import (
"encoding/binary"
"fmt"
"math/rand"
"net"
"syscall"
"unsafe"
)
const (
IFNAMSIZ = 16
DEFAULT_CHANGE = 0xFFFFFFFF
IFLA_INFO_KIND = 1
IFLA_INFO_DATA = 2
VETH_INFO_PEER = 1
IFLA_NET_NS_FD = 28
SIOC_BRADDBR = 0x89a0
SIOC_BRADDIF = 0x89a2
)
var nextSeqNr int
type ifreqHwaddr struct {
IfrnName [16]byte
IfruHwaddr syscall.RawSockaddr
}
type ifreqIndex struct {
IfrnName [16]byte
IfruIndex int32
}
func nativeEndian() binary.ByteOrder {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
return binary.BigEndian
}
return binary.LittleEndian
}
func getSeq() int {
nextSeqNr = nextSeqNr + 1
return nextSeqNr
}
func getIpFamily(ip net.IP) int {
if len(ip) <= net.IPv4len {
return syscall.AF_INET
}
if ip.To4() != nil {
return syscall.AF_INET
}
return syscall.AF_INET6
}
type NetlinkRequestData interface {
Len() int
ToWireFormat() []byte
}
type IfInfomsg struct {
syscall.IfInfomsg
}
func newIfInfomsg(family int) *IfInfomsg {
return &IfInfomsg{
IfInfomsg: syscall.IfInfomsg{
Family: uint8(family),
},
}
}
func newIfInfomsgChild(parent *RtAttr, family int) *IfInfomsg {
msg := newIfInfomsg(family)
parent.children = append(parent.children, msg)
return msg
}
func (msg *IfInfomsg) ToWireFormat() []byte {
native := nativeEndian()
length := syscall.SizeofIfInfomsg
b := make([]byte, length)
b[0] = msg.Family
b[1] = 0
native.PutUint16(b[2:4], msg.Type)
native.PutUint32(b[4:8], uint32(msg.Index))
native.PutUint32(b[8:12], msg.Flags)
native.PutUint32(b[12:16], msg.Change)
return b
}
func (msg *IfInfomsg) Len() int {
return syscall.SizeofIfInfomsg
}
type IfAddrmsg struct {
syscall.IfAddrmsg
}
func newIfAddrmsg(family int) *IfAddrmsg {
return &IfAddrmsg{
IfAddrmsg: syscall.IfAddrmsg{
Family: uint8(family),
},
}
}
func (msg *IfAddrmsg) ToWireFormat() []byte {
native := nativeEndian()
length := syscall.SizeofIfAddrmsg
b := make([]byte, length)
b[0] = msg.Family
b[1] = msg.Prefixlen
b[2] = msg.Flags
b[3] = msg.Scope
native.PutUint32(b[4:8], msg.Index)
return b
}
func (msg *IfAddrmsg) Len() int {
return syscall.SizeofIfAddrmsg
}
type RtMsg struct {
syscall.RtMsg
}
func newRtMsg(family int) *RtMsg {
return &RtMsg{
RtMsg: syscall.RtMsg{
Family: uint8(family),
Table: syscall.RT_TABLE_MAIN,
Scope: syscall.RT_SCOPE_UNIVERSE,
Protocol: syscall.RTPROT_BOOT,
Type: syscall.RTN_UNICAST,
},
}
}
func (msg *RtMsg) ToWireFormat() []byte {
native := nativeEndian()
length := syscall.SizeofRtMsg
b := make([]byte, length)
b[0] = msg.Family
b[1] = msg.Dst_len
b[2] = msg.Src_len
b[3] = msg.Tos
b[4] = msg.Table
b[5] = msg.Protocol
b[6] = msg.Scope
b[7] = msg.Type
native.PutUint32(b[8:12], msg.Flags)
return b
}
func (msg *RtMsg) Len() int {
return syscall.SizeofRtMsg
}
func rtaAlignOf(attrlen int) int {
return (attrlen + syscall.RTA_ALIGNTO - 1) & ^(syscall.RTA_ALIGNTO - 1)
}
type RtAttr struct {
syscall.RtAttr
Data []byte
children []NetlinkRequestData
}
func newRtAttr(attrType int, data []byte) *RtAttr {
return &RtAttr{
RtAttr: syscall.RtAttr{
Type: uint16(attrType),
},
children: []NetlinkRequestData{},
Data: data,
}
}
func newRtAttrChild(parent *RtAttr, attrType int, data []byte) *RtAttr {
attr := newRtAttr(attrType, data)
parent.children = append(parent.children, attr)
return attr
}
func (a *RtAttr) Len() int {
l := 0
for _, child := range a.children {
l += child.Len() + syscall.SizeofRtAttr
}
if l == 0 {
l++
}
return rtaAlignOf(l + len(a.Data))
}
func (a *RtAttr) ToWireFormat() []byte {
native := nativeEndian()
length := a.Len()
buf := make([]byte, rtaAlignOf(length+syscall.SizeofRtAttr))
if a.Data != nil {
copy(buf[4:], a.Data)
} else {
next := 4
for _, child := range a.children {
childBuf := child.ToWireFormat()
copy(buf[next:], childBuf)
next += rtaAlignOf(len(childBuf))
}
}
if l := uint16(rtaAlignOf(length)); l != 0 {
native.PutUint16(buf[0:2], l+1)
}
native.PutUint16(buf[2:4], a.Type)
return buf
}
type NetlinkRequest struct {
syscall.NlMsghdr
Data []NetlinkRequestData
}
func (rr *NetlinkRequest) ToWireFormat() []byte {
native := nativeEndian()
length := rr.Len
dataBytes := make([][]byte, len(rr.Data))
for i, data := range rr.Data {
dataBytes[i] = data.ToWireFormat()
length += uint32(len(dataBytes[i]))
}
b := make([]byte, length)
native.PutUint32(b[0:4], length)
native.PutUint16(b[4:6], rr.Type)
native.PutUint16(b[6:8], rr.Flags)
native.PutUint32(b[8:12], rr.Seq)
native.PutUint32(b[12:16], rr.Pid)
next := 16
for _, data := range dataBytes {
copy(b[next:], data)
next += len(data)
}
return b
}
func (rr *NetlinkRequest) AddData(data NetlinkRequestData) {
if data != nil {
rr.Data = append(rr.Data, data)
}
}
func newNetlinkRequest(proto, flags int) *NetlinkRequest {
return &NetlinkRequest{
NlMsghdr: syscall.NlMsghdr{
Len: uint32(syscall.NLMSG_HDRLEN),
Type: uint16(proto),
Flags: syscall.NLM_F_REQUEST | uint16(flags),
Seq: uint32(getSeq()),
},
}
}
type NetlinkSocket struct {
fd int
lsa syscall.SockaddrNetlink
}
func getNetlinkSocket() (*NetlinkSocket, error) {
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_ROUTE)
if err != nil {
return nil, err
}
s := &NetlinkSocket{
fd: fd,
}
s.lsa.Family = syscall.AF_NETLINK
if err := syscall.Bind(fd, &s.lsa); err != nil {
syscall.Close(fd)
return nil, err
}
return s, nil
}
func (s *NetlinkSocket) Close() {
syscall.Close(s.fd)
}
func (s *NetlinkSocket) Send(request *NetlinkRequest) error {
if err := syscall.Sendto(s.fd, request.ToWireFormat(), 0, &s.lsa); err != nil {
return err
}
return nil
}
func (s *NetlinkSocket) Receive() ([]syscall.NetlinkMessage, error) {
rb := make([]byte, syscall.Getpagesize())
nr, _, err := syscall.Recvfrom(s.fd, rb, 0)
if err != nil {
return nil, err
}
if nr < syscall.NLMSG_HDRLEN {
return nil, ErrShortResponse
}
rb = rb[:nr]
return syscall.ParseNetlinkMessage(rb)
}
func (s *NetlinkSocket) GetPid() (uint32, error) {
lsa, err := syscall.Getsockname(s.fd)
if err != nil {
return 0, err
}
switch v := lsa.(type) {
case *syscall.SockaddrNetlink:
return v.Pid, nil
}
return 0, ErrWrongSockType
}
func (s *NetlinkSocket) HandleAck(seq uint32) error {
native := nativeEndian()
pid, err := s.GetPid()
if err != nil {
return err
}
done:
for {
msgs, err := s.Receive()
if err != nil {
return err
}
for _, m := range msgs {
if m.Header.Seq != seq {
return fmt.Errorf("Wrong Seq nr %d, expected %d", m.Header.Seq, seq)
}
if m.Header.Pid != pid {
return fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid)
}
if m.Header.Type == syscall.NLMSG_DONE {
break done
}
if m.Header.Type == syscall.NLMSG_ERROR {
error := int32(native.Uint32(m.Data[0:4]))
if error == 0 {
break done
}
return syscall.Errno(-error)
}
}
}
return nil
}
// Add a new default gateway. Identical to:
// ip route add default via $ip
func AddDefaultGw(ip net.IP) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
family := getIpFamily(ip)
wb := newNetlinkRequest(syscall.RTM_NEWROUTE, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
msg := newRtMsg(family)
wb.AddData(msg)
var ipData []byte
if family == syscall.AF_INET {
ipData = ip.To4()
} else {
ipData = ip.To16()
}
gateway := newRtAttr(syscall.RTA_GATEWAY, ipData)
wb.AddData(gateway)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// Bring up a particular network interface
func NetworkLinkUp(iface *net.Interface) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Change = syscall.IFF_UP
msg.Flags = syscall.IFF_UP
msg.Index = int32(iface.Index)
wb.AddData(msg)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func NetworkLinkDown(iface *net.Interface) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Change = syscall.IFF_UP
msg.Flags = 0 & ^syscall.IFF_UP
msg.Index = int32(iface.Index)
wb.AddData(msg)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func NetworkSetMTU(iface *net.Interface, mtu int) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Type = syscall.RTM_SETLINK
msg.Flags = syscall.NLM_F_REQUEST
msg.Index = int32(iface.Index)
msg.Change = DEFAULT_CHANGE
wb.AddData(msg)
var (
b = make([]byte, 4)
native = nativeEndian()
)
native.PutUint32(b, uint32(mtu))
data := newRtAttr(syscall.IFLA_MTU, b)
wb.AddData(data)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// same as ip link set $name master $master
func NetworkSetMaster(iface, master *net.Interface) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Type = syscall.RTM_SETLINK
msg.Flags = syscall.NLM_F_REQUEST
msg.Index = int32(iface.Index)
msg.Change = DEFAULT_CHANGE
wb.AddData(msg)
var (
b = make([]byte, 4)
native = nativeEndian()
)
native.PutUint32(b, uint32(master.Index))
data := newRtAttr(syscall.IFLA_MASTER, b)
wb.AddData(data)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func NetworkSetNsPid(iface *net.Interface, nspid int) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Type = syscall.RTM_SETLINK
msg.Flags = syscall.NLM_F_REQUEST
msg.Index = int32(iface.Index)
msg.Change = DEFAULT_CHANGE
wb.AddData(msg)
var (
b = make([]byte, 4)
native = nativeEndian()
)
native.PutUint32(b, uint32(nspid))
data := newRtAttr(syscall.IFLA_NET_NS_PID, b)
wb.AddData(data)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func NetworkSetNsFd(iface *net.Interface, fd int) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Type = syscall.RTM_SETLINK
msg.Flags = syscall.NLM_F_REQUEST
msg.Index = int32(iface.Index)
msg.Change = DEFAULT_CHANGE
wb.AddData(msg)
var (
b = make([]byte, 4)
native = nativeEndian()
)
native.PutUint32(b, uint32(fd))
data := newRtAttr(IFLA_NET_NS_FD, b)
wb.AddData(data)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// Add an Ip address to an interface. This is identical to:
// ip addr add $ip/$ipNet dev $iface
func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
family := getIpFamily(ip)
wb := newNetlinkRequest(syscall.RTM_NEWADDR, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
msg := newIfAddrmsg(family)
msg.Index = uint32(iface.Index)
prefixLen, _ := ipNet.Mask.Size()
msg.Prefixlen = uint8(prefixLen)
wb.AddData(msg)
var ipData []byte
if family == syscall.AF_INET {
ipData = ip.To4()
} else {
ipData = ip.To16()
}
localData := newRtAttr(syscall.IFA_LOCAL, ipData)
wb.AddData(localData)
addrData := newRtAttr(syscall.IFA_ADDRESS, ipData)
wb.AddData(addrData)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
func zeroTerminated(s string) []byte {
return []byte(s + "\000")
}
func nonZeroTerminated(s string) []byte {
return []byte(s)
}
// Add a new network link of a specified type. This is identical to
// running: ip add link $name type $linkType
func NetworkLinkAdd(name string, linkType string) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
wb.AddData(msg)
if name != "" {
nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name))
wb.AddData(nameData)
}
kindData := newRtAttr(IFLA_INFO_KIND, nonZeroTerminated(linkType))
infoData := newRtAttr(syscall.IFLA_LINKINFO, kindData.ToWireFormat())
wb.AddData(infoData)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// Returns an array of IPNet for all the currently routed subnets on ipv4
// This is similar to the first column of "ip route" output
func NetworkGetRoutes() ([]Route, error) {
native := nativeEndian()
s, err := getNetlinkSocket()
if err != nil {
return nil, err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_GETROUTE, syscall.NLM_F_DUMP)
msg := newIfInfomsg(syscall.AF_UNSPEC)
wb.AddData(msg)
if err := s.Send(wb); err != nil {
return nil, err
}
pid, err := s.GetPid()
if err != nil {
return nil, err
}
res := make([]Route, 0)
done:
for {
msgs, err := s.Receive()
if err != nil {
return nil, err
}
for _, m := range msgs {
if m.Header.Seq != wb.Seq {
return nil, fmt.Errorf("Wrong Seq nr %d, expected 1", m.Header.Seq)
}
if m.Header.Pid != pid {
return nil, fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid)
}
if m.Header.Type == syscall.NLMSG_DONE {
break done
}
if m.Header.Type == syscall.NLMSG_ERROR {
error := int32(native.Uint32(m.Data[0:4]))
if error == 0 {
break done
}
return nil, syscall.Errno(-error)
}
if m.Header.Type != syscall.RTM_NEWROUTE {
continue
}
var r Route
msg := (*RtMsg)(unsafe.Pointer(&m.Data[0:syscall.SizeofRtMsg][0]))
if msg.Flags&syscall.RTM_F_CLONED != 0 {
// Ignore cloned routes
continue
}
if msg.Table != syscall.RT_TABLE_MAIN {
// Ignore non-main tables
continue
}
if msg.Family != syscall.AF_INET {
// Ignore non-ipv4 routes
continue
}
if msg.Dst_len == 0 {
// Default routes
r.Default = true
}
attrs, err := syscall.ParseNetlinkRouteAttr(&m)
if err != nil {
return nil, err
}
for _, attr := range attrs {
switch attr.Attr.Type {
case syscall.RTA_DST:
ip := attr.Value
r.IPNet = &net.IPNet{
IP: ip,
Mask: net.CIDRMask(int(msg.Dst_len), 8*len(ip)),
}
case syscall.RTA_OIF:
index := int(native.Uint32(attr.Value[0:4]))
r.Iface, _ = net.InterfaceByIndex(index)
}
}
if r.Default || r.IPNet != nil {
res = append(res, r)
}
}
}
return res, nil
}
func getIfSocket() (fd int, err error) {
for _, socket := range []int{
syscall.AF_INET,
syscall.AF_PACKET,
syscall.AF_INET6,
} {
if fd, err = syscall.Socket(socket, syscall.SOCK_DGRAM, 0); err == nil {
break
}
}
if err == nil {
return fd, nil
}
return -1, err
}
func NetworkChangeName(iface *net.Interface, newName string) error {
fd, err := getIfSocket()
if err != nil {
return err
}
defer syscall.Close(fd)
data := [IFNAMSIZ * 2]byte{}
// the "-1"s here are very important for ensuring we get proper null
// termination of our new C strings
copy(data[:IFNAMSIZ-1], iface.Name)
copy(data[IFNAMSIZ:IFNAMSIZ*2-1], newName)
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.SIOCSIFNAME, uintptr(unsafe.Pointer(&data[0]))); errno != 0 {
return errno
}
return nil
}
func NetworkCreateVethPair(name1, name2 string) error {
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
wb.AddData(msg)
nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name1))
wb.AddData(nameData)
nest1 := newRtAttr(syscall.IFLA_LINKINFO, nil)
newRtAttrChild(nest1, IFLA_INFO_KIND, zeroTerminated("veth"))
nest2 := newRtAttrChild(nest1, IFLA_INFO_DATA, nil)
nest3 := newRtAttrChild(nest2, VETH_INFO_PEER, nil)
newIfInfomsgChild(nest3, syscall.AF_UNSPEC)
newRtAttrChild(nest3, syscall.IFLA_IFNAME, zeroTerminated(name2))
wb.AddData(nest1)
if err := s.Send(wb); err != nil {
return err
}
return s.HandleAck(wb.Seq)
}
// Create the actual bridge device. This is more backward-compatible than
// netlink.NetworkLinkAdd and works on RHEL 6.
func CreateBridge(name string, setMacAddr bool) error {
s, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
if err != nil {
// ipv6 issue, creating with ipv4
s, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
if err != nil {
return err
}
}
defer syscall.Close(s)
nameBytePtr, err := syscall.BytePtrFromString(name)
if err != nil {
return err
}
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), SIOC_BRADDBR, uintptr(unsafe.Pointer(nameBytePtr))); err != 0 {
return err
}
if setMacAddr {
return setBridgeMacAddress(s, name)
}
return nil
}
// Add a slave to abridge device. This is more backward-compatible than
// netlink.NetworkSetMaster and works on RHEL 6.
func AddToBridge(iface, master *net.Interface) error {
s, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
if err != nil {
// ipv6 issue, creating with ipv4
s, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
if err != nil {
return err
}
}
defer syscall.Close(s)
ifr := ifreqIndex{}
copy(ifr.IfrnName[:], master.Name)
ifr.IfruIndex = int32(iface.Index)
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), SIOC_BRADDIF, uintptr(unsafe.Pointer(&ifr))); err != 0 {
return err
}
return nil
}
func setBridgeMacAddress(s int, name string) error {
ifr := ifreqHwaddr{}
ifr.IfruHwaddr.Family = syscall.ARPHRD_ETHER
copy(ifr.IfrnName[:], name)
for i := 0; i < 6; i++ {
ifr.IfruHwaddr.Data[i] = int8(rand.Intn(255))
}
ifr.IfruHwaddr.Data[0] &^= 0x1 // clear multicast bit
ifr.IfruHwaddr.Data[0] |= 0x2 // set local assignment bit (IEEE802)
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), syscall.SIOCSIFHWADDR, uintptr(unsafe.Pointer(&ifr))); err != 0 {
return err
}
return nil
}

View File

@@ -1,69 +0,0 @@
// +build !linux !amd64
package netlink
import (
"errors"
"net"
)
var (
ErrNotImplemented = errors.New("not implemented")
)
func NetworkGetRoutes() ([]Route, error) {
return nil, ErrNotImplemented
}
func NetworkLinkAdd(name string, linkType string) error {
return ErrNotImplemented
}
func NetworkLinkUp(iface *net.Interface) error {
return ErrNotImplemented
}
func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
return ErrNotImplemented
}
func AddDefaultGw(ip net.IP) error {
return ErrNotImplemented
}
func NetworkSetMTU(iface *net.Interface, mtu int) error {
return ErrNotImplemented
}
func NetworkCreateVethPair(name1, name2 string) error {
return ErrNotImplemented
}
func NetworkChangeName(iface *net.Interface, newName string) error {
return ErrNotImplemented
}
func NetworkSetNsFd(iface *net.Interface, fd int) error {
return ErrNotImplemented
}
func NetworkSetNsPid(iface *net.Interface, nspid int) error {
return ErrNotImplemented
}
func NetworkSetMaster(iface, master *net.Interface) error {
return ErrNotImplemented
}
func NetworkLinkDown(iface *net.Interface) error {
return ErrNotImplemented
}
func CreateBridge(name string, setMacAddr bool) error {
return ErrNotImplemented
}
func AddToBridge(iface, master *net.Interface) error {
return ErrNotImplemented
}

View File

@@ -1,11 +0,0 @@
# 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

@@ -1,11 +0,0 @@
[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

@@ -1,14 +0,0 @@
[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

@@ -1,11 +0,0 @@
[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

@@ -1,10 +0,0 @@
[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

@@ -1,12 +0,0 @@
[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

@@ -1,5 +0,0 @@
[Unit]
Description=Watch for a cloud-config at %f
[Path]
PathExists=%f

View File

@@ -1,12 +0,0 @@
[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

@@ -1,13 +0,0 @@
[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-configdrive.path
After=user-configdrive.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

View File

@@ -1,5 +0,0 @@
[Unit]
Description=Watch for a cloud-config at /media/configdrive
[Path]
DirectoryNotEmpty=/media/configdrive

View File

@@ -1,23 +0,0 @@
[Unit]
Description=Load cloud-config from /media/configdrive
Requires=coreos-setup-environment.service
After=coreos-setup-environment.service
Before=user-config.target
# 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-configdrive=/media/configdrive