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 }