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  }