github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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/json" 10 "fmt" 11 stdos "os" 12 "path" 13 "path/filepath" 14 "strings" 15 "text/template" 16 17 "github.com/juju/charm/v12" 18 "github.com/juju/errors" 19 "github.com/juju/featureflag" 20 "github.com/juju/loggo" 21 "github.com/juju/names/v5" 22 "github.com/juju/proxy" 23 "github.com/juju/utils/v3" 24 25 "github.com/juju/juju/agent" 26 "github.com/juju/juju/cloudconfig/cloudinit" 27 "github.com/juju/juju/core/os/ostype" 28 "github.com/juju/juju/environs/bootstrap" 29 "github.com/juju/juju/environs/simplestreams" 30 "github.com/juju/juju/juju/osenv" 31 ) 32 33 var logger = loggo.GetLogger("juju.cloudconfig") 34 35 const ( 36 // FileNameBootstrapParams is the name of bootstrap params file. 37 FileNameBootstrapParams = "bootstrap-params" 38 39 // curlCommand is the base curl command used to download tools. 40 curlCommand = "curl -sSf" 41 42 // toolsDownloadWaitTime is the number of seconds to wait between 43 // each iterations of download attempts. 44 toolsDownloadWaitTime = 15 45 46 // toolsDownloadTemplate is a bash template that generates a 47 // bash command to cycle through a list of URLs to download tools. 48 toolsDownloadTemplate = `{{$curl := .ToolsDownloadCommand}} 49 n=1 50 while true; do 51 {{range .URLs}} 52 echo "Attempt $n to download agent binaries from {{shquote .}}...\n" 53 {{$curl}} {{shquote .}} && echo "Agent binaries downloaded successfully." && break 54 {{end}} 55 echo "Download failed, retrying in {{.ToolsDownloadWaitTime}}s" 56 sleep {{.ToolsDownloadWaitTime}} 57 n=$((n+1)) 58 done` 59 60 // removeServicesScript is written to /sbin and can be used to remove 61 // all Juju services from a machine. 62 // Once this script is run, logic to check whether such a machine is already 63 // provisioned should return false and the machine can be reused as a target 64 // for either bootstrap or add-machine. 65 removeServicesScript = `#!/bin/bash 66 67 # WARNING 68 # This script will clean a host previously used to run a Juju controller/machine. 69 # Running this on a live installation will render Juju inoperable. 70 71 for path_to_unit in $(ls /etc/systemd/system/juju*); do 72 echo "removing juju service: $path_to_unit" 73 unit=$(basename "$path_to_unit") 74 systemctl stop "$unit" 75 systemctl disable "$unit" 76 systemctl daemon-reload 77 rm -f "$path_to_unit" 78 done 79 80 echo "removing /var/lib/juju/tools/*" 81 rm -rf /var/lib/juju/tools/* 82 83 echo "removing /var/lib/juju/db/*" 84 rm -rf /var/lib/juju/db/* 85 86 echo "removing /var/lib/juju/dqlite/*" 87 rm -rf /var/lib/juju/dqlite/* 88 89 echo "removing /var/lib/juju/raft/*" 90 rm -rf /var/lib/juju/raft/* 91 92 echo "removing /var/run/juju/*" 93 rm -rf /var/run/juju/* 94 95 has_juju_db_snap=$(snap info juju-db | grep installed:) 96 if [ ! -z "$has_juju_db_snap" ]; then 97 echo "removing juju-db snap and any persisted database data" 98 snap remove --purge juju-db 99 fi 100 ` 101 // We look to see if the proxy line is there already as 102 // the manual provider may have had it already. 103 // We write this file out whether we are using the legacy proxy 104 // or the juju proxy to deal with runtime changes. The proxy updater worker 105 // only modifies /etc/juju-proxy.conf, so if changes are written to that file 106 // we need to make sure the profile.d file exists to reflect these changes. 107 // If the new juju proxies are used, the legacy proxies will not be set, and the 108 // /etc/juju-proxy.conf file will be empty. 109 JujuProxyProfileScript = ` 110 if [ ! -e /etc/profile.d/juju-proxy.sh ]; then 111 ( 112 echo 113 echo '# Added by juju' 114 echo 115 echo '[ -f /etc/juju-proxy.conf ] && . /etc/juju-proxy.conf' 116 echo 117 ) >> /etc/profile.d/juju-proxy.sh 118 fi 119 ` 120 ) 121 122 var ( 123 // UbuntuGroups is the set of unix groups to add the "ubuntu" user to 124 // when initializing an Ubuntu system. 125 UbuntuGroups = []string{"adm", "audio", "cdrom", "dialout", "dip", 126 "floppy", "netdev", "plugdev", "sudo", "video"} 127 128 // CentOSGroups is the set of unix groups to add the "ubuntu" user to 129 // when initializing a CentOS system. 130 CentOSGroups = []string{"adm", "systemd-journal", "wheel"} 131 ) 132 133 type unixConfigure struct { 134 baseConfigure 135 } 136 137 // TODO(ericsnow) Move Configure to the baseConfigure type? 138 139 // Configure updates the provided cloudinit.Config with 140 // configuration to initialize a Juju machine agent. 141 func (w *unixConfigure) Configure() error { 142 if err := w.ConfigureBasic(); err != nil { 143 return err 144 } 145 if err := w.ConfigureJuju(); err != nil { 146 return err 147 } 148 return w.ConfigureCustomOverrides() 149 } 150 151 // ConfigureBasic updates the provided cloudinit.Config with 152 // basic configuration to initialise an OS image, such that it can 153 // be connected to via SSH, and log to a standard location. 154 // 155 // Any potentially failing operation should not be added to the 156 // configuration, but should instead be done in ConfigureJuju. 157 // 158 // Note: we don't do apt update/upgrade here so as not to have to wait on 159 // apt to finish when performing the second half of image initialisation. 160 // Doing it later brings the benefit of feedback in the face of errors, 161 // but adds to the running time of initialisation due to lack of activity 162 // between image bringup and start of agent installation. 163 func (w *unixConfigure) ConfigureBasic() error { 164 // Keep preruncmd at the beginning of any runcmd's that juju adds 165 if preruncmds, ok := w.icfg.CloudInitUserData["preruncmd"].([]interface{}); ok { 166 for i := len(preruncmds) - 1; i >= 0; i -= 1 { 167 cmd, err := runCmdToString(preruncmds[i]) 168 if err != nil { 169 return errors.Annotate(err, "invalid preruncmd") 170 } 171 w.conf.PrependRunCmd(cmd) 172 } 173 } 174 w.conf.AddRunCmd( 175 "set -xe", // ensure we run all the scripts or abort. 176 ) 177 switch w.os { 178 case ostype.CentOS: 179 w.conf.AddScripts( 180 // Mask and stop firewalld, if enabled, so it cannot start. See 181 // http://pad.lv/1492066. firewalld might be missing, in which case 182 // is-enabled and is-active prints an error, which is why the output 183 // is suppressed. 184 "systemctl is-enabled firewalld &> /dev/null && systemctl mask firewalld || true", 185 "systemctl is-active firewalld &> /dev/null && systemctl stop firewalld || true", 186 187 `sed -i "s/^.*requiretty/#Defaults requiretty/" /etc/sudoers`, 188 ) 189 } 190 SetUbuntuUser(w.conf, w.icfg.AuthorizedKeys) 191 192 if w.icfg.Bootstrap != nil { 193 // For the bootstrap machine only, we set the host keys 194 // except when manually provisioning. 195 icfgKeys := w.icfg.Bootstrap.InitialSSHHostKeys 196 var keys cloudinit.SSHKeys 197 for _, hostKey := range icfgKeys { 198 keys = append(keys, cloudinit.SSHKey{ 199 Private: hostKey.Private, 200 Public: hostKey.Public, 201 PublicKeyAlgorithm: hostKey.PublicKeyAlgorithm, 202 }) 203 } 204 err := w.conf.SetSSHKeys(keys) 205 if err != nil { 206 return errors.Annotate(err, "setting ssh keys") 207 } 208 } 209 210 w.conf.SetOutput(cloudinit.OutAll, "| tee -a "+w.icfg.CloudInitOutputLog, "") 211 // Create a file in a well-defined location containing the machine's 212 // nonce. The presence and contents of this file will be verified 213 // during bootstrap. 214 // 215 // Note: this must be the last runcmd we do in ConfigureBasic, as 216 // the presence of the nonce file is used to gate the remainder 217 // of synchronous bootstrap. 218 noncefile := path.Join(w.icfg.DataDir, NonceFile) 219 w.conf.AddRunTextFile(noncefile, w.icfg.MachineNonce, 0644) 220 return nil 221 } 222 223 func (w *unixConfigure) setDataDirPermissions() string { 224 var user string 225 switch w.os { 226 case ostype.CentOS: 227 user = "root" 228 default: 229 user = "syslog" 230 } 231 return fmt.Sprintf("chown %s:adm %s", user, w.icfg.LogDir) 232 } 233 234 // ConfigureJuju updates the provided cloudinit.Config with configuration 235 // to initialise a Juju machine agent. 236 func (w *unixConfigure) ConfigureJuju() error { 237 if err := w.icfg.VerifyConfig(); err != nil { 238 return err 239 } 240 241 // To keep postruncmd at the end of any runcmd's that juju adds, 242 // this block must stay at the top. 243 if postruncmds, ok := w.icfg.CloudInitUserData["postruncmd"].([]interface{}); ok { 244 245 // revert the `set -xe` shell flag which was set after preruncmd 246 // LP: #1978454 247 w.conf.AddRunCmd("set +xe") 248 cmds := make([]string, len(postruncmds)) 249 for i, v := range postruncmds { 250 cmd, err := runCmdToString(v) 251 if err != nil { 252 return errors.Annotate(err, "invalid postruncmd") 253 } 254 cmds[i] = cmd 255 } 256 defer w.conf.AddScripts(cmds...) 257 } 258 259 // Initialise progress reporting. We need to do separately for runcmd 260 // and (possibly, below) for bootcmd, as they may be run in different 261 // shell sessions. 262 initProgressCmd := cloudinit.InitProgressCmd() 263 w.conf.AddRunCmd(initProgressCmd) 264 265 // If we're doing synchronous bootstrap or manual provisioning, then 266 // ConfigureBasic won't have been invoked; thus, the output log won't 267 // have been set. We don't want to show the log to the user, so simply 268 // append to the log file rather than teeing. 269 if stdout, _ := w.conf.Output(cloudinit.OutAll); stdout == "" { 270 w.conf.SetOutput(cloudinit.OutAll, ">> "+w.icfg.CloudInitOutputLog, "") 271 w.conf.AddBootCmd(initProgressCmd) 272 w.conf.AddBootCmd(cloudinit.LogProgressCmd("Logging to %s on the bootstrap machine", w.icfg.CloudInitOutputLog)) 273 } 274 275 if w.icfg.Bootstrap != nil && len(w.icfg.Bootstrap.InitialSSHHostKeys) > 0 { 276 // Before anything else, we must regenerate the SSH host keys. 277 // During bootstrap we provide our own keys, but to prevent keys being 278 // sniffed from metadata by user applications that shouldn't have access, 279 // we regenerate them. 280 w.conf.AddBootCmd(cloudinit.LogProgressCmd("Regenerating SSH host keys")) 281 w.conf.AddBootCmd(`rm /etc/ssh/ssh_host_*_key*`) 282 w.conf.AddBootCmd(`ssh-keygen -t rsa -N "" -f /etc/ssh/ssh_host_rsa_key`) 283 w.conf.AddBootCmd(`ssh-keygen -t ecdsa -N "" -f /etc/ssh/ssh_host_ecdsa_key`) 284 // We drop DSA due to it not really being supported by default anymore, 285 // we also softly fail on ed25519 as it may not be supported by the target 286 // machine. 287 w.conf.AddBootCmd(`ssh-keygen -t ed25519 -N "" -f /etc/ssh/ssh_host_ed25519_key || true`) 288 } 289 290 if err := w.conf.AddPackageCommands( 291 packageManagerProxySettings{ 292 aptProxy: w.icfg.AptProxySettings, 293 aptMirror: w.icfg.AptMirror, 294 snapProxy: w.icfg.SnapProxySettings, 295 snapStoreAssertions: w.icfg.SnapStoreAssertions, 296 snapStoreProxyID: w.icfg.SnapStoreProxyID, 297 snapStoreProxyURL: w.icfg.SnapStoreProxyURL, 298 }, 299 w.icfg.EnableOSRefreshUpdate, 300 w.icfg.EnableOSUpgrade, 301 ); err != nil { 302 return errors.Trace(err) 303 } 304 305 // Write out the normal proxy settings so that the settings are 306 // sourced by bash, and ssh through that. 307 w.conf.AddScripts(JujuProxyProfileScript) 308 if w.icfg.LegacyProxySettings.HasProxySet() { 309 exportedProxyEnv := w.icfg.LegacyProxySettings.AsScriptEnvironment() 310 w.conf.AddScripts(strings.Split(exportedProxyEnv, "\n")...) 311 w.conf.AddScripts( 312 fmt.Sprintf( 313 `(echo %s > /etc/juju-proxy.conf && chmod 0644 /etc/juju-proxy.conf)`, 314 shquote(w.icfg.LegacyProxySettings.AsScriptEnvironment()))) 315 316 // Write out systemd proxy settings 317 w.conf.AddScripts(fmt.Sprintf(`echo %[1]s > /etc/juju-proxy-systemd.conf`, 318 shquote(w.icfg.LegacyProxySettings.AsSystemdDefaultEnv()))) 319 } 320 321 if w.icfg.PublicImageSigningKey != "" { 322 keyFile := filepath.Join(agent.DefaultPaths.ConfDir, simplestreams.SimplestreamsPublicKeyFile) 323 w.conf.AddRunTextFile(keyFile, w.icfg.PublicImageSigningKey, 0644) 324 } 325 326 // Make the lock dir and change the ownership of the lock dir itself to 327 // ubuntu:ubuntu from root:root so the juju-exec command run as the ubuntu 328 // user is able to get access to the hook execution lock (like the uniter 329 // itself does.) 330 lockDir := path.Join(w.icfg.DataDir, "locks") 331 w.conf.AddScripts( 332 fmt.Sprintf("mkdir -p %s", lockDir), 333 // We only try to change ownership if there is an ubuntu user defined. 334 fmt.Sprintf("(id ubuntu &> /dev/null) && chown ubuntu:ubuntu %s", lockDir), 335 fmt.Sprintf("mkdir -p %s", w.icfg.LogDir), 336 w.setDataDirPermissions(), 337 ) 338 339 // Make a directory for the tools to live in. 340 w.conf.AddScripts( 341 "bin="+shquote(w.icfg.JujuTools()), 342 "mkdir -p $bin", 343 ) 344 345 // Fetch the tools and unarchive them into it. 346 if err := w.addDownloadToolsCmds(); err != nil { 347 return errors.Trace(err) 348 } 349 350 // Don't remove tools tarball until after bootstrap agent 351 // runs, so it has a chance to add it to its catalogue. 352 defer w.conf.AddRunCmd( 353 fmt.Sprintf("rm $bin/tools.tar.gz && rm $bin/juju%s.sha256", w.icfg.AgentVersion()), 354 ) 355 356 // We add the machine agent's configuration info 357 // before running bootstrap-state so that bootstrap-state 358 // has a chance to rewrite it to change the password. 359 // It would be cleaner to change bootstrap-state to 360 // be responsible for starting the machine agent itself, 361 // but this would not be backwardly compatible. 362 machineTag := names.NewMachineTag(w.icfg.MachineId) 363 _, err := w.addAgentInfo(machineTag) 364 if err != nil { 365 return errors.Trace(err) 366 } 367 368 if w.icfg.Bootstrap != nil { 369 if err = w.addLocalSnapUpload(); err != nil { 370 return errors.Trace(err) 371 } 372 if err = w.addLocalControllerCharmsUpload(); err != nil { 373 return errors.Trace(err) 374 } 375 if err := w.configureBootstrap(); err != nil { 376 return errors.Trace(err) 377 } 378 } 379 380 // Append cloudinit-userdata packages to the end of the juju created ones. 381 if packagesToAdd, ok := w.icfg.CloudInitUserData["packages"].([]interface{}); ok { 382 for _, v := range packagesToAdd { 383 if pack, ok := v.(string); ok { 384 w.conf.AddPackage(pack) 385 } 386 } 387 } 388 389 w.conf.AddRunTextFile("/sbin/remove-juju-services", removeServicesScript, 0755) 390 391 return w.addMachineAgentToBoot() 392 } 393 394 // runCmdToString converts a postruncmd or preruncmd value to a string. 395 // Per https://cloudinit.readthedocs.io/en/latest/topics/examples.html, 396 // these run commands can be either a string or a list of strings. 397 func runCmdToString(v any) (string, error) { 398 switch v := v.(type) { 399 case string: 400 return v, nil 401 case []any: // beware! won't be be []string 402 strs := make([]string, len(v)) 403 for i, sv := range v { 404 ss, ok := sv.(string) 405 if !ok { 406 return "", errors.Errorf("expected list of strings, got list containing %T", sv) 407 } 408 strs[i] = ss 409 } 410 return utils.CommandString(strs...), nil 411 default: 412 return "", errors.Errorf("expected string or list of strings, got %T", v) 413 } 414 } 415 416 // Not all cloudinit-userdata attr are allowed to override, these attr have been 417 // dealt with in ConfigureBasic() and ConfigureJuju(). 418 func isAllowedOverrideAttr(attr string) bool { 419 switch attr { 420 case "packages", "preruncmd", "postruncmd": 421 return false 422 } 423 return true 424 } 425 426 func (w *unixConfigure) formatCurlProxyArguments() (proxyArgs string) { 427 tools := w.icfg.ToolsList()[0] 428 var proxySettings proxy.Settings 429 if w.icfg.JujuProxySettings.HasProxySet() { 430 proxySettings = w.icfg.JujuProxySettings 431 } else if w.icfg.LegacyProxySettings.HasProxySet() { 432 proxySettings = w.icfg.LegacyProxySettings 433 } 434 if strings.HasPrefix(tools.URL, httpSchemePrefix) && proxySettings.Http != "" { 435 proxyUrl := proxySettings.Http 436 proxyArgs += fmt.Sprintf(" --proxy %s", proxyUrl) 437 } else if strings.HasPrefix(tools.URL, httpsSchemePrefix) && proxySettings.Https != "" { 438 proxyUrl := proxySettings.Https 439 // curl automatically uses HTTP CONNECT for URLs containing HTTPS 440 proxyArgs += fmt.Sprintf(" --proxy %s", proxyUrl) 441 } 442 if proxySettings.NoProxy != "" { 443 proxyArgs += fmt.Sprintf(" --noproxy %s", proxySettings.NoProxy) 444 } 445 return 446 } 447 448 // ConfigureCustomOverrides implements UserdataConfig.ConfigureCustomOverrides 449 func (w *unixConfigure) ConfigureCustomOverrides() error { 450 for k, v := range w.icfg.CloudInitUserData { 451 // preruncmd was handled in ConfigureBasic() 452 // packages and postruncmd have been handled in ConfigureJuju() 453 if isAllowedOverrideAttr(k) { 454 w.conf.SetAttr(k, v) 455 } 456 } 457 return nil 458 } 459 460 func (w *unixConfigure) configureBootstrap() error { 461 bootstrapParamsFile := path.Join(w.icfg.DataDir, FileNameBootstrapParams) 462 bootstrapParams, err := w.icfg.Bootstrap.StateInitializationParams.Marshal() 463 if err != nil { 464 return errors.Annotate(err, "marshalling bootstrap params") 465 } 466 w.conf.AddRunTextFile(bootstrapParamsFile, string(bootstrapParams), 0600) 467 468 loggingOption := "--show-log" 469 if loggo.GetLogger("").LogLevel() == loggo.DEBUG { 470 // If the bootstrap command was requested with --debug, then the root 471 // logger will be set to DEBUG. If it is, then we use --debug here too. 472 loggingOption = "--debug" 473 } 474 featureFlags := featureflag.AsEnvironmentValue() 475 if featureFlags != "" { 476 featureFlags = fmt.Sprintf("%s=%s ", osenv.JujuFeatureFlagEnvKey, featureFlags) 477 } 478 bootstrapAgentArgs := []string{ 479 featureFlags + w.icfg.JujuTools() + "/jujud", 480 "bootstrap-state", 481 "--timeout", w.icfg.Bootstrap.Timeout.String(), 482 "--data-dir", shquote(w.icfg.DataDir), 483 loggingOption, 484 shquote(bootstrapParamsFile), 485 } 486 w.conf.AddRunCmd(cloudinit.LogProgressCmd("Installing Juju machine agent")) 487 w.conf.AddScripts(strings.Join(bootstrapAgentArgs, " ")) 488 489 return nil 490 } 491 492 func (w *unixConfigure) addLocalSnapUpload() error { 493 if w.icfg.Bootstrap == nil { 494 return nil 495 } 496 497 snapPath := w.icfg.Bootstrap.JujuDbSnapPath 498 assertionsPath := w.icfg.Bootstrap.JujuDbSnapAssertionsPath 499 500 if snapPath == "" { 501 return nil 502 } 503 504 logger.Infof("preparing to upload juju-db snap from %v", snapPath) 505 snapData, err := stdos.ReadFile(snapPath) 506 if err != nil { 507 return errors.Trace(err) 508 } 509 _, snapName := path.Split(snapPath) 510 w.conf.AddRunBinaryFile(path.Join(w.icfg.SnapDir(), snapName), snapData, 0644) 511 512 logger.Infof("preparing to upload juju-db assertions from %v", assertionsPath) 513 snapAssertionsData, err := stdos.ReadFile(assertionsPath) 514 if err != nil { 515 return errors.Trace(err) 516 } 517 _, snapAssertionsName := path.Split(assertionsPath) 518 w.conf.AddRunBinaryFile(path.Join(w.icfg.SnapDir(), snapAssertionsName), snapAssertionsData, 0644) 519 520 return nil 521 } 522 523 func (w *unixConfigure) addLocalControllerCharmsUpload() error { 524 if w.icfg.Bootstrap == nil { 525 return nil 526 } 527 528 charmPath := w.icfg.Bootstrap.ControllerCharm 529 530 if charmPath == "" { 531 return nil 532 } 533 534 logger.Infof("preparing to upload controller charm from %v", charmPath) 535 _, err := charm.ReadCharm(charmPath) 536 if err != nil { 537 return errors.Trace(err) 538 } 539 var charmData []byte 540 if charm.IsCharmDir(charmPath) { 541 ch, err := charm.ReadCharmDir(charmPath) 542 if err != nil { 543 return errors.Trace(err) 544 } 545 buf := bytes.NewBuffer(nil) 546 err = ch.ArchiveTo(buf) 547 if err != nil { 548 return errors.Trace(err) 549 } 550 charmData = buf.Bytes() 551 } else { 552 charmData, err = stdos.ReadFile(charmPath) 553 if err != nil { 554 return errors.Trace(err) 555 } 556 } 557 w.conf.AddRunBinaryFile(path.Join(w.icfg.CharmDir(), bootstrap.ControllerCharmArchive), charmData, 0644) 558 559 return nil 560 } 561 562 func (w *unixConfigure) addDownloadToolsCmds() error { 563 tools := w.icfg.ToolsList()[0] 564 if strings.HasPrefix(tools.URL, fileSchemePrefix) { 565 toolsData, err := stdos.ReadFile(tools.URL[len(fileSchemePrefix):]) 566 if err != nil { 567 return err 568 } 569 w.conf.AddRunBinaryFile(path.Join(w.icfg.JujuTools(), "tools.tar.gz"), toolsData, 0644) 570 } else { 571 curlCommand := curlCommand 572 var urls []string 573 for _, tools := range w.icfg.ToolsList() { 574 urls = append(urls, tools.URL) 575 } 576 if w.icfg.Bootstrap != nil { 577 curlCommand += " --retry 10" 578 if w.icfg.DisableSSLHostnameVerification { 579 curlCommand += " --insecure" 580 } 581 582 curlProxyArgs := w.formatCurlProxyArguments() 583 curlCommand += curlProxyArgs 584 } else { 585 // Allow up to 20 seconds for curl to make a connection. This prevents 586 // slow/broken routes from holding up others. 587 // 588 // TODO(axw) 2017-02-14 #1654943 589 // When we model spaces everywhere, we should give 590 // priority to the URLs that we know are accessible 591 // based on space overlap. 592 curlCommand += " --connect-timeout 20" 593 594 // Don't go through the proxy when downloading tools from the controllers 595 curlCommand += ` --noproxy "*"` 596 597 // Our API server certificates are unusable by curl (invalid subject name), 598 // so we must disable certificate validation. It doesn't actually 599 // matter, because there is no sensitive information being transmitted 600 // and we verify the tools' hash after. 601 curlCommand += " --insecure" 602 } 603 curlCommand += " -o $bin/tools.tar.gz" 604 w.conf.AddRunCmd(cloudinit.LogProgressCmd("Fetching Juju agent version %s for %s", tools.Version.Number, tools.Version.Arch)) 605 logger.Infof("Fetching agent: %s <%s>", curlCommand, urls) 606 w.conf.AddRunCmd(toolsDownloadCommand(curlCommand, urls)) 607 } 608 609 w.conf.AddScripts( 610 fmt.Sprintf("sha256sum $bin/tools.tar.gz > $bin/juju%s.sha256", tools.Version), 611 fmt.Sprintf(`grep '%s' $bin/juju%s.sha256 || (echo "Tools checksum mismatch"; exit 1)`, 612 tools.SHA256, tools.Version), 613 "tar zxf $bin/tools.tar.gz -C $bin", 614 ) 615 616 toolsJson, err := json.Marshal(tools) 617 if err != nil { 618 return err 619 } 620 w.conf.AddScripts( 621 fmt.Sprintf("echo -n %s > $bin/downloaded-tools.txt", shquote(string(toolsJson))), 622 ) 623 624 return nil 625 } 626 627 // toolsDownloadCommand takes a curl command minus the source URL, 628 // and generates a command that will cycle through the URLs until 629 // one succeeds. 630 func toolsDownloadCommand(curlCommand string, urls []string) string { 631 parsedTemplate := template.Must( 632 template.New("ToolsDownload").Funcs( 633 template.FuncMap{"shquote": shquote}, 634 ).Parse(toolsDownloadTemplate), 635 ) 636 var buf bytes.Buffer 637 err := parsedTemplate.Execute(&buf, map[string]interface{}{ 638 "ToolsDownloadCommand": curlCommand, 639 "ToolsDownloadWaitTime": toolsDownloadWaitTime, 640 "URLs": urls, 641 }) 642 if err != nil { 643 panic(errors.Annotate(err, "agent binaries download template error")) 644 } 645 return buf.String() 646 }