github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cloudconfig/userdatacfg_unix.go (about)

     1  // Copyright 2012, 2013, 2014, 2015 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  	"path"
    14  	"strings"
    15  	"text/template"
    16  	"time"
    17  
    18  	"github.com/juju/errors"
    19  	"github.com/juju/loggo"
    20  	"github.com/juju/names"
    21  	"github.com/juju/utils"
    22  	"github.com/juju/utils/proxy"
    23  	goyaml "gopkg.in/yaml.v1"
    24  
    25  	"github.com/juju/juju/cloudconfig/cloudinit"
    26  	"github.com/juju/juju/environs/config"
    27  	"github.com/juju/juju/environs/imagemetadata"
    28  	"github.com/juju/juju/service"
    29  	"github.com/juju/juju/service/systemd"
    30  	"github.com/juju/juju/service/upstart"
    31  	"github.com/juju/juju/version"
    32  )
    33  
    34  const (
    35  	// curlCommand is the base curl command used to download tools.
    36  	curlCommand = "curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s '"
    37  
    38  	// toolsDownloadAttempts is the number of attempts to make for
    39  	// each tools URL when downloading tools.
    40  	toolsDownloadAttempts = 5
    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  for n in $(seq {{.ToolsDownloadAttempts}}); do
    50  {{range .URLs}}
    51      printf "Attempt $n to download tools from %s...\n" {{shquote .}}
    52      {{$curl}} {{shquote .}} && echo "Tools downloaded successfully." && break
    53  {{end}}
    54      if [ $n -lt {{.ToolsDownloadAttempts}} ]; then
    55          echo "Download failed..... wait {{.ToolsDownloadWaitTime}}s"
    56      fi
    57      sleep {{.ToolsDownloadWaitTime}}
    58  done`
    59  )
    60  
    61  type unixConfigure struct {
    62  	baseConfigure
    63  }
    64  
    65  // TODO(ericsnow) Move Configure to the baseConfigure type?
    66  
    67  // Configure updates the provided cloudinit.Config with
    68  // configuration to initialize a Juju machine agent.
    69  func (w *unixConfigure) Configure() error {
    70  	if err := w.ConfigureBasic(); err != nil {
    71  		return err
    72  	}
    73  	return w.ConfigureJuju()
    74  }
    75  
    76  // ConfigureBasic updates the provided cloudinit.Config with
    77  // basic configuration to initialise an OS image, such that it can
    78  // be connected to via SSH, and log to a standard location.
    79  //
    80  // Any potentially failing operation should not be added to the
    81  // configuration, but should instead be done in ConfigureJuju.
    82  //
    83  // Note: we don't do apt update/upgrade here so as not to have to wait on
    84  // apt to finish when performing the second half of image initialisation.
    85  // Doing it later brings the benefit of feedback in the face of errors,
    86  // but adds to the running time of initialisation due to lack of activity
    87  // between image bringup and start of agent installation.
    88  func (w *unixConfigure) ConfigureBasic() error {
    89  	w.conf.AddScripts(
    90  		"set -xe", // ensure we run all the scripts or abort.
    91  	)
    92  	switch w.os {
    93  	case version.Ubuntu:
    94  		w.conf.AddSSHAuthorizedKeys(w.icfg.AuthorizedKeys)
    95  		if w.icfg.Tools != nil {
    96  			initSystem, err := service.VersionInitSystem(w.icfg.Series)
    97  			if err != nil {
    98  				return errors.Trace(err)
    99  			}
   100  			w.addCleanShutdownJob(initSystem)
   101  		}
   102  	// On unix systems that are not ubuntu we create an ubuntu user so that we
   103  	// are able to ssh in the machine and have all the functionality dependant
   104  	// on having an ubuntu user there.
   105  	// Hopefully in the future we are going to move all the distirbutions to
   106  	// having a "juju" user
   107  	case version.CentOS:
   108  		script := fmt.Sprintf(initUbuntuScript, utils.ShQuote(w.icfg.AuthorizedKeys))
   109  		w.conf.AddScripts(script)
   110  		w.conf.AddScripts("systemctl stop firewalld")
   111  		w.conf.AddScripts("systemctl disable firewalld")
   112  		w.conf.AddScripts(`sed -i "s/^.*requiretty/#Defaults requiretty/" /etc/sudoers`)
   113  		w.addCleanShutdownJob(service.InitSystemSystemd)
   114  	}
   115  	w.conf.SetOutput(cloudinit.OutAll, "| tee -a "+w.icfg.CloudInitOutputLog, "")
   116  	// Create a file in a well-defined location containing the machine's
   117  	// nonce. The presence and contents of this file will be verified
   118  	// during bootstrap.
   119  	//
   120  	// Note: this must be the last runcmd we do in ConfigureBasic, as
   121  	// the presence of the nonce file is used to gate the remainder
   122  	// of synchronous bootstrap.
   123  	noncefile := path.Join(w.icfg.DataDir, NonceFile)
   124  	w.conf.AddRunTextFile(noncefile, w.icfg.MachineNonce, 0644)
   125  	return nil
   126  }
   127  
   128  func (w *unixConfigure) addCleanShutdownJob(initSystem string) {
   129  	switch initSystem {
   130  	case service.InitSystemUpstart:
   131  		path, contents := upstart.CleanShutdownJobPath, upstart.CleanShutdownJob
   132  		w.conf.AddRunTextFile(path, contents, 0644)
   133  	case service.InitSystemSystemd:
   134  		path, contents := systemd.CleanShutdownServicePath, systemd.CleanShutdownService
   135  		w.conf.AddRunTextFile(path, contents, 0644)
   136  		w.conf.AddScripts(fmt.Sprintf("/bin/systemctl enable '%s'", path))
   137  	}
   138  }
   139  
   140  func (w *unixConfigure) setDataDirPermissions() string {
   141  	os, _ := version.GetOSFromSeries(w.icfg.Series)
   142  	var user string
   143  	switch os {
   144  	case version.CentOS:
   145  		user = "root"
   146  	default:
   147  		user = "syslog"
   148  	}
   149  	return fmt.Sprintf("chown %s:adm %s", user, w.icfg.LogDir)
   150  }
   151  
   152  // ConfigureJuju updates the provided cloudinit.Config with configuration
   153  // to initialise a Juju machine agent.
   154  func (w *unixConfigure) ConfigureJuju() error {
   155  	if err := w.icfg.VerifyConfig(); err != nil {
   156  		return err
   157  	}
   158  
   159  	// Initialise progress reporting. We need to do separately for runcmd
   160  	// and (possibly, below) for bootcmd, as they may be run in different
   161  	// shell sessions.
   162  	initProgressCmd := cloudinit.InitProgressCmd()
   163  	w.conf.AddRunCmd(initProgressCmd)
   164  
   165  	// If we're doing synchronous bootstrap or manual provisioning, then
   166  	// ConfigureBasic won't have been invoked; thus, the output log won't
   167  	// have been set. We don't want to show the log to the user, so simply
   168  	// append to the log file rather than teeing.
   169  	if stdout, _ := w.conf.Output(cloudinit.OutAll); stdout == "" {
   170  		w.conf.SetOutput(cloudinit.OutAll, ">> "+w.icfg.CloudInitOutputLog, "")
   171  		w.conf.AddBootCmd(initProgressCmd)
   172  		w.conf.AddBootCmd(cloudinit.LogProgressCmd("Logging to %s on remote host", w.icfg.CloudInitOutputLog))
   173  	}
   174  
   175  	w.conf.AddPackageCommands(
   176  		w.icfg.AptProxySettings,
   177  		w.icfg.AptMirror,
   178  		w.icfg.EnableOSRefreshUpdate,
   179  		w.icfg.EnableOSUpgrade,
   180  	)
   181  
   182  	// Write out the normal proxy settings so that the settings are
   183  	// sourced by bash, and ssh through that.
   184  	w.conf.AddScripts(
   185  		// We look to see if the proxy line is there already as
   186  		// the manual provider may have had it already. The ubuntu
   187  		// user may not exist (local provider only).
   188  		`([ ! -e /home/ubuntu/.profile ] || grep -q '.juju-proxy' /home/ubuntu/.profile) || ` +
   189  			`printf '\n# Added by juju\n[ -f "$HOME/.juju-proxy" ] && . "$HOME/.juju-proxy"\n' >> /home/ubuntu/.profile`)
   190  	if (w.icfg.ProxySettings != proxy.Settings{}) {
   191  		exportedProxyEnv := w.icfg.ProxySettings.AsScriptEnvironment()
   192  		w.conf.AddScripts(strings.Split(exportedProxyEnv, "\n")...)
   193  		w.conf.AddScripts(
   194  			fmt.Sprintf(
   195  				`(id ubuntu &> /dev/null) && (printf '%%s\n' %s > /home/ubuntu/.juju-proxy && chown ubuntu:ubuntu /home/ubuntu/.juju-proxy)`,
   196  				shquote(w.icfg.ProxySettings.AsScriptEnvironment())))
   197  	}
   198  
   199  	// Make the lock dir and change the ownership of the lock dir itself to
   200  	// ubuntu:ubuntu from root:root so the juju-run command run as the ubuntu
   201  	// user is able to get access to the hook execution lock (like the uniter
   202  	// itself does.)
   203  	lockDir := path.Join(w.icfg.DataDir, "locks")
   204  	w.conf.AddScripts(
   205  		fmt.Sprintf("mkdir -p %s", lockDir),
   206  		// We only try to change ownership if there is an ubuntu user defined.
   207  		fmt.Sprintf("(id ubuntu &> /dev/null) && chown ubuntu:ubuntu %s", lockDir),
   208  		fmt.Sprintf("mkdir -p %s", w.icfg.LogDir),
   209  		w.setDataDirPermissions(),
   210  	)
   211  
   212  	w.conf.AddScripts(
   213  		"bin="+shquote(w.icfg.JujuTools()),
   214  		"mkdir -p $bin",
   215  	)
   216  
   217  	// Make a directory for the tools to live in, then fetch the
   218  	// tools and unarchive them into it.
   219  	if strings.HasPrefix(w.icfg.Tools.URL, fileSchemePrefix) {
   220  		toolsData, err := ioutil.ReadFile(w.icfg.Tools.URL[len(fileSchemePrefix):])
   221  		if err != nil {
   222  			return err
   223  		}
   224  		w.conf.AddRunBinaryFile(path.Join(w.icfg.JujuTools(), "tools.tar.gz"), []byte(toolsData), 0644)
   225  	} else {
   226  		curlCommand := curlCommand
   227  		var urls []string
   228  		if w.icfg.Bootstrap {
   229  			curlCommand += " --retry 10"
   230  			if w.icfg.DisableSSLHostnameVerification {
   231  				curlCommand += " --insecure"
   232  			}
   233  			urls = append(urls, w.icfg.Tools.URL)
   234  		} else {
   235  			for _, addr := range w.icfg.ApiHostAddrs() {
   236  				// TODO(axw) encode env UUID in URL when EnvironTag
   237  				// is guaranteed to be available in APIInfo.
   238  				url := fmt.Sprintf("https://%s/tools/%s", addr, w.icfg.Tools.Version)
   239  				urls = append(urls, url)
   240  			}
   241  
   242  			// Don't go through the proxy when downloading tools from the state servers
   243  			curlCommand += ` --noproxy "*"`
   244  
   245  			// Our API server certificates are unusable by curl (invalid subject name),
   246  			// so we must disable certificate validation. It doesn't actually
   247  			// matter, because there is no sensitive information being transmitted
   248  			// and we verify the tools' hash after.
   249  			curlCommand += " --insecure"
   250  		}
   251  		curlCommand += " -o $bin/tools.tar.gz"
   252  		w.conf.AddRunCmd(cloudinit.LogProgressCmd("Fetching tools: %s <%s>", curlCommand, urls))
   253  		w.conf.AddRunCmd(toolsDownloadCommand(curlCommand, urls))
   254  	}
   255  	toolsJson, err := json.Marshal(w.icfg.Tools)
   256  	if err != nil {
   257  		return err
   258  	}
   259  
   260  	w.conf.AddScripts(
   261  		fmt.Sprintf("sha256sum $bin/tools.tar.gz > $bin/juju%s.sha256", w.icfg.Tools.Version),
   262  		fmt.Sprintf(`grep '%s' $bin/juju%s.sha256 || (echo "Tools checksum mismatch"; exit 1)`,
   263  			w.icfg.Tools.SHA256, w.icfg.Tools.Version),
   264  		fmt.Sprintf("tar zxf $bin/tools.tar.gz -C $bin"),
   265  		fmt.Sprintf("printf %%s %s > $bin/downloaded-tools.txt", shquote(string(toolsJson))),
   266  	)
   267  
   268  	// Don't remove tools tarball until after bootstrap agent
   269  	// runs, so it has a chance to add it to its catalogue.
   270  	defer w.conf.AddRunCmd(
   271  		fmt.Sprintf("rm $bin/tools.tar.gz && rm $bin/juju%s.sha256", w.icfg.Tools.Version),
   272  	)
   273  
   274  	// We add the machine agent's configuration info
   275  	// before running bootstrap-state so that bootstrap-state
   276  	// has a chance to rerwrite it to change the password.
   277  	// It would be cleaner to change bootstrap-state to
   278  	// be responsible for starting the machine agent itself,
   279  	// but this would not be backwardly compatible.
   280  	machineTag := names.NewMachineTag(w.icfg.MachineId)
   281  	_, err = w.addAgentInfo(machineTag)
   282  	if err != nil {
   283  		return errors.Trace(err)
   284  	}
   285  
   286  	// Add the cloud archive cloud-tools pocket to apt sources
   287  	// for series that need it. This gives us up-to-date LXC,
   288  	// MongoDB, and other infrastructure.
   289  	// This is only done on ubuntu.
   290  	if w.conf.SystemUpdate() && w.conf.RequiresCloudArchiveCloudTools() {
   291  		w.conf.AddCloudArchiveCloudTools()
   292  	}
   293  
   294  	if w.icfg.Bootstrap {
   295  		var metadataDir string
   296  		if len(w.icfg.CustomImageMetadata) > 0 {
   297  			metadataDir = path.Join(w.icfg.DataDir, "simplestreams")
   298  			index, products, err := imagemetadata.MarshalImageMetadataJSON(w.icfg.CustomImageMetadata, nil, time.Now())
   299  			if err != nil {
   300  				return err
   301  			}
   302  			indexFile := path.Join(metadataDir, imagemetadata.IndexStoragePath())
   303  			productFile := path.Join(metadataDir, imagemetadata.ProductMetadataStoragePath())
   304  			w.conf.AddRunTextFile(indexFile, string(index), 0644)
   305  			w.conf.AddRunTextFile(productFile, string(products), 0644)
   306  			metadataDir = "  --image-metadata " + shquote(metadataDir)
   307  		}
   308  
   309  		cons := w.icfg.Constraints.String()
   310  		if cons != "" {
   311  			cons = " --constraints " + shquote(cons)
   312  		}
   313  		var hardware string
   314  		if w.icfg.HardwareCharacteristics != nil {
   315  			if hardware = w.icfg.HardwareCharacteristics.String(); hardware != "" {
   316  				hardware = " --hardware " + shquote(hardware)
   317  			}
   318  		}
   319  		w.conf.AddRunCmd(cloudinit.LogProgressCmd("Bootstrapping Juju machine agent"))
   320  		loggingOption := " --show-log"
   321  		// If the bootstrap command was requsted with --debug, then the root
   322  		// logger will be set to DEBUG.  If it is, then we use --debug here too.
   323  		if loggo.GetLogger("").LogLevel() == loggo.DEBUG {
   324  			loggingOption = " --debug"
   325  		}
   326  		w.conf.AddScripts(
   327  			// The bootstrapping is always run with debug on.
   328  			w.icfg.JujuTools() + "/jujud bootstrap-state" +
   329  				" --data-dir " + shquote(w.icfg.DataDir) +
   330  				" --env-config " + shquote(base64yaml(w.icfg.Config)) +
   331  				" --instance-id " + shquote(string(w.icfg.InstanceId)) +
   332  				hardware +
   333  				cons +
   334  				metadataDir +
   335  				loggingOption,
   336  		)
   337  	}
   338  
   339  	return w.addMachineAgentToBoot()
   340  }
   341  
   342  // toolsDownloadCommand takes a curl command minus the source URL,
   343  // and generates a command that will cycle through the URLs until
   344  // one succeeds.
   345  func toolsDownloadCommand(curlCommand string, urls []string) string {
   346  	parsedTemplate := template.Must(
   347  		template.New("ToolsDownload").Funcs(
   348  			template.FuncMap{"shquote": shquote},
   349  		).Parse(toolsDownloadTemplate),
   350  	)
   351  	var buf bytes.Buffer
   352  	err := parsedTemplate.Execute(&buf, map[string]interface{}{
   353  		"ToolsDownloadCommand":  curlCommand,
   354  		"ToolsDownloadAttempts": toolsDownloadAttempts,
   355  		"ToolsDownloadWaitTime": toolsDownloadWaitTime,
   356  		"URLs":                  urls,
   357  	})
   358  	if err != nil {
   359  		panic(errors.Annotate(err, "tools download template error"))
   360  	}
   361  	return buf.String()
   362  }
   363  
   364  func base64yaml(m *config.Config) string {
   365  	data, err := goyaml.Marshal(m.AllAttrs())
   366  	if err != nil {
   367  		// can't happen, these values have been validated a number of times
   368  		panic(err)
   369  	}
   370  	return base64.StdEncoding.EncodeToString(data)
   371  }
   372  
   373  const initUbuntuScript = `
   374  set -e
   375  (id ubuntu &> /dev/null) || useradd -m ubuntu -s /bin/bash
   376  umask 0077
   377  temp=$(mktemp)
   378  echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > $temp
   379  install -m 0440 $temp /etc/sudoers.d/90-juju-ubuntu
   380  rm $temp
   381  su ubuntu -c 'install -D -m 0600 /dev/null ~/.ssh/authorized_keys'
   382  export authorized_keys=%s
   383  if [ ! -z "$authorized_keys" ]; then
   384      su ubuntu -c 'printf "%%s\n" "$authorized_keys" >> ~/.ssh/authorized_keys'
   385  fi`