launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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 "errors" 8 "fmt" 9 "time" 10 11 "launchpad.net/juju-core/cmd" 12 "launchpad.net/juju-core/instance" 13 "launchpad.net/juju-core/juju" 14 "launchpad.net/juju-core/names" 15 "launchpad.net/juju-core/state/api" 16 "launchpad.net/juju-core/state/api/params" 17 "launchpad.net/juju-core/utils" 18 "launchpad.net/juju-core/utils/ssh" 19 ) 20 21 // SSHCommand is responsible for launching a ssh shell on a given unit or machine. 22 type SSHCommand struct { 23 SSHCommon 24 } 25 26 // SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand. 27 type SSHCommon struct { 28 cmd.EnvCommandBase 29 Target string 30 Args []string 31 apiClient *api.Client 32 // Only used for compatibility with 1.16 33 rawConn *juju.Conn 34 } 35 36 const sshDoc = ` 37 Launch an ssh shell on the machine identified by the <target> parameter. 38 <target> can be either a machine id as listed by "juju status" in the 39 "machines" section or a unit name as listed in the "services" section. 40 Any extra parameters are passsed as extra parameters to the ssh command. 41 42 Examples 43 44 Connect to machine 0: 45 46 juju ssh 0 47 48 Connect to the first mysql unit: 49 50 juju ssh mysql/0 51 ` 52 53 func (c *SSHCommand) Info() *cmd.Info { 54 return &cmd.Info{ 55 Name: "ssh", 56 Args: "<target> [<ssh args>...]", 57 Purpose: "launch an ssh shell on a given unit or machine", 58 Doc: sshDoc, 59 } 60 } 61 62 func (c *SSHCommand) Init(args []string) error { 63 if len(args) == 0 { 64 return errors.New("no target name specified") 65 } 66 c.Target, c.Args = args[0], args[1:] 67 return nil 68 } 69 70 // Run resolves c.Target to a machine, to the address of a i 71 // machine or unit forks ssh passing any arguments provided. 72 func (c *SSHCommand) Run(ctx *cmd.Context) error { 73 if c.apiClient == nil { 74 var err error 75 c.apiClient, err = c.initAPIClient() 76 if err != nil { 77 return err 78 } 79 defer c.apiClient.Close() 80 } 81 host, err := c.hostFromTarget(c.Target) 82 if err != nil { 83 return err 84 } 85 args := c.Args 86 if len(args) > 0 && args[0] == "--" { 87 // utils/ssh adds "--"; we will continue to accept 88 // it from the CLI for backwards compatibility. 89 args = args[1:] 90 } 91 var options ssh.Options 92 options.EnablePTY() 93 cmd := ssh.Command("ubuntu@"+host, args, &options) 94 cmd.Stdin = ctx.Stdin 95 cmd.Stdout = ctx.Stdout 96 cmd.Stderr = ctx.Stderr 97 return cmd.Run() 98 } 99 100 // initAPIClient initialises the API connection. 101 // It is the caller's responsibility to close the connection. 102 func (c *SSHCommon) initAPIClient() (*api.Client, error) { 103 var err error 104 c.apiClient, err = juju.NewAPIClientFromName(c.EnvName) 105 return c.apiClient, err 106 } 107 108 // attemptStarter is an interface corresponding to utils.AttemptStrategy 109 type attemptStarter interface { 110 Start() attempt 111 } 112 113 type attempt interface { 114 Next() bool 115 } 116 117 type attemptStrategy utils.AttemptStrategy 118 119 func (s attemptStrategy) Start() attempt { 120 return utils.AttemptStrategy(s).Start() 121 } 122 123 var sshHostFromTargetAttemptStrategy attemptStarter = attemptStrategy{ 124 Total: 5 * time.Second, 125 Delay: 500 * time.Millisecond, 126 } 127 128 // ensureRawConn ensures that c.rawConn is valid (or returns an error) 129 // This is only for compatibility with a 1.16 API server (that doesn't have 130 // some of the API added more recently.) It can be removed once we no longer 131 // need compatibility with direct access to the state database 132 func (c *SSHCommon) ensureRawConn() error { 133 if c.rawConn != nil { 134 return nil 135 } 136 var err error 137 c.rawConn, err = juju.NewConnFromName(c.EnvName) 138 return err 139 } 140 141 func (c *SSHCommon) hostFromTarget1dot16(target string) (string, error) { 142 err := c.ensureRawConn() 143 if err != nil { 144 return "", err 145 } 146 // is the target the id of a machine ? 147 if names.IsMachine(target) { 148 logger.Infof("looking up address for machine %s...", target) 149 // This is not the exact code from the 1.16 client 150 // (machinePublicAddress), however it is the code used in the 151 // apiserver behind the PublicAddress call. (1.16 didn't know 152 // about SelectPublicAddress) 153 // The old code watched for changes on the Machine until it had 154 // an InstanceId and then would return the instance.WaitDNS() 155 machine, err := c.rawConn.State.Machine(target) 156 if err != nil { 157 return "", err 158 } 159 addr := instance.SelectPublicAddress(machine.Addresses()) 160 if addr == "" { 161 return "", fmt.Errorf("machine %q has no public address", machine) 162 } 163 return addr, nil 164 } 165 // maybe the target is a unit ? 166 if names.IsUnit(target) { 167 logger.Infof("looking up address for unit %q...", c.Target) 168 unit, err := c.rawConn.State.Unit(target) 169 if err != nil { 170 return "", err 171 } 172 addr, ok := unit.PublicAddress() 173 if !ok { 174 return "", fmt.Errorf("unit %q has no public address", unit) 175 } 176 return addr, nil 177 } 178 return "", fmt.Errorf("unknown unit or machine %q", target) 179 } 180 181 func (c *SSHCommon) hostFromTarget(target string) (string, error) { 182 var addr string 183 var err error 184 var useStateConn bool 185 // A target may not initially have an address (e.g. the 186 // address updater hasn't yet run), so we must do this in 187 // a loop. 188 for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); { 189 if !useStateConn { 190 addr, err = c.apiClient.PublicAddress(target) 191 if params.IsCodeNotImplemented(err) { 192 logger.Infof("API server does not support Client.PublicAddress falling back to 1.16 compatibility mode (direct DB access)") 193 useStateConn = true 194 } 195 } 196 if useStateConn { 197 addr, err = c.hostFromTarget1dot16(target) 198 } 199 if err == nil { 200 break 201 } 202 } 203 if err != nil { 204 return "", err 205 } 206 logger.Infof("Resolved public address of %q: %q", target, addr) 207 return addr, nil 208 } 209 210 // AllowInterspersedFlags for ssh/scp is set to false so that 211 // flags after the unit name are passed through to ssh, for eg. 212 // `juju ssh -v service-name/0 uname -a`. 213 func (c *SSHCommon) AllowInterspersedFlags() bool { 214 return false 215 }