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