mirror of
https://github.com/rancher/os.git
synced 2025-09-16 15:09:27 +00:00
Split cloud-init into cloud-init-execute and cloud-init-save
This commit is contained in:
314
cmd/cloudinitsave/cloudinitsave.go
Normal file
314
cmd/cloudinitsave/cloudinitsave.go
Normal file
@@ -0,0 +1,314 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
// Copyright 2015 Rancher Labs, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cloudinitsave
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/datasource"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/configdrive"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/file"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/ec2"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/packet"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/proc_cmdline"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/url"
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
"github.com/rancher/netconf"
|
||||
"github.com/rancher/os/cmd/cloudinitsave/gce"
|
||||
rancherConfig "github.com/rancher/os/config"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
const (
|
||||
datasourceInterval = 100 * time.Millisecond
|
||||
datasourceMaxInterval = 30 * time.Second
|
||||
datasourceTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
network bool
|
||||
flags *flag.FlagSet
|
||||
)
|
||||
|
||||
func init() {
|
||||
flags = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
flags.BoolVar(&network, "network", true, "use network based datasources")
|
||||
}
|
||||
|
||||
func Main() {
|
||||
flags.Parse(os.Args[1:])
|
||||
|
||||
log.Infof("Running cloud-init-save: network=%v", network)
|
||||
|
||||
if err := saveCloudConfig(); err != nil {
|
||||
log.Errorf("Failed to save cloud-config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func saveFiles(cloudConfigBytes, scriptBytes []byte, metadata datasource.Metadata) error {
|
||||
os.MkdirAll(rancherConfig.CloudConfigDir, os.ModeDir|0600)
|
||||
os.Remove(rancherConfig.CloudConfigScriptFile)
|
||||
os.Remove(rancherConfig.CloudConfigBootFile)
|
||||
os.Remove(rancherConfig.MetaDataFile)
|
||||
|
||||
if len(scriptBytes) > 0 {
|
||||
log.Infof("Writing to %s", rancherConfig.CloudConfigScriptFile)
|
||||
if err := util.WriteFileAtomic(rancherConfig.CloudConfigScriptFile, scriptBytes, 500); err != nil {
|
||||
log.Errorf("Error while writing file %s: %v", rancherConfig.CloudConfigScriptFile, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := util.WriteFileAtomic(rancherConfig.CloudConfigBootFile, cloudConfigBytes, 400); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Written to %s:\n%s", rancherConfig.CloudConfigBootFile, string(cloudConfigBytes))
|
||||
|
||||
metaDataBytes, err := yaml.Marshal(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = util.WriteFileAtomic(rancherConfig.MetaDataFile, metaDataBytes, 400); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Written to %s:\n%s", rancherConfig.MetaDataFile, string(metaDataBytes))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentDatasource() (datasource.Datasource, error) {
|
||||
cfg := rancherConfig.LoadConfig()
|
||||
|
||||
dss := getDatasources(cfg)
|
||||
if len(dss) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ds := selectDatasource(dss)
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func saveCloudConfig() error {
|
||||
userDataBytes, metadata, err := fetchUserData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userData := string(userDataBytes)
|
||||
scriptBytes := []byte{}
|
||||
|
||||
if config.IsScript(userData) {
|
||||
scriptBytes = userDataBytes
|
||||
userDataBytes = []byte{}
|
||||
} else if isCompose(userData) {
|
||||
if userDataBytes, err = composeToCloudConfig(userDataBytes); err != nil {
|
||||
log.Errorf("Failed to convert compose to cloud-config syntax: %v", err)
|
||||
return err
|
||||
}
|
||||
} else if config.IsCloudConfig(userData) {
|
||||
if _, err := rancherConfig.ReadConfig(userDataBytes, false); err != nil {
|
||||
log.WithFields(log.Fields{"cloud-config": userData, "err": err}).Warn("Failed to parse cloud-config, not saving.")
|
||||
userDataBytes = []byte{}
|
||||
}
|
||||
} else {
|
||||
log.Errorf("Unrecognized user-data\n%s", userData)
|
||||
userDataBytes = []byte{}
|
||||
}
|
||||
|
||||
if _, err := rancherConfig.ReadConfig(userDataBytes, false); err != nil {
|
||||
log.WithFields(log.Fields{"cloud-config": userData, "err": err}).Warn("Failed to parse cloud-config")
|
||||
return errors.New("Failed to parse cloud-config")
|
||||
}
|
||||
|
||||
return saveFiles(userDataBytes, scriptBytes, metadata)
|
||||
}
|
||||
|
||||
func fetchUserData() ([]byte, datasource.Metadata, error) {
|
||||
var metadata datasource.Metadata
|
||||
ds, err := currentDatasource()
|
||||
if err != nil || ds == nil {
|
||||
log.Errorf("Failed to select datasource: %v", err)
|
||||
return nil, metadata, err
|
||||
}
|
||||
log.Infof("Fetching user-data from datasource %v", ds.Type())
|
||||
userDataBytes, err := ds.FetchUserdata()
|
||||
if err != nil {
|
||||
log.Errorf("Failed fetching user-data from datasource: %v", err)
|
||||
return nil, metadata, err
|
||||
}
|
||||
log.Infof("Fetching meta-data from datasource of type %v", ds.Type())
|
||||
metadata, err = ds.FetchMetadata()
|
||||
if err != nil {
|
||||
log.Errorf("Failed fetching meta-data from datasource: %v", err)
|
||||
return nil, metadata, err
|
||||
}
|
||||
return userDataBytes, metadata, nil
|
||||
}
|
||||
|
||||
// getDatasources creates a slice of possible Datasources for cloudinit based
|
||||
// on the different source command-line flags.
|
||||
func getDatasources(cfg *rancherConfig.CloudConfig) []datasource.Datasource {
|
||||
dss := make([]datasource.Datasource, 0, 5)
|
||||
|
||||
for _, ds := range cfg.Rancher.CloudInit.Datasources {
|
||||
parts := strings.SplitN(ds, ":", 2)
|
||||
|
||||
switch parts[0] {
|
||||
case "ec2":
|
||||
if network {
|
||||
if len(parts) == 1 {
|
||||
dss = append(dss, ec2.NewDatasource(ec2.DefaultAddress))
|
||||
} else {
|
||||
dss = append(dss, ec2.NewDatasource(parts[1]))
|
||||
}
|
||||
}
|
||||
case "file":
|
||||
if len(parts) == 2 {
|
||||
dss = append(dss, file.NewDatasource(parts[1]))
|
||||
}
|
||||
case "url":
|
||||
if network {
|
||||
if len(parts) == 2 {
|
||||
dss = append(dss, url.NewDatasource(parts[1]))
|
||||
}
|
||||
}
|
||||
case "cmdline":
|
||||
if network {
|
||||
if len(parts) == 1 {
|
||||
dss = append(dss, proc_cmdline.NewDatasource())
|
||||
}
|
||||
}
|
||||
case "configdrive":
|
||||
if len(parts) == 2 {
|
||||
dss = append(dss, configdrive.NewDatasource(parts[1]))
|
||||
}
|
||||
case "digitalocean":
|
||||
if network {
|
||||
if len(parts) == 1 {
|
||||
dss = append(dss, digitalocean.NewDatasource(digitalocean.DefaultAddress))
|
||||
} else {
|
||||
dss = append(dss, digitalocean.NewDatasource(parts[1]))
|
||||
}
|
||||
} else {
|
||||
enableDoLinkLocal()
|
||||
}
|
||||
case "gce":
|
||||
if network {
|
||||
dss = append(dss, gce.NewDatasource("http://metadata.google.internal/"))
|
||||
}
|
||||
case "packet":
|
||||
if !network {
|
||||
enablePacketNetwork(&cfg.Rancher)
|
||||
}
|
||||
dss = append(dss, packet.NewDatasource("https://metadata.packet.net/"))
|
||||
}
|
||||
}
|
||||
|
||||
return dss
|
||||
}
|
||||
|
||||
func enableDoLinkLocal() {
|
||||
err := netconf.ApplyNetworkConfigs(&netconf.NetworkConfig{
|
||||
Interfaces: map[string]netconf.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPV4LL: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to apply link local on eth0: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// selectDatasource attempts to choose a valid Datasource to use based on its
|
||||
// current availability. The first Datasource to report to be available is
|
||||
// returned. Datasources will be retried if possible if they are not
|
||||
// immediately available. If all Datasources are permanently unavailable or
|
||||
// datasourceTimeout is reached before one becomes available, nil is returned.
|
||||
func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
||||
ds := make(chan datasource.Datasource)
|
||||
stop := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, s := range sources {
|
||||
wg.Add(1)
|
||||
go func(s datasource.Datasource) {
|
||||
defer wg.Done()
|
||||
|
||||
duration := datasourceInterval
|
||||
for {
|
||||
log.Infof("Checking availability of %q\n", s.Type())
|
||||
if s.IsAvailable() {
|
||||
ds <- s
|
||||
return
|
||||
} else if !s.AvailabilityChanges() {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-time.After(duration):
|
||||
duration = pkg.ExpBackoff(duration, datasourceMaxInterval)
|
||||
}
|
||||
}
|
||||
}(s)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
var s datasource.Datasource
|
||||
select {
|
||||
case s = <-ds:
|
||||
case <-done:
|
||||
case <-time.After(datasourceTimeout):
|
||||
}
|
||||
|
||||
close(stop)
|
||||
return s
|
||||
}
|
||||
|
||||
func isCompose(content string) bool {
|
||||
return strings.HasPrefix(content, "#compose\n")
|
||||
}
|
||||
|
||||
func composeToCloudConfig(bytes []byte) ([]byte, error) {
|
||||
compose := make(map[interface{}]interface{})
|
||||
err := yaml.Unmarshal(bytes, &compose)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return yaml.Marshal(map[interface{}]interface{}{
|
||||
"rancher": map[interface{}]interface{}{
|
||||
"services": compose,
|
||||
},
|
||||
})
|
||||
}
|
130
cmd/cloudinitsave/gce/metadata.go
Normal file
130
cmd/cloudinitsave/gce/metadata.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright 2016 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gce
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
apiVersion = "computeMetadata/v1/"
|
||||
metadataPath = apiVersion
|
||||
userdataPath = apiVersion + "instance/attributes/user-data"
|
||||
)
|
||||
|
||||
type metadataService struct {
|
||||
metadata.MetadataService
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *metadataService {
|
||||
return &metadataService{metadata.NewDatasource(root, apiVersion, userdataPath, metadataPath, http.Header{"Metadata-Flavor": {"Google"}})}
|
||||
}
|
||||
|
||||
func (ms metadataService) FetchMetadata() (datasource.Metadata, error) {
|
||||
public, err := ms.fetchIP("instance/network-interfaces/0/access-configs/0/external-ip")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
local, err := ms.fetchIP("instance/network-interfaces/0/ip")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
hostname, err := ms.fetchString("instance/hostname")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
|
||||
projectSshKeys, err := ms.fetchString("project/attributes/sshKeys")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
instanceSshKeys, err := ms.fetchString("instance/attributes/sshKeys")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
|
||||
keyStrings := strings.Split(projectSshKeys+"\n"+instanceSshKeys, "\n")
|
||||
|
||||
sshPublicKeys := map[string]string{}
|
||||
i := 0
|
||||
for _, keyString := range keyStrings {
|
||||
keySlice := strings.SplitN(keyString, ":", 2)
|
||||
if len(keySlice) == 2 {
|
||||
key := strings.TrimSpace(keySlice[1])
|
||||
if key != "" {
|
||||
sshPublicKeys[strconv.Itoa(i)] = strings.TrimSpace(keySlice[1])
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return datasource.Metadata{
|
||||
PublicIPv4: public,
|
||||
PrivateIPv4: local,
|
||||
Hostname: hostname,
|
||||
SSHPublicKeys: sshPublicKeys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ms metadataService) Type() string {
|
||||
return "gce-metadata-service"
|
||||
}
|
||||
|
||||
func (ms metadataService) fetchString(key string) (string, error) {
|
||||
data, err := ms.FetchData(ms.MetadataUrl() + key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (ms metadataService) fetchIP(key string) (net.IP, error) {
|
||||
str, err := ms.fetchString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(str); ip != nil {
|
||||
return ip, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("couldn't parse %q as IP address", str)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms metadataService) FetchUserdata() ([]byte, error) {
|
||||
data, err := ms.FetchData(ms.UserdataUrl())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
data, err = ms.FetchData(ms.MetadataUrl() + "instance/attributes/startup-script")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
95
cmd/cloudinitsave/packet.go
Normal file
95
cmd/cloudinitsave/packet.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package cloudinitsave
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/packethost/packngo/metadata"
|
||||
"github.com/rancher/netconf"
|
||||
rancherConfig "github.com/rancher/os/config"
|
||||
)
|
||||
|
||||
func enablePacketNetwork(cfg *rancherConfig.RancherConfig) {
|
||||
bootStrapped := false
|
||||
for _, v := range cfg.Network.Interfaces {
|
||||
if v.Address != "" {
|
||||
if err := netconf.ApplyNetworkConfigs(&cfg.Network); err != nil {
|
||||
logrus.Errorf("Failed to bootstrap network: %v", err)
|
||||
return
|
||||
}
|
||||
bootStrapped = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !bootStrapped {
|
||||
return
|
||||
}
|
||||
|
||||
c := metadata.NewClient(http.DefaultClient)
|
||||
m, err := c.Metadata.Get()
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to get Packet metadata: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
bondCfg := netconf.InterfaceConfig{
|
||||
Addresses: []string{},
|
||||
BondOpts: map[string]string{
|
||||
"lacp_rate": "1",
|
||||
"xmit_hash_policy": "layer3+4",
|
||||
"downdelay": "200",
|
||||
"updelay": "200",
|
||||
"miimon": "100",
|
||||
"mode": "4",
|
||||
},
|
||||
}
|
||||
netCfg := netconf.NetworkConfig{
|
||||
Interfaces: map[string]netconf.InterfaceConfig{},
|
||||
}
|
||||
for _, iface := range m.Network.Interfaces {
|
||||
netCfg.Interfaces["mac="+iface.Mac] = netconf.InterfaceConfig{
|
||||
Bond: "bond0",
|
||||
}
|
||||
}
|
||||
for _, addr := range m.Network.Addresses {
|
||||
bondCfg.Addresses = append(bondCfg.Addresses, fmt.Sprintf("%s/%d", addr.Address, addr.Cidr))
|
||||
if addr.Gateway != "" {
|
||||
if addr.AddressFamily == 4 {
|
||||
if addr.Public {
|
||||
bondCfg.Gateway = addr.Gateway
|
||||
}
|
||||
} else {
|
||||
bondCfg.GatewayIpv6 = addr.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
if addr.AddressFamily == 4 && strings.HasPrefix(addr.Gateway, "10.") {
|
||||
bondCfg.PostUp = append(bondCfg.PostUp, "ip route add 10.0.0.0/8 via "+addr.Gateway)
|
||||
}
|
||||
}
|
||||
|
||||
netCfg.Interfaces["bond0"] = bondCfg
|
||||
bytes, _ := yaml.Marshal(netCfg)
|
||||
logrus.Debugf("Generated network config: %s", string(bytes))
|
||||
|
||||
cc := rancherConfig.CloudConfig{
|
||||
Rancher: rancherConfig.RancherConfig{
|
||||
Network: netCfg,
|
||||
},
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(path.Dir(rancherConfig.CloudConfigNetworkFile), 0700); err != nil {
|
||||
logrus.Errorf("Failed to create directory for file %s: %v", rancherConfig.CloudConfigNetworkFile, err)
|
||||
}
|
||||
|
||||
if err := rancherConfig.WriteToFile(cc, rancherConfig.CloudConfigNetworkFile); err != nil {
|
||||
logrus.Errorf("Failed to save config file %s: %v", rancherConfig.CloudConfigNetworkFile, err)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user