github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/cloudinit/sshinit/configure.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package sshinit
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"strings"
    10  
    11  	"github.com/juju/loggo"
    12  	"github.com/juju/utils"
    13  
    14  	"github.com/juju/juju/cloudinit"
    15  	"github.com/juju/juju/utils/ssh"
    16  )
    17  
    18  var logger = loggo.GetLogger("juju.cloudinit.sshinit")
    19  
    20  type ConfigureParams struct {
    21  	// Host is the host to configure, in the format [user@]hostname.
    22  	Host string
    23  
    24  	// Client is the SSH client to connect with.
    25  	// If Client is nil, ssh.DefaultClient will be used.
    26  	Client ssh.Client
    27  
    28  	// Config is the cloudinit config to carry out.
    29  	Config *cloudinit.Config
    30  
    31  	// ProgressWriter is an io.Writer to which progress will be written,
    32  	// for realtime feedback.
    33  	ProgressWriter io.Writer
    34  }
    35  
    36  // Configure connects to the specified host over SSH,
    37  // and executes a script that carries out cloud-config.
    38  func Configure(params ConfigureParams) error {
    39  	logger.Infof("Provisioning machine agent on %s", params.Host)
    40  	script, err := ConfigureScript(params.Config)
    41  	if err != nil {
    42  		return err
    43  	}
    44  	return RunConfigureScript(script, params)
    45  }
    46  
    47  // RunConfigureScript connects to the specified host over
    48  // SSH, and executes the provided script which is expected
    49  // to have been returned by ConfigureScript.
    50  func RunConfigureScript(script string, params ConfigureParams) error {
    51  	logger.Tracef("Running script on %s: %s", params.Host, script)
    52  	client := params.Client
    53  	if client == nil {
    54  		client = ssh.DefaultClient
    55  	}
    56  	cmd := ssh.Command(params.Host, []string{"sudo", "/bin/bash"}, nil)
    57  	cmd.Stdin = strings.NewReader(script)
    58  	cmd.Stderr = params.ProgressWriter
    59  	return cmd.Run()
    60  }
    61  
    62  // ConfigureScript generates the bash script that applies
    63  // the specified cloud-config.
    64  func ConfigureScript(cloudcfg *cloudinit.Config) (string, error) {
    65  	if cloudcfg == nil {
    66  		panic("cloudcfg is nil")
    67  	}
    68  
    69  	// TODO(axw): 2013-08-23 bug 1215777
    70  	// Carry out configuration for ssh-keys-per-user,
    71  	// machine-updates-authkeys, using cloud-init config.
    72  	//
    73  	// We should work with smoser to get a supported
    74  	// command in (or next to) cloud-init for manually
    75  	// invoking cloud-config. This would address the
    76  	// above comment by removing the need to generate a
    77  	// script "by hand".
    78  
    79  	// Bootcmds must be run before anything else,
    80  	// as they may affect package installation.
    81  	bootcmds, err := cmdlist(cloudcfg.BootCmds())
    82  	if err != nil {
    83  		return "", err
    84  	}
    85  
    86  	// Depending on cloudcfg, potentially add package sources and packages.
    87  	pkgcmds, err := addPackageCommands(cloudcfg)
    88  	if err != nil {
    89  		return "", err
    90  	}
    91  
    92  	// Runcmds come last.
    93  	runcmds, err := cmdlist(cloudcfg.RunCmds())
    94  	if err != nil {
    95  		return "", err
    96  	}
    97  
    98  	// We prepend "set -xe". This is already in runcmds,
    99  	// but added here to avoid relying on that to be
   100  	// invariant.
   101  	script := []string{"#!/bin/bash", "set -e"}
   102  	// We must initialise progress reporting before entering
   103  	// the subshell and redirecting stderr.
   104  	script = append(script, cloudinit.InitProgressCmd())
   105  	stdout, stderr := cloudcfg.Output(cloudinit.OutAll)
   106  	script = append(script, "(")
   107  	if stderr != "" {
   108  		script = append(script, "(")
   109  	}
   110  	script = append(script, bootcmds...)
   111  	script = append(script, pkgcmds...)
   112  	script = append(script, runcmds...)
   113  	if stderr != "" {
   114  		script = append(script, ") "+stdout)
   115  		script = append(script, ") "+stderr)
   116  	} else {
   117  		script = append(script, ") "+stdout+" 2>&1")
   118  	}
   119  	return strings.Join(script, "\n"), nil
   120  }
   121  
   122  // The options specified are to prevent any kind of prompting.
   123  //  * --assume-yes answers yes to any yes/no question in apt-get;
   124  //  * the --force-confold option is passed to dpkg, and tells dpkg
   125  //    to always keep old configuration files in the face of change.
   126  const aptget = "apt-get --option Dpkg::Options::=--force-confold --assume-yes "
   127  
   128  // aptgetLoopFunction is a bash function that executes its arguments
   129  // in a loop with a delay until either the command either returns
   130  // with an exit code other than 100.
   131  const aptgetLoopFunction = `
   132  function apt_get_loop {
   133      local rc=
   134      while true; do
   135          if ($*); then
   136                  return 0
   137          else
   138                  rc=$?
   139          fi
   140          if [ $rc -eq 100 ]; then
   141  		sleep 10s
   142                  continue
   143          fi
   144          return $rc
   145      done
   146  }
   147  `
   148  
   149  // addPackageCommands returns a slice of commands that, when run,
   150  // will add the required apt repositories and packages.
   151  func addPackageCommands(cfg *cloudinit.Config) ([]string, error) {
   152  	if cfg == nil {
   153  		panic("cfg is nil")
   154  	} else if !cfg.AptUpdate() && len(cfg.AptSources()) > 0 {
   155  		return nil, fmt.Errorf("update sources were specified, but OS updates have been disabled.")
   156  	}
   157  
   158  	// If apt_get_wrapper is specified, then prepend it to aptget.
   159  	aptget := aptget
   160  	wrapper := cfg.AptGetWrapper()
   161  	switch wrapper.Enabled {
   162  	case true:
   163  		aptget = utils.ShQuote(wrapper.Command) + " " + aptget
   164  	case "auto":
   165  		aptget = fmt.Sprintf("$(which %s || true) %s", utils.ShQuote(wrapper.Command), aptget)
   166  	}
   167  
   168  	var cmds []string
   169  
   170  	// If a mirror is specified, rewrite sources.list and rename cached index files.
   171  	if newMirror, _ := cfg.AptMirror(); newMirror != "" {
   172  		cmds = append(cmds, cloudinit.LogProgressCmd("Changing apt mirror to "+newMirror))
   173  		cmds = append(cmds, "old_mirror=$("+extractAptSource+")")
   174  		cmds = append(cmds, "new_mirror="+newMirror)
   175  		cmds = append(cmds, `sed -i s,$old_mirror,$new_mirror, `+aptSourcesList)
   176  		cmds = append(cmds, renameAptListFilesCommands("$new_mirror", "$old_mirror")...)
   177  	}
   178  
   179  	if len(cfg.AptSources()) > 0 {
   180  		// Ensure add-apt-repository is available.
   181  		cmds = append(cmds, cloudinit.LogProgressCmd("Installing add-apt-repository"))
   182  		cmds = append(cmds, aptget+"install python-software-properties")
   183  	}
   184  	for _, src := range cfg.AptSources() {
   185  		// PPA keys are obtained by add-apt-repository, from launchpad.
   186  		if !strings.HasPrefix(src.Source, "ppa:") {
   187  			if src.Key != "" {
   188  				key := utils.ShQuote(src.Key)
   189  				cmd := fmt.Sprintf("printf '%%s\\n' %s | apt-key add -", key)
   190  				cmds = append(cmds, cmd)
   191  			}
   192  		}
   193  		cmds = append(cmds, cloudinit.LogProgressCmd("Adding apt repository: %s", src.Source))
   194  		cmds = append(cmds, "add-apt-repository -y "+utils.ShQuote(src.Source))
   195  		if src.Prefs != nil {
   196  			path := utils.ShQuote(src.Prefs.Path)
   197  			contents := utils.ShQuote(src.Prefs.FileContents())
   198  			cmds = append(cmds, "install -D -m 644 /dev/null "+path)
   199  			cmds = append(cmds, `printf '%s\n' `+contents+` > `+path)
   200  		}
   201  	}
   202  
   203  	// Define the "apt_get_loop" function, and wrap apt-get with it.
   204  	cmds = append(cmds, aptgetLoopFunction)
   205  	aptget = "apt_get_loop " + aptget
   206  
   207  	if cfg.AptUpdate() {
   208  		cmds = append(cmds, cloudinit.LogProgressCmd("Running apt-get update"))
   209  		cmds = append(cmds, aptget+"update")
   210  	}
   211  	if cfg.AptUpgrade() {
   212  		cmds = append(cmds, cloudinit.LogProgressCmd("Running apt-get upgrade"))
   213  		cmds = append(cmds, aptget+"upgrade")
   214  	}
   215  	for _, pkg := range cfg.Packages() {
   216  		cmds = append(cmds, cloudinit.LogProgressCmd("Installing package: %s", pkg))
   217  		if !strings.Contains(pkg, "--target-release") {
   218  			// We only need to shquote the package name if it does not
   219  			// contain additional arguments.
   220  			pkg = utils.ShQuote(pkg)
   221  		}
   222  		cmd := fmt.Sprintf(aptget+"install %s", pkg)
   223  		cmds = append(cmds, cmd)
   224  	}
   225  	if len(cmds) > 0 {
   226  		// setting DEBIAN_FRONTEND=noninteractive prevents debconf
   227  		// from prompting, always taking default values instead.
   228  		cmds = append([]string{"export DEBIAN_FRONTEND=noninteractive"}, cmds...)
   229  	}
   230  	return cmds, nil
   231  }
   232  
   233  func cmdlist(cmds []interface{}) ([]string, error) {
   234  	result := make([]string, 0, len(cmds))
   235  	for _, cmd := range cmds {
   236  		switch cmd := cmd.(type) {
   237  		case []string:
   238  			// Quote args, so shell meta-characters are not interpreted.
   239  			for i, arg := range cmd[1:] {
   240  				cmd[i] = utils.ShQuote(arg)
   241  			}
   242  			result = append(result, strings.Join(cmd, " "))
   243  		case string:
   244  			result = append(result, cmd)
   245  		default:
   246  			return nil, fmt.Errorf("unexpected command type: %T", cmd)
   247  		}
   248  	}
   249  	return result, nil
   250  }