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 }