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