github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/cloudconfig/userdatacfg_unix.go (about) 1 // Copyright 2012-2016 Canonical Ltd. 2 // Copyright 2014, 2015 Cloudbase Solutions 3 // Licensed under the AGPLv3, see LICENCE file for details. 4 5 package cloudconfig 6 7 import ( 8 "bytes" 9 "encoding/base64" 10 "encoding/json" 11 "fmt" 12 "io/ioutil" 13 "net/url" 14 "path" 15 "path/filepath" 16 "strings" 17 "text/template" 18 "time" 19 20 "github.com/juju/errors" 21 "github.com/juju/loggo" 22 "github.com/juju/names" 23 "github.com/juju/utils/featureflag" 24 "github.com/juju/utils/os" 25 "github.com/juju/utils/proxy" 26 "github.com/juju/version" 27 goyaml "gopkg.in/yaml.v2" 28 29 "github.com/juju/juju/agent" 30 "github.com/juju/juju/cloudconfig/cloudinit" 31 "github.com/juju/juju/environs/imagemetadata" 32 "github.com/juju/juju/environs/simplestreams" 33 "github.com/juju/juju/juju/osenv" 34 "github.com/juju/juju/service" 35 "github.com/juju/juju/service/systemd" 36 "github.com/juju/juju/service/upstart" 37 ) 38 39 const ( 40 // curlCommand is the base curl command used to download tools. 41 curlCommand = "curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s '" 42 43 // toolsDownloadWaitTime is the number of seconds to wait between 44 // each iterations of download attempts. 45 toolsDownloadWaitTime = 15 46 47 // toolsDownloadTemplate is a bash template that generates a 48 // bash command to cycle through a list of URLs to download tools. 49 toolsDownloadTemplate = `{{$curl := .ToolsDownloadCommand}} 50 n=1 51 while true; do 52 {{range .URLs}} 53 printf "Attempt $n to download tools from %s...\n" {{shquote .}} 54 {{$curl}} {{shquote .}} && echo "Tools downloaded successfully." && break 55 {{end}} 56 echo "Download failed, retrying in {{.ToolsDownloadWaitTime}}s" 57 sleep {{.ToolsDownloadWaitTime}} 58 n=$((n+1)) 59 done` 60 ) 61 62 var ( 63 // UbuntuGroups is the set of unix groups to add the "ubuntu" user to 64 // when initializing an Ubuntu system. 65 UbuntuGroups = []string{"adm", "audio", "cdrom", "dialout", "dip", 66 "floppy", "netdev", "plugdev", "sudo", "video"} 67 68 // CentOSGroups is the set of unix groups to add the "ubuntu" user to 69 // when initializing a CentOS system. 70 CentOSGroups = []string{"adm", "systemd-journal", "wheel"} 71 ) 72 73 type unixConfigure struct { 74 baseConfigure 75 } 76 77 // TODO(ericsnow) Move Configure to the baseConfigure type? 78 79 // Configure updates the provided cloudinit.Config with 80 // configuration to initialize a Juju machine agent. 81 func (w *unixConfigure) Configure() error { 82 if err := w.ConfigureBasic(); err != nil { 83 return err 84 } 85 return w.ConfigureJuju() 86 } 87 88 // ConfigureBasic updates the provided cloudinit.Config with 89 // basic configuration to initialise an OS image, such that it can 90 // be connected to via SSH, and log to a standard location. 91 // 92 // Any potentially failing operation should not be added to the 93 // configuration, but should instead be done in ConfigureJuju. 94 // 95 // Note: we don't do apt update/upgrade here so as not to have to wait on 96 // apt to finish when performing the second half of image initialisation. 97 // Doing it later brings the benefit of feedback in the face of errors, 98 // but adds to the running time of initialisation due to lack of activity 99 // between image bringup and start of agent installation. 100 func (w *unixConfigure) ConfigureBasic() error { 101 w.conf.AddScripts( 102 "set -xe", // ensure we run all the scripts or abort. 103 ) 104 switch w.os { 105 case os.Ubuntu: 106 if (w.icfg.AgentVersion() != version.Binary{}) { 107 initSystem, err := service.VersionInitSystem(w.icfg.Series) 108 if err != nil { 109 return errors.Trace(err) 110 } 111 w.addCleanShutdownJob(initSystem) 112 } 113 case os.CentOS: 114 w.conf.AddScripts( 115 // Mask and stop firewalld, if enabled, so it cannot start. See 116 // http://pad.lv/1492066. firewalld might be missing, in which case 117 // is-enabled and is-active prints an error, which is why the output 118 // is surpressed. 119 "systemctl is-enabled firewalld &> /dev/null && systemctl mask firewalld || true", 120 "systemctl is-active firewalld &> /dev/null && systemctl stop firewalld || true", 121 122 `sed -i "s/^.*requiretty/#Defaults requiretty/" /etc/sudoers`, 123 ) 124 w.addCleanShutdownJob(service.InitSystemSystemd) 125 } 126 SetUbuntuUser(w.conf, w.icfg.AuthorizedKeys) 127 w.conf.SetOutput(cloudinit.OutAll, "| tee -a "+w.icfg.CloudInitOutputLog, "") 128 // Create a file in a well-defined location containing the machine's 129 // nonce. The presence and contents of this file will be verified 130 // during bootstrap. 131 // 132 // Note: this must be the last runcmd we do in ConfigureBasic, as 133 // the presence of the nonce file is used to gate the remainder 134 // of synchronous bootstrap. 135 noncefile := path.Join(w.icfg.DataDir, NonceFile) 136 w.conf.AddRunTextFile(noncefile, w.icfg.MachineNonce, 0644) 137 return nil 138 } 139 140 func (w *unixConfigure) addCleanShutdownJob(initSystem string) { 141 switch initSystem { 142 case service.InitSystemUpstart: 143 path, contents := upstart.CleanShutdownJobPath, upstart.CleanShutdownJob 144 w.conf.AddRunTextFile(path, contents, 0644) 145 case service.InitSystemSystemd: 146 path, contents := systemd.CleanShutdownServicePath, systemd.CleanShutdownService 147 w.conf.AddRunTextFile(path, contents, 0644) 148 w.conf.AddScripts(fmt.Sprintf("/bin/systemctl enable '%s'", path)) 149 } 150 } 151 152 func (w *unixConfigure) setDataDirPermissions() string { 153 var user string 154 switch w.os { 155 case os.CentOS: 156 user = "root" 157 default: 158 user = "syslog" 159 } 160 return fmt.Sprintf("chown %s:adm %s", user, w.icfg.LogDir) 161 } 162 163 // ConfigureJuju updates the provided cloudinit.Config with configuration 164 // to initialise a Juju machine agent. 165 func (w *unixConfigure) ConfigureJuju() error { 166 if err := w.icfg.VerifyConfig(); err != nil { 167 return err 168 } 169 170 // Initialise progress reporting. We need to do separately for runcmd 171 // and (possibly, below) for bootcmd, as they may be run in different 172 // shell sessions. 173 initProgressCmd := cloudinit.InitProgressCmd() 174 w.conf.AddRunCmd(initProgressCmd) 175 176 // If we're doing synchronous bootstrap or manual provisioning, then 177 // ConfigureBasic won't have been invoked; thus, the output log won't 178 // have been set. We don't want to show the log to the user, so simply 179 // append to the log file rather than teeing. 180 if stdout, _ := w.conf.Output(cloudinit.OutAll); stdout == "" { 181 w.conf.SetOutput(cloudinit.OutAll, ">> "+w.icfg.CloudInitOutputLog, "") 182 w.conf.AddBootCmd(initProgressCmd) 183 w.conf.AddBootCmd(cloudinit.LogProgressCmd("Logging to %s on remote host", w.icfg.CloudInitOutputLog)) 184 } 185 186 w.conf.AddPackageCommands( 187 w.icfg.AptProxySettings, 188 w.icfg.AptMirror, 189 w.icfg.EnableOSRefreshUpdate, 190 w.icfg.EnableOSUpgrade, 191 ) 192 193 // Write out the normal proxy settings so that the settings are 194 // sourced by bash, and ssh through that. 195 w.conf.AddScripts( 196 // We look to see if the proxy line is there already as 197 // the manual provider may have had it already. The ubuntu 198 // user may not exist. 199 `([ ! -e /home/ubuntu/.profile ] || grep -q '.juju-proxy' /home/ubuntu/.profile) || ` + 200 `printf '\n# Added by juju\n[ -f "$HOME/.juju-proxy" ] && . "$HOME/.juju-proxy"\n' >> /home/ubuntu/.profile`) 201 if (w.icfg.ProxySettings != proxy.Settings{}) { 202 exportedProxyEnv := w.icfg.ProxySettings.AsScriptEnvironment() 203 w.conf.AddScripts(strings.Split(exportedProxyEnv, "\n")...) 204 w.conf.AddScripts( 205 fmt.Sprintf( 206 `(id ubuntu &> /dev/null) && (printf '%%s\n' %s > /home/ubuntu/.juju-proxy && chown ubuntu:ubuntu /home/ubuntu/.juju-proxy)`, 207 shquote(w.icfg.ProxySettings.AsScriptEnvironment()))) 208 } 209 210 if w.icfg.PublicImageSigningKey != "" { 211 keyFile := filepath.Join(agent.DefaultPaths.ConfDir, simplestreams.SimplestreamsPublicKeyFile) 212 w.conf.AddRunTextFile(keyFile, w.icfg.PublicImageSigningKey, 0644) 213 } 214 215 // Make the lock dir and change the ownership of the lock dir itself to 216 // ubuntu:ubuntu from root:root so the juju-run command run as the ubuntu 217 // user is able to get access to the hook execution lock (like the uniter 218 // itself does.) 219 lockDir := path.Join(w.icfg.DataDir, "locks") 220 w.conf.AddScripts( 221 fmt.Sprintf("mkdir -p %s", lockDir), 222 // We only try to change ownership if there is an ubuntu user defined. 223 fmt.Sprintf("(id ubuntu &> /dev/null) && chown ubuntu:ubuntu %s", lockDir), 224 fmt.Sprintf("mkdir -p %s", w.icfg.LogDir), 225 w.setDataDirPermissions(), 226 ) 227 228 // Make a directory for the tools to live in. 229 w.conf.AddScripts( 230 "bin="+shquote(w.icfg.JujuTools()), 231 "mkdir -p $bin", 232 ) 233 234 // Fetch the tools and unarchive them into it. 235 if err := w.addDownloadToolsCmds(); err != nil { 236 return errors.Trace(err) 237 } 238 239 // Don't remove tools tarball until after bootstrap agent 240 // runs, so it has a chance to add it to its catalogue. 241 defer w.conf.AddRunCmd( 242 fmt.Sprintf("rm $bin/tools.tar.gz && rm $bin/juju%s.sha256", w.icfg.AgentVersion()), 243 ) 244 245 // We add the machine agent's configuration info 246 // before running bootstrap-state so that bootstrap-state 247 // has a chance to rerwrite it to change the password. 248 // It would be cleaner to change bootstrap-state to 249 // be responsible for starting the machine agent itself, 250 // but this would not be backwardly compatible. 251 machineTag := names.NewMachineTag(w.icfg.MachineId) 252 _, err := w.addAgentInfo(machineTag) 253 if err != nil { 254 return errors.Trace(err) 255 } 256 257 // Add the cloud archive cloud-tools pocket to apt sources 258 // for series that need it. This gives us up-to-date LXC, 259 // MongoDB, and other infrastructure. 260 // This is only done on ubuntu. 261 if w.conf.SystemUpdate() && w.conf.RequiresCloudArchiveCloudTools() { 262 w.conf.AddCloudArchiveCloudTools() 263 } 264 265 if w.icfg.Bootstrap { 266 // Add the Juju GUI to the bootstrap node. 267 cleanup, err := w.setUpGUI() 268 if err != nil { 269 return errors.Annotate(err, "cannot set up Juju GUI") 270 } 271 if cleanup != nil { 272 defer cleanup() 273 } 274 275 var metadataDir string 276 if len(w.icfg.CustomImageMetadata) > 0 { 277 metadataDir = path.Join(w.icfg.DataDir, "simplestreams") 278 index, products, err := imagemetadata.MarshalImageMetadataJSON(w.icfg.CustomImageMetadata, nil, time.Now()) 279 if err != nil { 280 return err 281 } 282 indexFile := path.Join(metadataDir, imagemetadata.IndexStoragePath()) 283 productFile := path.Join(metadataDir, imagemetadata.ProductMetadataStoragePath()) 284 w.conf.AddRunTextFile(indexFile, string(index), 0644) 285 w.conf.AddRunTextFile(productFile, string(products), 0644) 286 metadataDir = " --image-metadata " + shquote(metadataDir) 287 } 288 289 bootstrapCons := w.icfg.Constraints.String() 290 if bootstrapCons != "" { 291 bootstrapCons = " --bootstrap-constraints " + shquote(bootstrapCons) 292 } 293 modelCons := w.icfg.ModelConstraints.String() 294 if modelCons != "" { 295 modelCons = " --constraints " + shquote(modelCons) 296 } 297 var hardware string 298 if w.icfg.HardwareCharacteristics != nil { 299 if hardware = w.icfg.HardwareCharacteristics.String(); hardware != "" { 300 hardware = " --hardware " + shquote(hardware) 301 } 302 } 303 w.conf.AddRunCmd(cloudinit.LogProgressCmd("Bootstrapping Juju machine agent")) 304 loggingOption := " --show-log" 305 // If the bootstrap command was requsted with --debug, then the root 306 // logger will be set to DEBUG. If it is, then we use --debug here too. 307 if loggo.GetLogger("").LogLevel() == loggo.DEBUG { 308 loggingOption = " --debug" 309 } 310 featureFlags := featureflag.AsEnvironmentValue() 311 if featureFlags != "" { 312 featureFlags = fmt.Sprintf("%s=%s ", osenv.JujuFeatureFlagEnvKey, featureFlags) 313 } 314 w.conf.AddScripts( 315 // The bootstrapping is always run with debug on. 316 featureFlags + w.icfg.JujuTools() + "/jujud bootstrap-state" + 317 " --data-dir " + shquote(w.icfg.DataDir) + 318 " --model-config " + shquote(base64yaml(w.icfg.Config.AllAttrs())) + 319 " --hosted-model-config " + shquote(base64yaml(w.icfg.HostedModelConfig)) + 320 " --instance-id " + shquote(string(w.icfg.InstanceId)) + 321 hardware + 322 bootstrapCons + 323 modelCons + 324 metadataDir + 325 loggingOption, 326 ) 327 } 328 329 return w.addMachineAgentToBoot() 330 } 331 332 func (w unixConfigure) addDownloadToolsCmds() error { 333 tools := w.icfg.ToolsList()[0] 334 if strings.HasPrefix(tools.URL, fileSchemePrefix) { 335 toolsData, err := ioutil.ReadFile(tools.URL[len(fileSchemePrefix):]) 336 if err != nil { 337 return err 338 } 339 w.conf.AddRunBinaryFile(path.Join(w.icfg.JujuTools(), "tools.tar.gz"), []byte(toolsData), 0644) 340 } else { 341 curlCommand := curlCommand 342 var urls []string 343 for _, tools := range w.icfg.ToolsList() { 344 urls = append(urls, tools.URL) 345 } 346 if w.icfg.Bootstrap { 347 curlCommand += " --retry 10" 348 if w.icfg.DisableSSLHostnameVerification { 349 curlCommand += " --insecure" 350 } 351 } else { 352 // Don't go through the proxy when downloading tools from the controllers 353 curlCommand += ` --noproxy "*"` 354 355 // Our API server certificates are unusable by curl (invalid subject name), 356 // so we must disable certificate validation. It doesn't actually 357 // matter, because there is no sensitive information being transmitted 358 // and we verify the tools' hash after. 359 curlCommand += " --insecure" 360 } 361 curlCommand += " -o $bin/tools.tar.gz" 362 w.conf.AddRunCmd(cloudinit.LogProgressCmd("Fetching tools: %s <%s>", curlCommand, urls)) 363 w.conf.AddRunCmd(toolsDownloadCommand(curlCommand, urls)) 364 } 365 366 w.conf.AddScripts( 367 fmt.Sprintf("sha256sum $bin/tools.tar.gz > $bin/juju%s.sha256", tools.Version), 368 fmt.Sprintf(`grep '%s' $bin/juju%s.sha256 || (echo "Tools checksum mismatch"; exit 1)`, 369 tools.SHA256, tools.Version), 370 fmt.Sprintf("tar zxf $bin/tools.tar.gz -C $bin"), 371 ) 372 373 toolsJson, err := json.Marshal(tools) 374 if err != nil { 375 return err 376 } 377 w.conf.AddScripts( 378 fmt.Sprintf("printf %%s %s > $bin/downloaded-tools.txt", shquote(string(toolsJson))), 379 ) 380 381 return nil 382 } 383 384 // setUpGUI fetches the Juju GUI archive and save it to the controller. 385 // The returned clean up function must be called when the bootstrapping 386 // process is completed. 387 func (w *unixConfigure) setUpGUI() (func(), error) { 388 if w.icfg.GUI == nil { 389 // No GUI archives were found on simplestreams, and no development 390 // GUI path has been passed with the JUJU_GUI environment variable. 391 return nil, nil 392 } 393 u, err := url.Parse(w.icfg.GUI.URL) 394 if err != nil { 395 return nil, errors.Annotate(err, "cannot parse Juju GUI URL") 396 } 397 guiJson, err := json.Marshal(w.icfg.GUI) 398 if err != nil { 399 return nil, errors.Trace(err) 400 } 401 guiDir := w.icfg.GUITools() 402 w.conf.AddScripts( 403 "gui="+shquote(guiDir), 404 "mkdir -p $gui", 405 ) 406 if u.Scheme == "file" { 407 // Upload the GUI from a local archive file. 408 guiData, err := ioutil.ReadFile(filepath.FromSlash(u.Path)) 409 if err != nil { 410 return nil, errors.Annotate(err, "cannot read Juju GUI archive") 411 } 412 w.conf.AddRunBinaryFile(path.Join(guiDir, "gui.tar.bz2"), guiData, 0644) 413 } else { 414 // Download the GUI from simplestreams. 415 command := "curl -sSf -o $gui/gui.tar.bz2 --retry 10" 416 if w.icfg.DisableSSLHostnameVerification { 417 command += " --insecure" 418 } 419 command += " " + shquote(u.String()) 420 // A failure in fetching the Juju GUI archive should not prevent the 421 // model to be bootstrapped. Better no GUI than no Juju at all. 422 command += " || echo Unable to retrieve Juju GUI" 423 w.conf.AddRunCmd(command) 424 } 425 w.conf.AddScripts( 426 "[ -f $gui/gui.tar.bz2 ] && sha256sum $gui/gui.tar.bz2 > $gui/jujugui.sha256", 427 fmt.Sprintf( 428 `[ -f $gui/jujugui.sha256 ] && (grep '%s' $gui/jujugui.sha256 && printf %%s %s > $gui/downloaded-gui.txt || echo Juju GUI checksum mismatch)`, 429 w.icfg.GUI.SHA256, shquote(string(guiJson))), 430 ) 431 return func() { 432 // Don't remove the GUI archive until after bootstrap agent runs, 433 // so it has a chance to add it to its catalogue. 434 w.conf.AddRunCmd("rm -f $gui/gui.tar.bz2 $gui/jujugui.sha256 $gui/downloaded-gui.txt") 435 }, nil 436 437 } 438 439 // toolsDownloadCommand takes a curl command minus the source URL, 440 // and generates a command that will cycle through the URLs until 441 // one succeeds. 442 func toolsDownloadCommand(curlCommand string, urls []string) string { 443 parsedTemplate := template.Must( 444 template.New("ToolsDownload").Funcs( 445 template.FuncMap{"shquote": shquote}, 446 ).Parse(toolsDownloadTemplate), 447 ) 448 var buf bytes.Buffer 449 err := parsedTemplate.Execute(&buf, map[string]interface{}{ 450 "ToolsDownloadCommand": curlCommand, 451 "ToolsDownloadWaitTime": toolsDownloadWaitTime, 452 "URLs": urls, 453 }) 454 if err != nil { 455 panic(errors.Annotate(err, "tools download template error")) 456 } 457 return buf.String() 458 } 459 460 func base64yaml(attrs map[string]interface{}) string { 461 data, err := goyaml.Marshal(attrs) 462 if err != nil { 463 // can't happen, these values have been validated a number of times 464 panic(err) 465 } 466 return base64.StdEncoding.EncodeToString(data) 467 }