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