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  }