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  }