launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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/loggo/loggo"
    12  
    13  	"launchpad.net/juju-core/cloudinit"
    14  	"launchpad.net/juju-core/utils"
    15  	"launchpad.net/juju-core/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.Debugf("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  	// TODO(axw): 2013-08-23 bug 1215777
    66  	// Carry out configuration for ssh-keys-per-user,
    67  	// machine-updates-authkeys, using cloud-init config.
    68  	//
    69  	// We should work with smoser to get a supported
    70  	// command in (or next to) cloud-init for manually
    71  	// invoking cloud-config. This would address the
    72  	// above comment by removing the need to generate a
    73  	// script "by hand".
    74  
    75  	// Bootcmds must be run before anything else,
    76  	// as they may affect package installation.
    77  	bootcmds, err := cmdlist(cloudcfg.BootCmds())
    78  	if err != nil {
    79  		return "", err
    80  	}
    81  
    82  	// Add package sources and packages.
    83  	pkgcmds, err := addPackageCommands(cloudcfg)
    84  	if err != nil {
    85  		return "", err
    86  	}
    87  
    88  	// Runcmds come last.
    89  	runcmds, err := cmdlist(cloudcfg.RunCmds())
    90  	if err != nil {
    91  		return "", err
    92  	}
    93  
    94  	// We prepend "set -xe". This is already in runcmds,
    95  	// but added here to avoid relying on that to be
    96  	// invariant.
    97  	script := []string{"#!/bin/bash", "set -e"}
    98  	// We must initialise progress reporting before entering
    99  	// the subshell and redirecting stderr.
   100  	script = append(script, cloudinit.InitProgressCmd())
   101  	stdout, stderr := cloudcfg.Output(cloudinit.OutAll)
   102  	script = append(script, "(")
   103  	if stderr != "" {
   104  		script = append(script, "(")
   105  	}
   106  	script = append(script, bootcmds...)
   107  	script = append(script, pkgcmds...)
   108  	script = append(script, runcmds...)
   109  	if stderr != "" {
   110  		script = append(script, ") "+stdout)
   111  		script = append(script, ") "+stderr)
   112  	} else {
   113  		script = append(script, ") "+stdout+" 2>&1")
   114  	}
   115  	return strings.Join(script, "\n"), nil
   116  }
   117  
   118  // The options specified are to prevent any kind of prompting.
   119  //  * --assume-yes answers yes to any yes/no question in apt-get;
   120  //  * the --force-confold option is passed to dpkg, and tells dpkg
   121  //    to always keep old configuration files in the face of change.
   122  const aptget = "apt-get --option Dpkg::Options::=--force-confold --assume-yes "
   123  
   124  // addPackageCommands returns a slice of commands that, when run,
   125  // will add the required apt repositories and packages.
   126  func addPackageCommands(cfg *cloudinit.Config) ([]string, error) {
   127  	var cmds []string
   128  	if len(cfg.AptSources()) > 0 {
   129  		// Ensure add-apt-repository is available.
   130  		cmds = append(cmds, cloudinit.LogProgressCmd("Installing add-apt-repository"))
   131  		cmds = append(cmds, aptget+"install python-software-properties")
   132  	}
   133  	for _, src := range cfg.AptSources() {
   134  		// PPA keys are obtained by add-apt-repository, from launchpad.
   135  		if !strings.HasPrefix(src.Source, "ppa:") {
   136  			if src.Key != "" {
   137  				key := utils.ShQuote(src.Key)
   138  				cmd := fmt.Sprintf("printf '%%s\\n' %s | apt-key add -", key)
   139  				cmds = append(cmds, cmd)
   140  			}
   141  		}
   142  		cmds = append(cmds, cloudinit.LogProgressCmd("Adding apt repository: %s", src.Source))
   143  		cmds = append(cmds, "add-apt-repository -y "+utils.ShQuote(src.Source))
   144  		if src.Prefs != nil {
   145  			path := utils.ShQuote(src.Prefs.Path)
   146  			contents := utils.ShQuote(src.Prefs.FileContents())
   147  			cmds = append(cmds, "install -D -m 644 /dev/null "+path)
   148  			cmds = append(cmds, `printf '%s\n' `+contents+` > `+path)
   149  		}
   150  	}
   151  	if len(cfg.AptSources()) > 0 || cfg.AptUpdate() {
   152  		cmds = append(cmds, cloudinit.LogProgressCmd("Running apt-get update"))
   153  		cmds = append(cmds, aptget+"update")
   154  	}
   155  	if cfg.AptUpgrade() {
   156  		cmds = append(cmds, cloudinit.LogProgressCmd("Running apt-get upgrade"))
   157  		cmds = append(cmds, aptget+"upgrade")
   158  	}
   159  	for _, pkg := range cfg.Packages() {
   160  		cmds = append(cmds, cloudinit.LogProgressCmd("Installing package: %s", pkg))
   161  		if !strings.Contains(pkg, "--target-release") {
   162  			// We only need to shquote the package name if it does not
   163  			// contain additional arguments.
   164  			pkg = utils.ShQuote(pkg)
   165  		}
   166  		cmd := fmt.Sprintf(aptget+"install %s", pkg)
   167  		cmds = append(cmds, cmd)
   168  	}
   169  	if len(cmds) > 0 {
   170  		// setting DEBIAN_FRONTEND=noninteractive prevents debconf
   171  		// from prompting, always taking default values instead.
   172  		cmds = append([]string{"export DEBIAN_FRONTEND=noninteractive"}, cmds...)
   173  	}
   174  	return cmds, nil
   175  }
   176  
   177  func cmdlist(cmds []interface{}) ([]string, error) {
   178  	result := make([]string, 0, len(cmds))
   179  	for _, cmd := range cmds {
   180  		switch cmd := cmd.(type) {
   181  		case []string:
   182  			// Quote args, so shell meta-characters are not interpreted.
   183  			for i, arg := range cmd[1:] {
   184  				cmd[i] = utils.ShQuote(arg)
   185  			}
   186  			result = append(result, strings.Join(cmd, " "))
   187  		case string:
   188  			result = append(result, cmd)
   189  		default:
   190  			return nil, fmt.Errorf("unexpected command type: %T", cmd)
   191  		}
   192  	}
   193  	return result, nil
   194  }