github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/utils/ssh/ssh.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // Package ssh contains utilities for dealing with SSH connections, 5 // key management, and so on. All SSH-based command executions in 6 // Juju should use the Command/ScpCommand functions in this package. 7 // 8 package ssh 9 10 import ( 11 "bytes" 12 "errors" 13 "io" 14 "os/exec" 15 "syscall" 16 17 "launchpad.net/juju-core/cmd" 18 ) 19 20 // Options is a client-implementation independent SSH options set. 21 type Options struct { 22 // ssh server port; zero means use the default (22) 23 port int 24 // no PTY forced by default 25 allocatePTY bool 26 // password authentication is disallowed by default 27 passwordAuthAllowed bool 28 // identities is a sequence of paths to private key/identity files 29 // to use when attempting to login. A client implementaton may attempt 30 // with additional identities, but must give preference to these 31 identities []string 32 } 33 34 // SetPort sets the SSH server port to connect to. 35 func (o *Options) SetPort(port int) { 36 o.port = port 37 } 38 39 // EnablePTY forces the allocation of a pseudo-TTY. 40 // 41 // Forcing a pseudo-TTY is required, for example, for sudo 42 // prompts on the target host. 43 func (o *Options) EnablePTY() { 44 o.allocatePTY = true 45 } 46 47 // AllowPasswordAuthentication allows the SSH 48 // client to prompt the user for a password. 49 // 50 // Password authentication is disallowed by default. 51 func (o *Options) AllowPasswordAuthentication() { 52 o.passwordAuthAllowed = true 53 } 54 55 // SetIdentities sets a sequence of paths to private key/identity files 56 // to use when attempting login. Client implementations may attempt to 57 // use additional identities, but must give preference to the ones 58 // specified here. 59 func (o *Options) SetIdentities(identityFiles ...string) { 60 o.identities = append([]string{}, identityFiles...) 61 } 62 63 // Client is an interface for SSH clients to implement 64 type Client interface { 65 // Command returns a Command for executing a command 66 // on the specified host. Each Command is executed 67 // within its own SSH session. 68 // 69 // Host is specified in the format [user@]host. 70 Command(host string, command []string, options *Options) *Cmd 71 72 // Copy copies file(s) between local and remote host(s). 73 // Paths are specified in the scp format, [[user@]host:]path. If 74 // any extra arguments are specified in extraArgs, they are passed 75 // verbatim. 76 Copy(targets, extraArgs []string, options *Options) error 77 } 78 79 // Cmd represents a command to be (or being) executed 80 // on a remote host. 81 type Cmd struct { 82 Stdin io.Reader 83 Stdout io.Writer 84 Stderr io.Writer 85 impl command 86 } 87 88 // CombinedOutput runs the command, and returns the 89 // combined stdout/stderr output and result of 90 // executing the command. 91 func (c *Cmd) CombinedOutput() ([]byte, error) { 92 if c.Stdout != nil { 93 return nil, errors.New("ssh: Stdout already set") 94 } 95 if c.Stderr != nil { 96 return nil, errors.New("ssh: Stderr already set") 97 } 98 var b bytes.Buffer 99 c.Stdout = &b 100 c.Stderr = &b 101 err := c.Run() 102 return b.Bytes(), err 103 } 104 105 // Output runs the command, and returns the stdout 106 // output and result of executing the command. 107 func (c *Cmd) Output() ([]byte, error) { 108 if c.Stdout != nil { 109 return nil, errors.New("ssh: Stdout already set") 110 } 111 var b bytes.Buffer 112 c.Stdout = &b 113 err := c.Run() 114 return b.Bytes(), err 115 } 116 117 // Run runs the command, and returns the result as an error. 118 func (c *Cmd) Run() error { 119 if err := c.Start(); err != nil { 120 return err 121 } 122 err := c.Wait() 123 if exitError, ok := err.(*exec.ExitError); ok && exitError != nil { 124 status := exitError.ProcessState.Sys().(syscall.WaitStatus) 125 if status.Exited() { 126 return cmd.NewRcPassthroughError(status.ExitStatus()) 127 } 128 } 129 return err 130 } 131 132 // Start starts the command running, but does not wait for 133 // it to complete. If the command could not be started, an 134 // error is returned. 135 func (c *Cmd) Start() error { 136 c.impl.SetStdio(c.Stdin, c.Stdout, c.Stderr) 137 return c.impl.Start() 138 } 139 140 // Wait waits for the started command to complete, 141 // and returns the result as an error. 142 func (c *Cmd) Wait() error { 143 return c.impl.Wait() 144 } 145 146 // Kill kills the started command. 147 func (c *Cmd) Kill() error { 148 return c.impl.Kill() 149 } 150 151 // StdinPipe creates a pipe and connects it to 152 // the command's stdin. The read end of the pipe 153 // is assigned to c.Stdin. 154 func (c *Cmd) StdinPipe() (io.WriteCloser, error) { 155 wc, r, err := c.impl.StdinPipe() 156 if err != nil { 157 return nil, err 158 } 159 c.Stdin = r 160 return wc, nil 161 } 162 163 // StdoutPipe creates a pipe and connects it to 164 // the command's stdout. The write end of the pipe 165 // is assigned to c.Stdout. 166 func (c *Cmd) StdoutPipe() (io.ReadCloser, error) { 167 rc, w, err := c.impl.StdoutPipe() 168 if err != nil { 169 return nil, err 170 } 171 c.Stdout = w 172 return rc, nil 173 } 174 175 // StderrPipe creates a pipe and connects it to 176 // the command's stderr. The write end of the pipe 177 // is assigned to c.Stderr. 178 func (c *Cmd) StderrPipe() (io.ReadCloser, error) { 179 rc, w, err := c.impl.StderrPipe() 180 if err != nil { 181 return nil, err 182 } 183 c.Stderr = w 184 return rc, nil 185 } 186 187 // command is an implementation-specific representation of a 188 // command prepared to execute against a specific host. 189 type command interface { 190 Start() error 191 Wait() error 192 Kill() error 193 SetStdio(stdin io.Reader, stdout, stderr io.Writer) 194 StdinPipe() (io.WriteCloser, io.Reader, error) 195 StdoutPipe() (io.ReadCloser, io.Writer, error) 196 StderrPipe() (io.ReadCloser, io.Writer, error) 197 } 198 199 // DefaultClient is the default SSH client for the process. 200 // 201 // If the OpenSSH client is found in $PATH, then it will be 202 // used for DefaultClient; otherwise, DefaultClient will use 203 // an embedded client based on go.crypto/ssh. 204 var DefaultClient Client 205 206 // chosenClient holds the type of SSH client created for 207 // DefaultClient, so that we can log it in Command or Copy. 208 var chosenClient string 209 210 func init() { 211 initDefaultClient() 212 } 213 214 func initDefaultClient() { 215 if client, err := NewOpenSSHClient(); err == nil { 216 DefaultClient = client 217 chosenClient = "OpenSSH" 218 } else if client, err := NewGoCryptoClient(); err == nil { 219 DefaultClient = client 220 chosenClient = "go.crypto (embedded)" 221 } 222 } 223 224 // Command is a short-cut for DefaultClient.Command. 225 func Command(host string, command []string, options *Options) *Cmd { 226 logger.Debugf("using %s ssh client", chosenClient) 227 return DefaultClient.Command(host, command, options) 228 } 229 230 // Copy is a short-cut for DefaultClient.Copy. 231 func Copy(targets, extraArgs []string, options *Options) error { 232 logger.Debugf("using %s ssh client", chosenClient) 233 return DefaultClient.Copy(targets, extraArgs, options) 234 }