Compare commits

..

64 Commits

Author SHA1 Message Date
Michael Marineau
d0a6d6f92f chore(coreos-cloudinit): bump to 0.7.7 2014-06-18 14:55:38 -07:00
Michael Marineau
2be1e52f32 Merge pull request #151 from marineam/mount
fix(configdrive): Use mount units, give virtfs a new mount point.
2014-06-18 13:51:11 -07:00
Michael Marineau
784a71e2bf fix(configdrive): Use mount units, give virtfs a new mount point.
Currently systemd cannot track dependencies on configdrive very well
because it is mounted via a service instead of a mount unit. Also since
the interaction between path and mount units can lead to unexpected
behavior if something goes wrong the cloudinit service is now triggered
explicitly by the mount again. The configdrive path unit remains only as
a fall back for containers where the mount unit doesn't kick in. Better
to have two mechanisms that trigger the cloudinit service than none. :)

Since mounting a virtfs based configdrive requires different mount
options and two different mount units cannot refer to the same path the
virtfs version now mounts to /media/configvirtfs.

There are also two new kernel options:
- `coreos.configdrive=1`: enable config drive on physical hardware.
- `coreos.configdrive=0`: disable config drive on virtual machines.
2014-06-18 13:01:19 -07:00
Michael Marineau
160668284c chore(coreos-cloudinit): bump to 0.7.6+git 2014-06-07 16:04:33 -04:00
Michael Marineau
41b9dfcb1c chore(coreos-cloudinit): bump to 0.7.6 2014-06-07 16:01:31 -04:00
Michael Marineau
ef4c3483b6 Merge pull request #146 from marineam/fix
fix(update): Fix restart of update-engine
2014-06-07 13:04:49 -07:00
Michael Marineau
4bdf633075 fix(update): Fix restart of update-engine
The name was missing .service.
2014-06-07 12:08:22 -07:00
Brian Waldon
c9fc718e18 Merge pull request #145 from bcwaldon/drop-group-req
Relax requirements of update group value
2014-06-06 11:43:22 -07:00
Brian Waldon
4461b3d33d fix(update): Relax requirements of update group value 2014-06-06 11:29:09 -07:00
Jonathan Boulle
c6a1412f6b chore(coreos-cloudinit): bump to 0.7.5+git 2014-06-06 11:14:39 -07:00
Jonathan Boulle
d0cbbd2007 chore(coreos-cloudinit): bump to 0.7.5 2014-06-06 11:10:48 -07:00
Jonathan Boulle
7b5e542eb4 Merge pull request #132 from jonboulle/locksmith
reboot-strategy=off breaks subsequent reboot strategies
2014-06-06 11:08:06 -07:00
Jonathan Boulle
376d82ba63 doc(*): add note about runtime locksmithd unit file 2014-06-06 10:55:42 -07:00
Jonathan Boulle
a6aa9f82b8 fix(systemd): unmask runtime units when mask=False 2014-06-06 10:55:42 -07:00
Jonathan Boulle
00ee047753 fix(locksmith): use a runtime unit for locksmith 2014-06-06 10:55:42 -07:00
Jonathan Boulle
f127406d01 Merge pull request #140 from jonboulle/atomic
fix(system): write all files atomically
2014-06-06 10:37:09 -07:00
Jonathan Boulle
0ddc08d55a fix(system): write all files atomically 2014-06-06 10:36:36 -07:00
Jonathan Boulle
56f455f890 Merge pull request #141 from jonboulle/141
cloudinit doesn't restart update-engine.service
2014-06-06 10:25:24 -07:00
Jonathan Boulle
dd861b9f88 fix(initialize): ensure update-engine is restarted after group/server
changes
2014-06-05 16:12:40 -07:00
Alex Crawford
f7d01da267 Merge pull request #138 from spkane/github-ent-key-docs
Add a valid URL example for Github Enterprise token based API auth
2014-06-04 16:15:04 -07:00
Sean P. Kane
fc8f30bf08 Add a valid URL example for Github Enterprise token based API auth 2014-06-04 16:03:02 -07:00
Brandon Philips
075c0557e7 Merge pull request #137 from robszumski/patch-1
fix(docs): remove unneeded install section
2014-06-04 14:22:55 -07:00
Rob Szumski
d25e13a2c6 fix(docs): remove unneeded install section 2014-06-04 13:57:18 -07:00
Alex Crawford
cf1ffad533 chore(coreos-cloudinit): bump to 0.7.4+git 2014-06-03 14:14:47 -07:00
Alex Crawford
82706b1d5f chore(coreos-cloudinit): bump to 0.7.4 2014-06-03 14:13:56 -07:00
Alex Crawford
38c8fda0d1 Merge pull request #124 from crawford/networkd
feat(networkd): Adding support for debian-interface-to-networkd conversion
2014-06-03 13:55:06 -07:00
Alex Crawford
69240a7e39 feat(systemd): Update the systemd unit files to use configdrive
This makes it so that /media/configdrive can be used for user-data
and network configs.
2014-06-02 18:43:22 -07:00
Brian Waldon
c4f1996843 fix(doc): Correct spacing in cloud-config.md 2014-06-02 16:49:44 -07:00
Alex Crawford
48df1be793 feat(convertNetconf): Add support for network config conversion
Adding the flag -convertNetconf which is used to specify the config
format to convert from (right now, only 'debian' is supported).
Once the network configs are generated, they are written to
systemd's runtime network directory and the network is restarted.
2014-06-02 15:31:30 -07:00
Alex Crawford
79a40a38d8 add(netlink): import dotcloud/docker/pkg/netlink 2014-06-02 15:31:30 -07:00
Alex Crawford
856061b445 test(interfaces): Add tests for network conversion
These tests should be an exhaustive set of tests for the parsing
of Debian interface files and generation of equivilent networkd
config files.
2014-06-02 15:31:27 -07:00
Alex Crawford
38321fedce feat(interfaces): Add support for interfaces file
This adds the ability for cloudinit to parse a debian interfaces
file and generate the coresponding networkd configs.
2014-06-02 15:30:37 -07:00
Alex Crawford
f8a823cf7e refactor(userdata): Move userdata processing into a function 2014-06-02 14:59:01 -07:00
Alex Crawford
a4035cffea feat(config-drive): Add support for reading user-data from config-drive
The -config-drive flag tells cloudinit to read the user-data from
within the config-drive (./openstack/latest/user-data).
2014-06-02 14:58:57 -07:00
Brian Waldon
5c8fb7f465 fix(doc): Add newlines for proper formatting 2014-06-02 11:42:43 -07:00
Alex Crawford
7a02bf54ed Merge pull request #130 from crawford/docs
fix(docs): Fix minor typo describing runtime field for units
2014-05-30 11:52:30 -07:00
Alex Crawford
388dd67388 fix(docs): Fix minor typo describing runtime field for units 2014-05-30 11:45:44 -07:00
Jonathan Boulle
ded6d94180 chore(coreos-cloudinit): bump to 0.7.3+git 2014-05-29 14:55:34 -07:00
Jonathan Boulle
a9a910b5c4 chore(coreos-cloudinit): bump to 0.7.3 2014-05-29 14:52:58 -07:00
Jonathan Boulle
8e94b4140a Merge pull request #122 from jonboulle/122
ec2-cloudinit service fails after reboot with "reboot-strategy: off"
2014-05-29 14:25:58 -07:00
Jonathan Boulle
cd322863e9 Merge pull request #129 from jonboulle/exp
fix(pkg): simplify exponential backoff to avoid overflows
2014-05-29 14:02:47 -07:00
Jonathan Boulle
786e4bef65 fix(systemd): remove any existing unit when calling mask 2014-05-29 13:59:55 -07:00
Jonathan Boulle
269a658d4b fix(pkg): simplify exponential backoff to avoid overflows 2014-05-29 11:11:18 -07:00
Michael Marineau
e317c7eb9a chore(coreos-cloudinit): bump to 0.7.2+git 2014-05-27 14:02:11 -07:00
Michael Marineau
974de943e0 chore(coreos-cloudinit): bump to 0.7.2 2014-05-27 13:37:58 -07:00
Jonathan Boulle
db3f008543 Merge pull request #127 from jonboulle/127
"Enable" option does not support units in /usr/lib64/systemd
2014-05-26 15:24:30 -07:00
Jonathan Boulle
b04509ae54 fix(systemd): EnableUnitFile unit name rather than absolute destination 2014-05-26 15:16:24 -07:00
Jonathan Boulle
6c07e8784f Merge pull request #125 from jonboulle/no_locksmith_enable
Dies trying to enable non-existent /etc/systemd/system/locksmithd.service
2014-05-26 13:11:47 -07:00
Jonathan Boulle
60ab4222de fix(update): locksmith service does not need disabling/enabling 2014-05-26 12:33:23 -07:00
Brandon Philips
1a295f65c7 Merge pull request #123 from c4milo/shared-http-client
feat(util/http_client): Adds generic HTTP client
2014-05-22 14:37:32 -07:00
Camilo Aguilar
cec0926c5c fix(pkg/http_client): Printf is smarter than you think
Printf determines what the duration unit is
and prints it accordingly.
2014-05-22 14:53:54 -04:00
Camilo Aguilar
8ca3c2ed1f style(httpbackoff -> pkg): Adjusts package name to follow convention 2014-05-22 14:37:19 -04:00
Camilo Aguilar
2cedebb4eb style(util->httpbackoff): Changes package as per @philips suggestion 2014-05-21 21:12:16 -04:00
Camilo Aguilar
3e00a37ef5 feat(util/http_client): Adds generic HTTP client
Supports retries with exponential backoff as well as connection
timeouts and the ability to skip SSL/TLS verification.

This commit also refactors datasource and initialize packages
in order to use the new HTTP client.
2014-05-21 13:31:50 -04:00
Jonathan Boulle
59d1eba423 Merge pull request #111 from namsral/patch-1
Trim newlines from the cloud-config-url option
2014-05-21 10:18:24 -07:00
Jonathan Boulle
af69149260 Merge pull request #120 from brianredbeard/pr20-fix
fix(docs) Clear description of update server changes
2014-05-21 10:01:25 -07:00
Brandon Philips
5fa2ad8dfd Merge pull request #121 from iamveen/master
removed tricky space from cloud-config header
2014-05-21 05:33:05 -07:00
Lars Wiegman
513a1eb602 Trim newlines from the cloud-config-url kernel parameter and added a test
- In the Fetch function trim whitespace from /proc/cmdline
- New test for Fetch function
- Added Location field to the procCmdline struct for testing
2014-05-21 11:09:39 +02:00
Gavin Dunne
5189e1594e removed tricky space from cloud-config header 2014-05-21 01:22:09 -07:00
Brian 'Redbeard' Harrington
8b5bc47429 fix(doc) more sensible ordering
It makes a bit more sense to specify the scope of the section
before getting into details about how it's done.
2014-05-20 23:29:56 -07:00
Brian 'Redbeard' Harrington
a64fcd2893 fix(docs) Clear description of update server changes TBD
Pulling in @philips' changes from coreos/coreos-cloudinit#6 after
trashing PR coreos/coreos-cloudinit#20.  Cleanup of that PR was
beyond my git-fu.

cc @jonboulle
2014-05-20 22:53:29 -07:00
Brandon Philips
5b1145c044 Merge pull request #118 from c4milo/log-timestamp-fix
chore(logging): Removes duplicated timestamp during booting
2014-05-17 16:31:07 -07:00
Michael Marineau
a49877b99f chore(coreos-cloudinit): bump to 0.7.1+git 2014-05-16 21:23:34 -07:00
Camilo Aguilar
5d58c6c1c1 chore(logging): Removes duplicated timestamp during booting 2014-05-16 17:35:31 -04:00
48 changed files with 3263 additions and 338 deletions

2
.gitignore vendored
View File

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

View File

@@ -73,16 +73,18 @@ Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
#### 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
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"
@@ -97,11 +99,20 @@ For more information on fleet configuration, see the [fleet documentation][fleet
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
@@ -115,10 +126,11 @@ coreos:
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 `systemd enable`. Default value is false.
- **runtime**: Boolean indicating whether or not to persist the unit across reboots. This is analogous to the `--runtime` argument to `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.
@@ -143,15 +155,12 @@ coreos:
Restart=always
ExecStart=/usr/bin/docker start -a redis_server
ExecStop=/usr/bin/docker stop -t 2 redis_server
[Install]
WantedBy=local.target
```
Start the built-in `etcd` and `fleet` services:
```
# cloud-config
#cloud-config
coreos:
units:
@@ -270,7 +279,7 @@ For example, if you have an installation of GitHub Enterprise, you can provide a
users:
- name: elroy
coreos-ssh-import-url: https://token:<OAUTH-TOKEN>@github-enterprise.example.com/users/elroy/keys
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:

7
build
View File

@@ -3,7 +3,12 @@
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}
export GOPATH=${PWD}/gopath
go build -o bin/coreos-cloudinit ${REPO_PATH}

View File

@@ -1,17 +1,20 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"io/ioutil"
"os"
"path"
"github.com/coreos/coreos-cloudinit/datasource"
"github.com/coreos/coreos-cloudinit/initialize"
"github.com/coreos/coreos-cloudinit/network"
"github.com/coreos/coreos-cloudinit/system"
)
const version = "0.7.1"
const version = "0.7.7"
func main() {
var printVersion bool
@@ -23,12 +26,18 @@ func main() {
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")
@@ -47,17 +56,32 @@ func main() {
ds = datasource.NewLocalFile(file)
} 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-url or --from-proc-cmdline")
fmt.Println("Provide one of --from-file, --from-configdrive, --from-url or --from-proc-cmdline")
os.Exit(1)
}
log.Printf("Fetching user-data from datasource of type %q", ds.Type())
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 {
log.Printf("Failed fetching user-data from datasource: %v", err)
fmt.Printf("Failed fetching user-data from datasource: %v\n", err)
if ignoreFailure {
os.Exit(0)
} else {
@@ -65,29 +89,41 @@ func main() {
}
}
if len(userdataBytes) == 0 {
log.Printf("No user data to handle, exiting.")
os.Exit(0)
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)
}
}
} else {
fmt.Println("No user data to handle.")
}
env := initialize.NewEnvironment("/", workspace)
if convertNetconf != "" {
if err := processNetconf(convertNetconf, configdrive); err != nil {
fmt.Printf("Failed to process network config: %v\n", err)
if !ignoreFailure {
os.Exit(1)
}
}
}
}
userdata := string(userdataBytes)
func processUserdata(userdata string, env *initialize.Environment) error {
userdata = env.Apply(userdata)
parsed, err := initialize.ParseUserData(userdata)
if err != nil {
log.Printf("Failed parsing user-data: %v", err)
if ignoreFailure {
os.Exit(0)
} else {
os.Exit(1)
}
fmt.Printf("Failed parsing user-data: %v\n", err)
return err
}
err = initialize.PrepWorkspace(env.Workspace())
if err != nil {
log.Fatalf("Failed preparing workspace: %v", err)
fmt.Printf("Failed preparing workspace: %v\n", err)
return err
}
switch t := parsed.(type) {
@@ -99,11 +135,54 @@ func main() {
if err == nil {
var name string
name, err = system.ExecuteScript(path)
initialize.PersistUnitNameInWorkspace(name, workspace)
initialize.PersistUnitNameInWorkspace(name, env.Workspace())
}
}
if err != nil {
log.Fatalf("Failed resolving user-data: %v", err)
}
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)
}

27
datasource/configdrive.go Normal file
View File

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

View File

@@ -1,5 +1,7 @@
package datasource
import "github.com/coreos/coreos-cloudinit/pkg"
type metadataService struct {
url string
}
@@ -9,7 +11,8 @@ func NewMetadataService(url string) *metadataService {
}
func (ms *metadataService) Fetch() ([]byte, error) {
return fetchURL(ms.url)
client := pkg.NewHttpClient()
return client.Get(ms.url)
}
func (ms *metadataService) Type() string {

View File

@@ -5,6 +5,8 @@ import (
"io/ioutil"
"log"
"strings"
"github.com/coreos/coreos-cloudinit/pkg"
)
const (
@@ -12,24 +14,28 @@ const (
ProcCmdlineCloudConfigFlag = "cloud-config-url"
)
type procCmdline struct{}
type procCmdline struct{
Location string
}
func NewProcCmdline() *procCmdline {
return &procCmdline{}
return &procCmdline{Location: ProcCmdlineLocation}
}
func (self *procCmdline) Fetch() ([]byte, error) {
cmdline, err := ioutil.ReadFile(ProcCmdlineLocation)
contents, err := ioutil.ReadFile(self.Location)
if err != nil {
return nil, err
}
url, err := findCloudConfigURL(string(cmdline))
cmdline := strings.TrimSpace(string(contents))
url, err := findCloudConfigURL(cmdline)
if err != nil {
return nil, err
}
cfg, err := fetchURL(url)
client := pkg.NewHttpClient()
cfg, err := client.Get(url)
if err != nil {
return nil, err
}

View File

@@ -1,6 +1,11 @@
package datasource
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
)
@@ -45,3 +50,39 @@ func TestParseCmdlineCloudConfigFound(t *testing.T) {
}
}
}
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

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"log"
"path"
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
@@ -20,11 +19,9 @@ type CloudConfigFile interface {
}
// CloudConfigUnit represents a CoreOS specific configuration option that can generate
// an associated system.Unit to be created/enabled appropriately
// associated system.Units to be created/enabled appropriately
type CloudConfigUnit interface {
// Unit should either return (*system.Unit, error), or (nil, nil) if nothing
// needs to be done for this configuration option.
Unit(root string) (*system.Unit, error)
Units(root string) ([]system.Unit, error)
}
// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
@@ -215,27 +212,25 @@ func Apply(cfg CloudConfig, env *Environment) error {
}
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
u, err := ccu.Unit(env.Root())
u, err := ccu.Units(env.Root())
if err != nil {
return err
}
if u != nil {
cfg.Coreos.Units = append(cfg.Coreos.Units, *u)
}
cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
}
for _, file := range cfg.WriteFiles {
file.Path = path.Join(env.Root(), file.Path)
if err := system.WriteFile(&file); err != nil {
path, err := system.WriteFile(&file, env.Root())
if err != nil {
return err
}
log.Printf("Wrote file %s to filesystem", file.Path)
log.Printf("Wrote file %s to filesystem", path)
}
commands := make(map[string]string, 0)
reload := false
for _, unit := range cfg.Coreos.Units {
dst := system.UnitDestination(&unit, env.Root())
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 {
@@ -247,15 +242,20 @@ func Apply(cfg CloudConfig, env *Environment) error {
if unit.Mask {
log.Printf("Masking unit file %s", unit.Name)
if err := system.MaskUnit(unit.Name, env.Root()); err != nil {
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", dst)
if err := system.EnableUnitFile(dst, unit.Runtime); err != nil {
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)

View File

@@ -28,9 +28,9 @@ func (ee EtcdEnvironment) String() (out string) {
return
}
// Unit creates a Unit file drop-in for etcd, using any configured
// Units creates a Unit file drop-in for etcd, using any configured
// options and adding a default MachineID if unset.
func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) {
func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
if ee == nil {
return nil, nil
}
@@ -45,10 +45,11 @@ func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) {
}
}
return &system.Unit{
etcd := system.Unit{
Name: "etcd.service",
Runtime: true,
DropIn: true,
Content: ee.String(),
}, nil
}
return []system.Unit{etcd}, nil
}

View File

@@ -59,7 +59,7 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
}
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
ec := EtcdEnvironment{
ee := EtcdEnvironment{
"name": "node001",
"discovery": "http://disco.example.com/foobar",
"peer-bind-addr": "127.0.0.1:7002",
@@ -70,17 +70,18 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
}
defer os.RemoveAll(dir)
u, err := ec.Unit(dir)
uu, err := ee.Units(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
}
if u == nil {
t.Fatalf("Returned nil etcd unit unexpectedly")
if len(uu) != 1 {
t.Fatalf("Expected 1 unit to be returned, got %d", len(uu))
}
u := uu[0]
dst := system.UnitDestination(u, dir)
dst := u.Destination(dir)
os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil {
if err := system.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
}
@@ -124,17 +125,18 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
t.Fatalf("Failed writing out /etc/machine-id: %v", err)
}
u, err := ee.Unit(dir)
uu, err := ee.Units(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
}
if u == nil {
t.Fatalf("Returned nil etcd unit unexpectedly")
if len(uu) == 0 {
t.Fatalf("Returned empty etcd units unexpectedly")
}
u := uu[0]
dst := system.UnitDestination(u, dir)
dst := u.Destination(dir)
os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil {
if err := system.PlaceUnit(&u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
}
@@ -159,8 +161,8 @@ func TestEtcdEnvironmentWhenNil(t *testing.T) {
if ee != nil {
t.Fatalf("EtcdEnvironment is not nil")
}
u, err := ee.Unit("")
if u != nil || err != nil {
t.Fatalf("Unit returned a non-nil value for nil input")
uu, err := ee.Units("")
if len(uu) != 0 || err != nil {
t.Fatalf("Units returned value for nil input")
}
}

View File

@@ -19,16 +19,17 @@ func (fe FleetEnvironment) String() (out string) {
return
}
// Unit generates a Unit file drop-in for fleet, if any fleet options were
// Units generates a Unit file drop-in for fleet, if any fleet options were
// configured in cloud-config
func (fe FleetEnvironment) Unit(root string) (*system.Unit, error) {
func (fe FleetEnvironment) Units(root string) ([]system.Unit, error) {
if len(fe) < 1 {
return nil, nil
}
return &system.Unit{
fleet := system.Unit{
Name: "fleet.service",
Runtime: true,
DropIn: true,
Content: fe.String(),
}, nil
}
return []system.Unit{fleet}, nil
}

View File

@@ -19,20 +19,21 @@ Environment="FLEET_PUBLIC_IP=12.34.56.78"
func TestFleetUnit(t *testing.T) {
cfg := make(FleetEnvironment, 0)
u, err := cfg.Unit("/")
if u != nil {
uu, err := cfg.Units("/")
if len(uu) != 0 {
t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
}
cfg["public-ip"] = "12.34.56.78"
u, err = cfg.Unit("/")
uu, err = cfg.Units("/")
if err != nil {
t.Errorf("error generating fleet unit: %v", err)
}
if u == nil {
t.Fatalf("unexpectedly got nil unit generating fleet unit!")
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!")
}

View File

@@ -50,9 +50,7 @@ func TestEtcHostsWrittenToDisk(t *testing.T) {
t.Fatalf("manageEtcHosts returned nil file unexpectedly")
}
f.Path = path.Join(dir, f.Path)
if err := system.WriteFile(f); err != nil {
if _, err := system.WriteFile(f, dir); err != nil {
t.Fatalf("Error writing EtcHosts: %v", err)
}

View File

@@ -31,8 +31,7 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
t.Fatalf("OEMRelease returned nil file unexpectedly")
}
f.Path = path.Join(dir, f.Path)
if err := system.WriteFile(f); err != nil {
if _, err := system.WriteFile(f, dir); err != nil {
t.Fatalf("Writing of OEMRelease failed: %v", err)
}

View File

@@ -3,9 +3,8 @@ package initialize
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/coreos/coreos-cloudinit/pkg"
"github.com/coreos/coreos-cloudinit/system"
)
@@ -25,22 +24,19 @@ func SSHImportKeysFromURL(system_user string, url string) error {
}
func fetchUserKeys(url string) ([]string, error) {
res, err := http.Get(url)
client := pkg.NewHttpClient()
data, err := client.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
var data []UserKey
err = json.Unmarshal(body, &data)
var userKeys []UserKey
err = json.Unmarshal(data, &userKeys)
if err != nil {
return nil, err
}
keys := make([]string, 0)
for _, key := range data {
for _, key := range userKeys {
keys = append(keys, key.Key)
}
return keys, err

View File

@@ -12,7 +12,8 @@ import (
)
const (
locksmithUnit = "locksmithd.service"
locksmithUnit = "locksmithd.service"
updateEngineUnit = "update-engine.service"
)
// updateOption represents a configurable update option, which, if set, will be
@@ -36,7 +37,6 @@ var updateOptions = []*updateOption{
&updateOption{
key: "group",
prefix: "GROUP=",
valid: []string{"master", "beta", "alpha", "stable"},
},
&updateOption{
key: "server",
@@ -126,26 +126,40 @@ func (uc UpdateConfig) File(root string) (*system.File, error) {
}, nil
}
// GetUnit generates a locksmith system.Unit, if reboot-strategy was set in
// cloud-config, for the cloud-init initializer to act on appropriately
func (uc UpdateConfig) Unit(root string) (*system.Unit, error) {
strategy, ok := uc["reboot-strategy"]
if !ok {
return nil, nil
// 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)
}
u := &system.Unit{
Name: locksmithUnit,
Enable: true,
Command: "restart",
Mask: false,
rue := false
if _, ok := uc["group"]; ok {
rue = true
}
if _, ok := uc["server"]; ok {
rue = true
}
if rue {
ue := system.Unit{
Name: updateEngineUnit,
Command: "restart",
}
units = append(units, ue)
}
if strategy == "off" {
u.Enable = false
u.Command = "stop"
u.Mask = true
}
return u, nil
return units, nil
}

View File

@@ -38,12 +38,12 @@ func TestEmptyUpdateConfig(t *testing.T) {
if f != nil {
t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f)
}
u, err := uc.Unit("")
uu, err := uc.Units("")
if err != nil {
t.Error("unexpected error getting unit from empty UpdateConfig")
}
if u != nil {
t.Errorf("getting unit from empty UpdateConfig should have returned nil, got %v", u)
if len(uu) != 0 {
t.Errorf("getting unit from empty UpdateConfig should have returned zero units, got %d", len(uu))
}
}
@@ -106,6 +106,21 @@ SERVER=http://foo.com`
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.service" {
t.Errorf("bad name for generated unit: want update-engine.service, 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) {
@@ -145,12 +160,13 @@ func TestRebootStrategies(t *testing.T) {
t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line)
}
}
u, err := uc.Unit(dir)
uu, err := uc.Units(dir)
if err != nil {
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
} else if u == nil {
t.Errorf("generated empty unit for reboot-strategy=%v", s.name)
} else if 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)
}
@@ -189,8 +205,7 @@ func TestUpdateConfWrittenToDisk(t *testing.T) {
t.Fatal("Unexpectedly got nil updateconfig file")
}
f.Path = path.Join(dir, f.Path)
if err := system.WriteFile(f); err != nil {
if _, err := system.WriteFile(f, dir); err != nil {
t.Fatalf("Error writing update config: %v", err)
}

View File

@@ -3,6 +3,7 @@ package initialize
import (
"io/ioutil"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
@@ -28,21 +29,23 @@ func PersistScriptInWorkspace(script system.Script, workspace string) (string, e
}
tmp.Close()
relpath := strings.TrimPrefix(tmp.Name(), workspace)
file := system.File{
Path: tmp.Name(),
Path: relpath,
RawFilePermissions: "0744",
Content: string(script),
Content: string(script),
}
err = system.WriteFile(&file)
return file.Path, err
return system.WriteFile(&file, workspace)
}
func PersistUnitNameInWorkspace(name string, workspace string) error {
file := system.File{
Path: path.Join(workspace, "scripts", "unit-name"),
Path: path.Join("scripts", "unit-name"),
RawFilePermissions: "0644",
Content: name,
Content: name,
}
return system.WriteFile(&file)
_, err := system.WriteFile(&file, workspace)
return err
}

193
network/interface.go Normal file
View File

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

321
network/interface_test.go Normal file
View File

@@ -0,0 +1,321 @@
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()
}
}

45
network/network.go Normal file
View File

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

42
network/network_test.go Normal file
View File

@@ -0,0 +1,42 @@
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)
}
}
}

295
network/stanza.go Normal file
View File

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

502
network/stanza_test.go Normal file
View File

@@ -0,0 +1,502 @@
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()
}
}

123
pkg/http_client.go Normal file
View File

@@ -0,0 +1,123 @@
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,25 +1,42 @@
package datasource
package pkg
import (
"fmt"
"io"
"math"
"net/http"
"net/http/httptest"
"testing"
"time"
)
var expBackoffTests = []struct {
count int
body string
}{
{0, "number of attempts: 0"},
{1, "number of attempts: 1"},
{2, "number of attempts: 2"},
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 TestFetchURLExpBackOff(t *testing.T) {
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
@@ -34,7 +51,7 @@ func TestFetchURLExpBackOff(t *testing.T) {
ts := httptest.NewServer(mux)
defer ts.Close()
data, err := fetchURL(ts.URL)
data, err := client.Get(ts.URL)
if err != nil {
t.Errorf("Test case %d produced error: %v", i, err)
}
@@ -50,7 +67,8 @@ func TestFetchURLExpBackOff(t *testing.T) {
}
// Test that it stops retrying if a 4xx response comes back
func TestFetchURL4xx(t *testing.T) {
func TestGetURL4xx(t *testing.T) {
client := NewHttpClient()
retries := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
retries++
@@ -58,9 +76,9 @@ func TestFetchURL4xx(t *testing.T) {
}))
defer ts.Close()
_, err := fetchURL(ts.URL)
_, err := client.Get(ts.URL)
if err == nil {
t.Errorf("Incorrect result\ngot: %s\nwant: %s", err.Error(), "user-data not found. HTTP status code: 404")
t.Errorf("Incorrect result\ngot: %s\nwant: %s", err.Error(), "Not found. HTTP status code: 404")
}
if retries > 1 {
@@ -69,7 +87,7 @@ func TestFetchURL4xx(t *testing.T) {
}
// Test that it fetches and returns user-data just fine
func TestFetchURL2xx(t *testing.T) {
func TestGetURL2xx(t *testing.T) {
var cloudcfg = `
#cloud-config
coreos:
@@ -83,12 +101,13 @@ coreos:
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 := fetchURL(ts.URL)
data, err := client.Get(ts.URL)
if err != nil {
t.Errorf("Incorrect result\ngot: %v\nwant: %v", err, nil)
}
@@ -99,19 +118,21 @@ coreos:
}
// Test attempt to fetching using malformed URL
func TestFetchURLMalformed(t *testing.T) {
func TestGetMalformedURL(t *testing.T) {
client := NewHttpClient()
var tests = []struct {
url string
want string
}{
{"boo", "user-data URL boo does not have a valid HTTP scheme. Skipping."},
{"mailto://boo", "user-data URL mailto://boo does not have a valid HTTP scheme. Skipping."},
{"ftp://boo", "user-data URL ftp://boo does not have a valid HTTP scheme. Skipping."},
{"", "user-data URL is empty. Skipping."},
{"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 := fetchURL(test.url)
_, 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

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

View File

@@ -31,33 +31,55 @@ func (f *File) Permissions() (os.FileMode, error) {
return os.FileMode(perm), nil
}
func WriteFile(f *File) error {
func WriteFile(f *File, root string) (string, error) {
if f.Encoding != "" {
return fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
}
if err := os.MkdirAll(path.Dir(f.Path), os.FileMode(0755)); err != nil {
return err
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
return "", err
}
if err := ioutil.WriteFile(f.Path, []byte(f.Content), perm); 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, f.Path)
cmd := exec.Command("chown", f.Owner, tmp.Name())
if err := cmd.Run(); err != nil {
return err
return "", err
}
}
return nil
if err := os.Rename(tmp.Name(), fullpath); err != nil {
return "", err
}
return fullpath, nil
}
func EnsureDirectoryExists(dir string) error {

View File

@@ -4,7 +4,6 @@ import (
"io/ioutil"
"os"
"path"
"syscall"
"testing"
)
@@ -13,18 +12,22 @@ func TestWriteFileUnencodedContent(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
defer os.RemoveAll(dir)
fullPath := path.Join(dir, "tmp", "foo")
fn := "foo"
fullPath := path.Join(dir, fn)
wf := File{
Path: fullPath,
Content: "bar",
Path: fn,
Content: "bar",
RawFilePermissions: "0644",
}
if err := WriteFile(&wf); err != nil {
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)
@@ -51,15 +54,15 @@ func TestWriteFileInvalidPermission(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
defer os.RemoveAll(dir)
wf := File{
Path: path.Join(dir, "tmp", "foo"),
Content: "bar",
Path: path.Join(dir, "tmp", "foo"),
Content: "bar",
RawFilePermissions: "pants",
}
if err := WriteFile(&wf); err == nil {
if _, err := WriteFile(&wf, dir); err == nil {
t.Fatalf("Expected error to be raised when writing file with invalid permission")
}
}
@@ -69,17 +72,21 @@ func TestWriteFilePermissions(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
defer os.RemoveAll(dir)
fullPath := path.Join(dir, "tmp", "foo")
fn := "foo"
fullPath := path.Join(dir, fn)
wf := File{
Path: fullPath,
Path: fn,
RawFilePermissions: "0755",
}
if err := WriteFile(&wf); err != nil {
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)
@@ -97,15 +104,15 @@ func TestWriteFileEncodedContent(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer syscall.Rmdir(dir)
defer os.RemoveAll(dir)
wf := File{
Path: path.Join(dir, "tmp", "foo"),
Content: "",
Path: path.Join(dir, "tmp", "foo"),
Content: "",
Encoding: "base64",
}
if err := WriteFile(&wf); err == nil {
if _, err := WriteFile(&wf, dir); err == nil {
t.Fatalf("Expected error to be raised when writing file with encoding")
}
}

89
system/networkd.go Normal file
View File

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

@@ -51,10 +51,10 @@ func (u *Unit) Group() (group string) {
type Script []byte
// UnitDestination builds the appropriate absolute file path for
// the given Unit. The root argument indicates the effective base
// 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 UnitDestination(u *Unit, root string) string {
func (u *Unit) Destination(root string) string {
dir := "etc"
if u.Runtime {
dir = "run"
@@ -78,12 +78,12 @@ func PlaceUnit(u *Unit, dst string) error {
}
file := File{
Path: dst,
Path: filepath.Base(dst),
Content: u.Content,
RawFilePermissions: "0644",
}
err := WriteFile(&file)
_, err := WriteFile(&file, dir)
if err != nil {
return err
}
@@ -91,14 +91,14 @@ func PlaceUnit(u *Unit, dst string) error {
return nil
}
func EnableUnitFile(file string, runtime bool) error {
func EnableUnitFile(unit string, runtime bool) error {
conn, err := dbus.New()
if err != nil {
return err
}
files := []string{file}
_, _, err = conn.EnableUnitFiles(files, runtime, true)
units := []string{unit}
_, _, err = conn.EnableUnitFiles(units, runtime, true)
return err
}
@@ -179,10 +179,53 @@ func MachineID(root string) string {
return id
}
func MaskUnit(unit string, root string) error {
masked := path.Join(root, "etc", "systemd", "system", unit)
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
// 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

@@ -25,10 +25,10 @@ Address=10.209.171.177/19
}
defer os.RemoveAll(dir)
dst := UnitDestination(&u, dir)
dst := u.Destination(dir)
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
if dst != expectDst {
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
}
if err := PlaceUnit(&u, dst); err != nil {
@@ -69,18 +69,18 @@ func TestUnitDestination(t *testing.T) {
DropIn: false,
}
dst := UnitDestination(&u, dir)
dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst)
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
}
u.DropIn = true
dst = UnitDestination(&u, dir)
dst = u.Destination(dir)
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst)
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
}
}
@@ -100,10 +100,10 @@ Where=/media/state
}
defer os.RemoveAll(dir)
dst := UnitDestination(&u, dir)
dst := u.Destination(dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
if dst != expectDst {
t.Fatalf("UnitDestination returned %s, expected %s", dst, expectDst)
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
}
if err := PlaceUnit(&u, dst); err != nil {
@@ -154,16 +154,126 @@ func TestMaskUnit(t *testing.T) {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
if err := MaskUnit("foo.service", dir); err != nil {
t.Fatalf("Unable to mask unit: %v", err)
}
fullPath := path.Join(dir, "etc", "systemd", "system", "foo.service")
target, err := os.Readlink(fullPath)
// 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 target != "/dev/null" {
t.Fatalf("unit not masked, got unit target", target)
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)
}
}

2
test
View File

@@ -13,7 +13,7 @@ COVER=${COVER:-"-cover"}
source ./build
declare -a TESTPKGS=(initialize system datasource)
declare -a TESTPKGS=(initialize system datasource pkg network)
if [ -z "$PKG" ]; then
GOFMTPATH="$TESTPKGS coreos-cloudinit.go"

View File

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

View File

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

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

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

@@ -3,9 +3,9 @@
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"
SUBSYSTEM=="block", ENV{ID_FS_TYPE}=="iso9660|vfat", ENV{ID_FS_LABEL}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="media-configdrive.mount"
# Addtionally support virtfs from QEMU
SUBSYSTEM=="virtio", DRIVER=="9pnet_virtio", ATTR{mount_tag}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="configdrive-virtfs.service"
SUBSYSTEM=="virtio", DRIVER=="9pnet_virtio", ATTR{mount_tag}=="config-2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="media-configvirtfs.mount"
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

@@ -0,0 +1,13 @@
[Unit]
Wants=user-configdrive.service
Before=user-configdrive.service
# Only mount config drive block devices automatically in virtual machines
# or any host that has it explicitly enabled and not explicitly disabled.
ConditionVirtualization=|vm
ConditionKernelCommandLine=|coreos.configdrive=1
ConditionKernelCommandLine=!coreos.configdrive=0
[Mount]
What=LABEL=config-2
Where=/media/configdrive
Options=ro

View File

@@ -0,0 +1,18 @@
[Unit]
Wants=user-configvirtfs.service
Before=user-configvirtfs.service
# Only mount config drive block devices automatically in virtual machines
# or any host that has it explicitly enabled and not explicitly disabled.
ConditionVirtualization=|vm
ConditionKernelCommandLine=|coreos.configdrive=1
ConditionKernelCommandLine=!coreos.configdrive=0
# Support old style setup for now
Wants=addon-run@media-configvirtfs.service addon-config@media-configvirtfs.service
Before=addon-run@media-configvirtfs.service addon-config@media-configvirtfs.service
[Mount]
What=config-2
Where=/media/configvirtfs
Options=ro,trans=virtio,version=9p2000.L
Type=9p

View File

@@ -4,8 +4,8 @@ Requires=system-config.target
After=system-config.target
# Watch for configs at a couple common paths
Requires=user-cloudinit@media-configdrive-openstack-latest-user_data.path
After=user-cloudinit@media-configdrive-openstack-latest-user_data.path
Requires=user-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

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Watch for a cloud-config at /media/configdrive
# Note: This unit is essentially just here as a fall-back mechanism to
# trigger cloudinit if it isn't triggered explicitly by other means
# such as by a Wants= in the mount unit. This ensures we handle the
# case where /media/configdrive is provided to a CoreOS container.
[Path]
DirectoryNotEmpty=/media/configdrive

View File

@@ -1,9 +1,8 @@
[Unit]
Description=Load cloud-config from %f
Description=Load cloud-config from /media/configdrive
Requires=coreos-setup-environment.service
After=coreos-setup-environment.service
Before=user-config.target
ConditionFileNotEmpty=%f
# HACK: work around ordering between config drive and ec2 metadata It is
# possible for OpenStack style systems to provide both the metadata service
@@ -21,4 +20,4 @@ After=ec2-cloudinit.service
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=-/etc/environment
ExecStart=/usr/bin/coreos-cloudinit --from-file=%f
ExecStart=/usr/bin/coreos-cloudinit --from-configdrive=/media/configdrive

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Load cloud-config from /media/configvirtfs
Requires=coreos-setup-environment.service
After=coreos-setup-environment.service
Before=user-config.target
[Service]
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=-/etc/environment
ExecStart=/usr/bin/coreos-cloudinit --from-configdrive=/media/configvirtfs