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 }