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