github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/juju/commands/ssh.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package commands 5 6 import ( 7 "fmt" 8 "net" 9 "os" 10 "os/exec" 11 "strings" 12 "time" 13 14 "github.com/juju/cmd" 15 "github.com/juju/names" 16 "github.com/juju/utils" 17 "github.com/juju/utils/ssh" 18 "launchpad.net/gnuflag" 19 20 "github.com/juju/juju/cmd/modelcmd" 21 "github.com/juju/juju/environs/config" 22 ) 23 24 var usageSSHSummary = ` 25 Initiates an SSH session or executes a command on a Juju machine.`[1:] 26 27 var usageSSHDetails = ` 28 The machine is identified by the <target> argument which is either a 'unit 29 name' or a 'machine id'. Both are obtained in the output to `[1:] + "`juju status`" + `. 30 If 'user' is specified then the connection is made to that user account; 31 otherwise, the default 'ubuntu' account, created by Juju, is used. 32 The optional command is executed on the remote machine. Any output is sent 33 back to the user. Screen-based programs require the default of '--pty=true'. 34 35 Examples: 36 Connect to machine 0: 37 38 juju ssh 0 39 40 Connect to machine 1 and run command 'uname -a': 41 42 juju ssh 1 uname -a 43 44 Connect to a mysql unit: 45 46 juju ssh mysql/0 47 48 Connect to a jenkins unit as user jenkins: 49 50 juju ssh jenkins@jenkins/0 51 52 See also: 53 scp` 54 55 func newSSHCommand() cmd.Command { 56 return modelcmd.Wrap(&sshCommand{}) 57 } 58 59 // sshCommand is responsible for launching a ssh shell on a given unit or machine. 60 type sshCommand struct { 61 SSHCommon 62 } 63 64 // SSHCommon provides common methods for sshCommand, SCPCommand and DebugHooksCommand. 65 type SSHCommon struct { 66 modelcmd.ModelCommandBase 67 proxy bool 68 pty bool 69 Target string 70 Args []string 71 apiClient sshAPIClient 72 apiAddr string 73 } 74 75 func (c *SSHCommon) SetFlags(f *gnuflag.FlagSet) { 76 f.BoolVar(&c.proxy, "proxy", false, "Proxy through the API server") 77 f.BoolVar(&c.pty, "pty", true, "Enable pseudo-tty allocation") 78 } 79 80 // setProxyCommand sets the proxy command option. 81 func (c *SSHCommon) setProxyCommand(options *ssh.Options) error { 82 apiServerHost, _, err := net.SplitHostPort(c.apiAddr) 83 if err != nil { 84 return fmt.Errorf("failed to get proxy address: %v", err) 85 } 86 juju, err := getJujuExecutable() 87 if err != nil { 88 return fmt.Errorf("failed to get juju executable path: %v", err) 89 } 90 options.SetProxyCommand(juju, "ssh", "--proxy=false", "--pty=false", apiServerHost, "nc", "%h", "%p") 91 return nil 92 } 93 94 func (c *sshCommand) Info() *cmd.Info { 95 return &cmd.Info{ 96 Name: "ssh", 97 Args: "<[user@]target> [command]", 98 Purpose: usageSSHSummary, 99 Doc: usageSSHDetails, 100 } 101 } 102 103 func (c *sshCommand) Init(args []string) error { 104 if len(args) == 0 { 105 return fmt.Errorf("no target name specified") 106 } 107 c.Target, c.Args = args[0], args[1:] 108 return nil 109 } 110 111 // getJujuExecutable returns the path to the juju 112 // executable, or an error if it could not be found. 113 var getJujuExecutable = func() (string, error) { 114 return exec.LookPath(os.Args[0]) 115 } 116 117 // getSSHOptions configures and returns SSH options and proxy settings. 118 func (c *SSHCommon) getSSHOptions(enablePty bool) (*ssh.Options, error) { 119 var options ssh.Options 120 121 // TODO(waigani) do not save fingerprint only until this bug is addressed: 122 // lp:892552. Also see lp:1334481. 123 options.SetKnownHostsFile("/dev/null") 124 if enablePty { 125 options.EnablePTY() 126 } 127 var err error 128 if c.proxy, err = c.proxySSH(); err != nil { 129 return nil, err 130 } else if c.proxy { 131 if err := c.setProxyCommand(&options); err != nil { 132 return nil, err 133 } 134 } 135 return &options, nil 136 } 137 138 // Run resolves c.Target to a machine, to the address of a i 139 // machine or unit forks ssh passing any arguments provided. 140 func (c *sshCommand) Run(ctx *cmd.Context) error { 141 if c.apiClient == nil { 142 // If the apClient is not already opened and it is opened 143 // by ensureAPIClient, then close it when we're done. 144 defer func() { 145 if c.apiClient != nil { 146 c.apiClient.Close() 147 c.apiClient = nil 148 } 149 }() 150 } 151 options, err := c.getSSHOptions(c.pty) 152 if err != nil { 153 return err 154 } 155 156 user, host, err := c.userHostFromTarget(c.Target) 157 if err != nil { 158 return err 159 } 160 cmd := ssh.Command(user+"@"+host, c.Args, options) 161 cmd.Stdin = ctx.Stdin 162 cmd.Stdout = ctx.Stdout 163 cmd.Stderr = ctx.Stderr 164 return cmd.Run() 165 } 166 167 // proxySSH returns false if both c.proxy and 168 // the proxy-ssh environment configuration 169 // are false -- otherwise it returns true. 170 func (c *SSHCommon) proxySSH() (bool, error) { 171 if _, err := c.ensureAPIClient(); err != nil { 172 return false, err 173 } 174 var cfg *config.Config 175 attrs, err := c.apiClient.ModelGet() 176 if err == nil { 177 cfg, err = config.New(config.NoDefaults, attrs) 178 } 179 if err != nil { 180 return false, err 181 } 182 logger.Debugf("proxy-ssh is %v", cfg.ProxySSH()) 183 184 return cfg.ProxySSH() || c.proxy, nil 185 } 186 187 func (c *SSHCommon) ensureAPIClient() (sshAPIClient, error) { 188 if c.apiClient != nil { 189 return c.apiClient, nil 190 } 191 return c.initAPIClient() 192 } 193 194 // initAPIClient initialises the API connection. 195 // It is the caller's responsibility to close the connection. 196 func (c *SSHCommon) initAPIClient() (sshAPIClient, error) { 197 st, err := c.NewAPIRoot() 198 if err != nil { 199 return nil, err 200 } 201 c.apiClient = st.Client() 202 c.apiAddr = st.Addr() 203 return c.apiClient, nil 204 } 205 206 type sshAPIClient interface { 207 ModelGet() (map[string]interface{}, error) 208 PublicAddress(target string) (string, error) 209 PrivateAddress(target string) (string, error) 210 Close() error 211 } 212 213 // attemptStarter is an interface corresponding to utils.AttemptStrategy 214 type attemptStarter interface { 215 Start() attempt 216 } 217 218 type attempt interface { 219 Next() bool 220 } 221 222 type attemptStrategy utils.AttemptStrategy 223 224 func (s attemptStrategy) Start() attempt { 225 return utils.AttemptStrategy(s).Start() 226 } 227 228 var sshHostFromTargetAttemptStrategy attemptStarter = attemptStrategy{ 229 Total: 5 * time.Second, 230 Delay: 500 * time.Millisecond, 231 } 232 233 func (c *SSHCommon) userHostFromTarget(target string) (user, host string, err error) { 234 if i := strings.IndexRune(target, '@'); i != -1 { 235 user = target[:i] 236 target = target[i+1:] 237 } else { 238 user = "ubuntu" 239 } 240 241 // If the target is neither a machine nor a unit, 242 // assume it's a hostname and try it directly. 243 if !names.IsValidMachine(target) && !names.IsValidUnit(target) { 244 return user, target, nil 245 } 246 247 // A target may not initially have an address (e.g. the 248 // address updater hasn't yet run), so we must do this in 249 // a loop. 250 if _, err := c.ensureAPIClient(); err != nil { 251 return "", "", err 252 } 253 for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); { 254 var addr string 255 if c.proxy { 256 addr, err = c.apiClient.PrivateAddress(target) 257 } else { 258 addr, err = c.apiClient.PublicAddress(target) 259 } 260 if err == nil { 261 return user, addr, nil 262 } 263 } 264 return "", "", err 265 } 266 267 // AllowInterspersedFlags for ssh/scp is set to false so that 268 // flags after the unit name are passed through to ssh, for eg. 269 // `juju ssh -v service-name/0 uname -a`. 270 func (c *SSHCommon) AllowInterspersedFlags() bool { 271 return false 272 }