github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cloudconfig/machinecloudconfig.go (about)

     1  package cloudconfig
     2  
     3  import (
     4  	"encoding/base64"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	utilsos "github.com/juju/os"
    14  	utilsseries "github.com/juju/os/series"
    15  	"github.com/juju/utils"
    16  	"gopkg.in/yaml.v2"
    17  
    18  	"github.com/juju/juju/juju/paths"
    19  )
    20  
    21  // GetMachineCloudInitData returns a map of all cloud init data on the machine.
    22  func GetMachineCloudInitData(series string) (map[string]interface{}, error) {
    23  	containerOS, err := utilsseries.GetOSFromSeries(series)
    24  	if err != nil {
    25  		return nil, err
    26  	}
    27  	switch containerOS {
    28  	case utilsos.Ubuntu, utilsos.CentOS, utilsos.OpenSUSE:
    29  		if series != utilsseries.MustHostSeries() {
    30  			logger.Debugf("not attempting to get cloudinit data for %s, series of machine and container differ", series)
    31  			return nil, nil
    32  		}
    33  	default:
    34  		logger.Debugf("not attempting to get cloudinit data for %s container", series)
    35  		return nil, nil
    36  	}
    37  
    38  	machineCloudInitData, err := getMachineCloudCfgDirData(series)
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  	vendorData, err := getMachineVendorData(series)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	for k, v := range vendorData {
    47  		machineCloudInitData[k] = v
    48  	}
    49  	return machineCloudInitData, nil
    50  }
    51  
    52  type sortableFileInfos []os.FileInfo
    53  
    54  func (fil sortableFileInfos) Len() int {
    55  	return len(fil)
    56  }
    57  
    58  func (fil sortableFileInfos) Less(i, j int) bool {
    59  	return fil[i].Name() < fil[j].Name()
    60  }
    61  
    62  func (fil sortableFileInfos) Swap(i, j int) {
    63  	fil[i], fil[j] = fil[j], fil[i]
    64  }
    65  
    66  // CloudInitCfgDir is for testing purposes.
    67  var CloudInitCfgDir = paths.CloudInitCfgDir
    68  
    69  // getMachineCloudCfgDirData returns a map of the combined machine's cloud init
    70  // cloud.cfg.d config files.  Files are read in lexical order.
    71  func getMachineCloudCfgDirData(series string) (map[string]interface{}, error) {
    72  	dir, err := CloudInitCfgDir(series)
    73  	if err != nil {
    74  		return nil, errors.Annotate(err, "cannot determine CloudInitCfgDir for the machine")
    75  	}
    76  	fileInfo, err := ioutil.ReadDir(dir)
    77  	if err != nil {
    78  		return nil, errors.Annotate(err, "cannot determine files in CloudInitCfgDir for the machine")
    79  	}
    80  	sortedFileInfos := sortableFileInfos(fileInfo)
    81  	sort.Sort(sortedFileInfos)
    82  	cloudInit := make(map[string]interface{})
    83  	for _, file := range fileInfo {
    84  		name := file.Name()
    85  		if !strings.HasSuffix(name, ".cfg") {
    86  			continue
    87  		}
    88  		data, err := ioutil.ReadFile(filepath.Join(dir, name))
    89  		if err != nil {
    90  			return nil, errors.Annotatef(err, "cannot read %q from machine", name)
    91  		}
    92  		cloudCfgData, err := unmarshallContainerCloudInit(data)
    93  		if err != nil {
    94  			return nil, errors.Annotatef(err, "cannot unmarshall %q from machine", name)
    95  		}
    96  		for k, v := range cloudCfgData {
    97  			cloudInit[k] = v
    98  		}
    99  	}
   100  	return cloudInit, nil
   101  }
   102  
   103  // getMachineVendorData returns a map of machine's cloud init vendor-data.txt.
   104  func getMachineVendorData(series string) (map[string]interface{}, error) {
   105  	// vendor-data.txt may or may not be compressed.
   106  	return getMachineData(series, "vendor-data.txt")
   107  }
   108  
   109  // MachineCloudInitDir is for testing purposes.
   110  var MachineCloudInitDir = paths.MachineCloudInitDir
   111  
   112  func getMachineData(series, file string) (map[string]interface{}, error) {
   113  	dir, err := MachineCloudInitDir(series)
   114  	if err != nil {
   115  		return nil, errors.Annotate(err, "cannot determine MachineCloudInitDir for the machine")
   116  	}
   117  	data, err := ioutil.ReadFile(filepath.Join(dir, file))
   118  	if err != nil {
   119  		return nil, errors.Annotatef(err, "cannot read %q from machine", file)
   120  	}
   121  
   122  	if len(data) == 0 {
   123  		// vendor-data.txt is sometimes empty
   124  		return nil, nil
   125  	}
   126  	// The userdata maybe be gzip'd, base64 encoded, both, or neither.
   127  	// If both, it's been gzip'd, then base64 encoded.
   128  	rawBuf, err := unmarshallContainerCloudInit(data)
   129  	if err == nil {
   130  		return rawBuf, nil
   131  	}
   132  	logger.Tracef("unmarshal of %q failed (%s), maybe it is compressed", file, err)
   133  
   134  	zippedData, err := utils.Gunzip(data)
   135  	if err == nil {
   136  		return unmarshallContainerCloudInit(zippedData)
   137  	}
   138  	logger.Tracef("Gunzip of %q failed (%s), maybe it is encoded", file, err)
   139  
   140  	decodedData, err := base64.StdEncoding.DecodeString(string(data))
   141  	if err == nil {
   142  		// it could still be gzip'd.
   143  		buf, err := unmarshallContainerCloudInit(decodedData)
   144  		if err == nil {
   145  			return buf, nil
   146  		}
   147  	}
   148  	logger.Tracef("Decoding of %q failed (%s), maybe it is encoded and gzipped", file, err)
   149  
   150  	decodedZippedBuf, err := utils.Gunzip(decodedData)
   151  	if err != nil {
   152  		// During testing, it was found that the trusty vendor-data.txt.i file
   153  		// can contain only the text "NONE", which doesn't unmarshall or decompress
   154  		// we don't want to fail in that case.
   155  		if series == "trusty" {
   156  			logger.Debugf("failed to unmarshall or decompress %q: %s", file, err)
   157  			return nil, nil
   158  		}
   159  		return nil, errors.Annotatef(err, "cannot unmarshall or decompress %q", file)
   160  	}
   161  	return unmarshallContainerCloudInit(decodedZippedBuf)
   162  }
   163  
   164  func unmarshallContainerCloudInit(raw []byte) (map[string]interface{}, error) {
   165  	dataMap := make(map[string]interface{})
   166  	err := yaml.Unmarshal(raw, &dataMap)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	return dataMap, nil
   171  }
   172  
   173  type cloudConfigTranslateFunc func(string, map[string]interface{}, loggo.Logger) map[string]interface{}
   174  
   175  // CloudConfigByVersionFunc returns the correct function to translate
   176  // container-inherit-properties to cloud-init data based on series.
   177  func CloudConfigByVersionFunc(series string) cloudConfigTranslateFunc {
   178  	if series == "trusty" {
   179  		return machineCloudConfigV077
   180  	}
   181  	// There is a big assumption that supported CentOS and OpenSUSE versions
   182  	// supported by juju are using cloud-init version >= 0.7.8
   183  	return machineCloudConfigV078
   184  }
   185  
   186  // machineCloudConfigV078 finds the containerInheritProperties properties and
   187  // values in the given dataMap and returns a cloud-init v0.7.8 formatted map.
   188  func machineCloudConfigV078(containerInheritProperties string, dataMap map[string]interface{}, log loggo.Logger) map[string]interface{} {
   189  	if containerInheritProperties == "" {
   190  		return nil
   191  	}
   192  	foundDataMap := make(map[string]interface{})
   193  	for _, k := range strings.Split(containerInheritProperties, ",") {
   194  		key := strings.TrimSpace(k)
   195  		switch key {
   196  		case "apt-security", "apt-sources", "apt-primary":
   197  			if val, ok := dataMap["apt"]; ok {
   198  				for k, v := range machineCloudConfigAptV078(key, val, log) {
   199  					// security, sources, and primary all nest under apt, ensure
   200  					// we don't overwrite prior translated data.
   201  					if apt, ok := foundDataMap["apt"].(map[string]interface{}); ok {
   202  						apt[k] = v
   203  					} else {
   204  						foundDataMap["apt"] = map[string]interface{}{
   205  							k: v,
   206  						}
   207  					}
   208  				}
   209  			} else {
   210  				log.Debugf("%s not found in machine cloud-init data", key)
   211  			}
   212  		case "ca-certs":
   213  			// no translation needed, ca-certs the same in both versions of cloudinit
   214  			if val, ok := dataMap[key]; ok {
   215  				foundDataMap[key] = val
   216  			} else {
   217  				log.Debugf("%s not found in machine cloud-init data", key)
   218  			}
   219  		}
   220  	}
   221  	return foundDataMap
   222  }
   223  
   224  func machineCloudConfigAptV078(key string, val interface{}, log loggo.Logger) map[string]interface{} {
   225  	split := strings.Split(key, "-")
   226  	secondary := split[1]
   227  
   228  	for k, v := range interfaceToMapStringInterface(val) {
   229  		if k == secondary {
   230  			foundDataMap := make(map[string]interface{})
   231  			foundDataMap[k] = v
   232  			return foundDataMap
   233  		}
   234  	}
   235  
   236  	log.Debugf("%s not found in machine cloud-init data", key)
   237  	return nil
   238  }
   239  
   240  var aptPrimaryKeys = []string{"apt_mirror", "apt_mirror_search", "apt_mirror_search_dns"}
   241  var aptSourcesKeys = []string{"apt_sources"}
   242  
   243  // machineCloudConfigV077 finds the containerInheritProperties properties and
   244  // values in the given dataMap and returns a cloud-init v0.7.7 formatted map.
   245  func machineCloudConfigV077(containerInheritProperties string, dataMap map[string]interface{}, log loggo.Logger) map[string]interface{} {
   246  	if containerInheritProperties == "" {
   247  		return nil
   248  	}
   249  	foundDataMap := make(map[string]interface{})
   250  	keySplit := strings.Split(containerInheritProperties, ",")
   251  	for _, k := range keySplit {
   252  		key := strings.TrimSpace(k)
   253  		switch key {
   254  		case "apt-primary", "apt-sources":
   255  			for _, aptKey := range append(aptPrimaryKeys, aptSourcesKeys...) {
   256  				if val, ok := dataMap[aptKey]; ok {
   257  					foundDataMap[aptKey] = val
   258  				} else {
   259  					log.Debugf("%s not found as part of %s, in machine cloud-init data", strings.Join(keySplit, "-"), key)
   260  				}
   261  			}
   262  		case "apt-security":
   263  			// Translation for apt-security unknown at this time.
   264  			log.Debugf("%s not found in machine cloud-init data", key)
   265  		case "ca-certs":
   266  			// no translation needed, ca-certs the same in both versions of cloudinit
   267  			if val, ok := dataMap[key]; ok {
   268  				foundDataMap[key] = val
   269  			} else {
   270  				log.Debugf("%s not found in machine cloud-init data", key)
   271  			}
   272  		}
   273  	}
   274  	return foundDataMap
   275  }
   276  
   277  func interfaceToMapStringInterface(in interface{}) map[string]interface{} {
   278  	if inMap, ok := in.(map[interface{}]interface{}); ok {
   279  		outMap := make(map[string]interface{}, len(inMap))
   280  		for k, v := range inMap {
   281  			if key, ok := k.(string); ok {
   282  				outMap[key] = v
   283  			}
   284  		}
   285  		return outMap
   286  	}
   287  	return nil
   288  }