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