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  }