/
machinecloudconfig.go
292 lines (254 loc) · 8.86 KB
/
machinecloudconfig.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
package cloudconfig
import (
"encoding/base64"
"os"
"path/filepath"
"sort"
"strings"
"github.com/juju/errors"
"github.com/juju/loggo"
osseries "github.com/juju/os/v2/series"
"github.com/juju/utils/v3"
"gopkg.in/yaml.v2"
corebase "github.com/juju/juju/core/base"
utilsos "github.com/juju/juju/core/os"
"github.com/juju/juju/core/paths"
)
// InitReader describes methods for extracting machine provisioning config,
// and extracting keys from config based on properties sourced from
// "container-inherit-properties" in model config.
type InitReader interface {
// GetInitConfig aggregates and returns provisioning data from the machine.
GetInitConfig() (map[string]interface{}, error)
// ExtractPropertiesFromConfig filters the input config data according to
// the input list of properties and returns the result
ExtractPropertiesFromConfig([]string, map[string]interface{}, loggo.Logger) map[string]interface{}
}
// MachineInitReaderConfig holds configuration values required by
// MachineInitReader to retrieve initialisation configuration for
// a single machine.
type MachineInitReaderConfig struct {
// Base is the base of the machine.
Base corebase.Base
// CloudInitConfigDir is the directory where cloud configuration resides
// on MAAS hosts.
CloudInitConfigDir string
// CloudInitInstanceConfigDir is the directory where cloud-init data for
// the instance resides. Cloud-Init user-data supplied to Juju lives here.
CloudInitInstanceConfigDir string
// CurtinInstallConfigFile is the file containing initialisation config
// written by Curtin.
// Apt configuration for MAAS versions 2.5+ resides here.
CurtinInstallConfigFile string
}
// MachineInitReader accesses Cloud-Init and Curtin configuration data,
// and extracts from it values for keys set in model configuration as
// "container-inherit-properties".
type MachineInitReader struct {
config MachineInitReaderConfig
}
// NewMachineInitReader creates and returns a new MachineInitReader for the
// input os name.
func NewMachineInitReader(base corebase.Base) (InitReader, error) {
osType := paths.OSType(base.OS)
cfg := MachineInitReaderConfig{
Base: base,
CloudInitConfigDir: paths.CloudInitCfgDir(osType),
CloudInitInstanceConfigDir: paths.MachineCloudInitDir(osType),
CurtinInstallConfigFile: paths.CurtinInstallConfig(osType),
}
return NewMachineInitReaderFromConfig(cfg), nil
}
// NewMachineInitReaderFromConfig creates and returns a new MachineInitReader using
// the input configuration.
func NewMachineInitReaderFromConfig(cfg MachineInitReaderConfig) InitReader {
return &MachineInitReader{config: cfg}
}
// GetInitConfig returns a map of configuration data used to provision the
// machine. It is sourced from both Cloud-Init and Curtin data.
func (r *MachineInitReader) GetInitConfig() (map[string]interface{}, error) {
switch utilsos.OSTypeForName(r.config.Base.OS) {
case utilsos.Ubuntu, utilsos.CentOS:
hostSeries, err := osseries.HostSeries()
series, err2 := corebase.GetSeriesFromBase(r.config.Base)
if err != nil || err2 != nil || series != hostSeries {
logger.Debugf("not attempting to get init config for %s, base of machine and container differ", r.config.Base.DisplayString())
return nil, nil
}
default:
logger.Debugf("not attempting to get init config for %s container", r.config.Base.DisplayString())
return nil, nil
}
machineCloudInitData, err := r.getMachineCloudCfgDirData()
if err != nil {
return nil, errors.Trace(err)
}
file := filepath.Join(r.config.CloudInitInstanceConfigDir, "vendor-data.txt")
vendorData, err := r.unmarshallConfigFile(file)
if err != nil {
return nil, errors.Trace(err)
}
for k, v := range vendorData {
machineCloudInitData[k] = v
}
_, curtinData, err := fileAsConfigMap(r.config.CurtinInstallConfigFile)
if err != nil {
return nil, errors.Trace(err)
}
for k, v := range curtinData {
machineCloudInitData[k] = v
}
return machineCloudInitData, nil
}
// getMachineCloudCfgDirData returns a map of the combined machine's Cloud-Init
// cloud.cfg.d config files. Files are read in lexical order.
func (r *MachineInitReader) getMachineCloudCfgDirData() (map[string]interface{}, error) {
dir := r.config.CloudInitConfigDir
files, err := os.ReadDir(dir)
if err != nil {
return nil, errors.Annotate(err, "determining files in CloudInitCfgDir for the machine")
}
sortedFiles := sortableDirEntries(files)
sort.Sort(sortedFiles)
cloudInit := make(map[string]interface{})
for _, file := range files {
name := file.Name()
if !strings.HasSuffix(name, ".cfg") {
continue
}
_, cloudCfgData, err := fileAsConfigMap(filepath.Join(dir, name))
if err != nil {
return nil, errors.Trace(err)
}
for k, v := range cloudCfgData {
cloudInit[k] = v
}
}
return cloudInit, nil
}
// unmarshallConfigFile reads the file at the input path,
// decompressing it if required, and converts the contents to a map of
// configuration key-values.
func (r *MachineInitReader) unmarshallConfigFile(file string) (map[string]interface{}, error) {
raw, config, err := fileAsConfigMap(file)
if err == nil {
return config, nil
}
if !errors.IsNotValid(err) {
return nil, errors.Trace(err)
}
// The data maybe be gzipped, base64 encoded, both, or neither.
// If both, it has been gzipped, then base64 encoded.
logger.Tracef("unmarshall failed (%s), file may be compressed", err.Error())
zippedData, err := utils.Gunzip(raw)
if err == nil {
cfg, err := bytesAsConfigMap(zippedData)
return cfg, errors.Trace(err)
}
logger.Tracef("Gunzip of %q failed (%s), maybe it is encoded", file, err)
decodedData, err := base64.StdEncoding.DecodeString(string(raw))
if err == nil {
if buf, err := bytesAsConfigMap(decodedData); err == nil {
return buf, nil
}
}
logger.Tracef("Decoding of %q failed (%s), maybe it is encoded and gzipped", file, err)
decodedZippedBuf, err := utils.Gunzip(decodedData)
if err != nil {
return nil, errors.Annotatef(err, "cannot unmarshall or decompress %q", file)
}
cfg, err := bytesAsConfigMap(decodedZippedBuf)
return cfg, errors.Trace(err)
}
// fileAsConfigMap reads the file at the input path and returns its contents as
// raw bytes, and if possible a map of config key-values.
func fileAsConfigMap(file string) ([]byte, map[string]interface{}, error) {
raw, err := os.ReadFile(file)
if err != nil {
return nil, nil, errors.Annotatef(err, "reading config from %q", file)
}
if len(raw) == 0 {
return nil, nil, nil
}
cfg, err := bytesAsConfigMap(raw)
if err != nil {
return raw, cfg, errors.NotValidf("converting %q contents to map: %s", file, err.Error())
}
return raw, cfg, nil
}
// ExtractPropertiesFromConfig filters the input config based on the
// input properties and returns a map of cloud-init data.
func (r *MachineInitReader) ExtractPropertiesFromConfig(
keys []string, cfg map[string]interface{}, log loggo.Logger,
) map[string]interface{} {
foundDataMap := make(map[string]interface{})
for _, k := range keys {
key := strings.TrimSpace(k)
switch key {
case "apt-security", "apt-primary", "apt-sources", "apt-sources_list":
if val, ok := cfg["apt"]; ok {
for k, v := range nestedAptConfig(key, val, log) {
// security, sources, and primary all nest under apt, ensure
// we don't overwrite prior translated data.
if apt, ok := foundDataMap["apt"].(map[string]interface{}); ok {
apt[k] = v
} else {
foundDataMap["apt"] = map[string]interface{}{
k: v,
}
}
}
} else {
log.Debugf("%s not found in machine init data", key)
}
case "ca-certs":
// No translation needed, ca-certs the same in both versions of Cloud-Init.
if val, ok := cfg[key]; ok {
foundDataMap[key] = val
} else {
log.Debugf("%s not found in machine init data", key)
}
}
}
return foundDataMap
}
func nestedAptConfig(key string, val interface{}, log loggo.Logger) map[string]interface{} {
split := strings.Split(key, "-")
secondary := split[1]
for k, v := range interfaceToMapStringInterface(val) {
if k == secondary {
foundDataMap := make(map[string]interface{})
foundDataMap[k] = v
return foundDataMap
}
}
log.Debugf("%s not found in machine init data", key)
return nil
}
type sortableDirEntries []os.DirEntry
func (fil sortableDirEntries) Len() int {
return len(fil)
}
func (fil sortableDirEntries) Less(i, j int) bool {
return fil[i].Name() < fil[j].Name()
}
func (fil sortableDirEntries) Swap(i, j int) {
fil[i], fil[j] = fil[j], fil[i]
}
func bytesAsConfigMap(raw []byte) (map[string]interface{}, error) {
dataMap := make(map[string]interface{})
err := yaml.Unmarshal(raw, &dataMap)
return dataMap, errors.Trace(err)
}
func interfaceToMapStringInterface(in interface{}) map[string]interface{} {
if inMap, ok := in.(map[interface{}]interface{}); ok {
outMap := make(map[string]interface{}, len(inMap))
for k, v := range inMap {
if key, ok := k.(string); ok {
outMap[key] = v
}
}
return outMap
}
return nil
}