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