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  }