github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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  	"strings"
    12  	"time"
    13  
    14  	"github.com/juju/cmd"
    15  	"github.com/juju/names"
    16  	"github.com/juju/utils"
    17  	"launchpad.net/gnuflag"
    18  
    19  	"github.com/juju/juju/cmd/envcmd"
    20  	"github.com/juju/juju/environs/config"
    21  	"github.com/juju/juju/utils/ssh"
    22  )
    23  
    24  // SSHCommand is responsible for launching a ssh shell on a given unit or machine.
    25  type SSHCommand struct {
    26  	SSHCommon
    27  }
    28  
    29  // SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand.
    30  type SSHCommon struct {
    31  	envcmd.EnvCommandBase
    32  	proxy     bool
    33  	pty       bool
    34  	Target    string
    35  	Args      []string
    36  	apiClient sshAPIClient
    37  	apiAddr   string
    38  }
    39  
    40  func (c *SSHCommon) SetFlags(f *gnuflag.FlagSet) {
    41  	f.BoolVar(&c.proxy, "proxy", true, "proxy through the API server")
    42  	f.BoolVar(&c.pty, "pty", true, "enable pseudo-tty allocation")
    43  }
    44  
    45  // setProxyCommand sets the proxy command option.
    46  func (c *SSHCommon) setProxyCommand(options *ssh.Options) error {
    47  	apiServerHost, _, err := net.SplitHostPort(c.apiAddr)
    48  	if err != nil {
    49  		return fmt.Errorf("failed to get proxy address: %v", err)
    50  	}
    51  	juju, err := getJujuExecutable()
    52  	if err != nil {
    53  		return fmt.Errorf("failed to get juju executable path: %v", err)
    54  	}
    55  	options.SetProxyCommand(juju, "ssh", "--proxy=false", "--pty=false", apiServerHost, "nc", "-q0", "%h", "%p")
    56  	return nil
    57  }
    58  
    59  const sshDoc = `
    60  Launch an ssh shell on the machine identified by the <target> parameter.
    61  <target> can be either a machine id  as listed by "juju status" in the
    62  "machines" section or a unit name as listed in the "services" section.
    63  Any extra parameters are passsed as extra parameters to the ssh command.
    64  
    65  Examples:
    66  
    67  Connect to machine 0:
    68  
    69      juju ssh 0
    70  
    71  Connect to machine 1 and run 'uname -a':
    72  
    73      juju ssh 1 uname -a
    74  
    75  Connect to the first mysql unit:
    76  
    77      juju ssh mysql/0
    78  
    79  Connect to the first mysql unit and run 'ls -la /var/log/juju':
    80  
    81      juju ssh mysql/0 ls -la /var/log/juju
    82  
    83  Connect to the first jenkins unit as the user jenkins:
    84  
    85      juju ssh jenkins@jenkins/0
    86  `
    87  
    88  func (c *SSHCommand) Info() *cmd.Info {
    89  	return &cmd.Info{
    90  		Name:    "ssh",
    91  		Args:    "<target> [<ssh args>...]",
    92  		Purpose: "launch an ssh shell on a given unit or machine",
    93  		Doc:     sshDoc,
    94  	}
    95  }
    96  
    97  func (c *SSHCommand) Init(args []string) error {
    98  	if len(args) == 0 {
    99  		return fmt.Errorf("no target name specified")
   100  	}
   101  	c.Target, c.Args = args[0], args[1:]
   102  	return nil
   103  }
   104  
   105  // getJujuExecutable returns the path to the juju
   106  // executable, or an error if it could not be found.
   107  var getJujuExecutable = func() (string, error) {
   108  	return exec.LookPath(os.Args[0])
   109  }
   110  
   111  // getSSHOptions configures and returns SSH options and proxy settings.
   112  func (c *SSHCommon) getSSHOptions(enablePty bool) (*ssh.Options, error) {
   113  	var options ssh.Options
   114  
   115  	// TODO(waigani) do not save fingerprint only until this bug is addressed:
   116  	// lp:892552. Also see lp:1334481.
   117  	options.SetKnownHostsFile("/dev/null")
   118  	if enablePty {
   119  		options.EnablePTY()
   120  	}
   121  	var err error
   122  	if c.proxy, err = c.proxySSH(); err != nil {
   123  		return nil, err
   124  	} else if c.proxy {
   125  		if err := c.setProxyCommand(&options); err != nil {
   126  			return nil, err
   127  		}
   128  	}
   129  	return &options, nil
   130  }
   131  
   132  // Run resolves c.Target to a machine, to the address of a i
   133  // machine or unit forks ssh passing any arguments provided.
   134  func (c *SSHCommand) Run(ctx *cmd.Context) error {
   135  	if c.apiClient == nil {
   136  		// If the apClient is not already opened and it is opened
   137  		// by ensureAPIClient, then close it when we're done.
   138  		defer func() {
   139  			if c.apiClient != nil {
   140  				c.apiClient.Close()
   141  				c.apiClient = nil
   142  			}
   143  		}()
   144  	}
   145  	options, err := c.getSSHOptions(c.pty)
   146  	if err != nil {
   147  		return err
   148  	}
   149  
   150  	user, host, err := c.userHostFromTarget(c.Target)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	cmd := ssh.Command(user+"@"+host, c.Args, options)
   155  	cmd.Stdin = ctx.Stdin
   156  	cmd.Stdout = ctx.Stdout
   157  	cmd.Stderr = ctx.Stderr
   158  	return cmd.Run()
   159  }
   160  
   161  // proxySSH returns true iff both c.proxy and
   162  // the proxy-ssh environment configuration
   163  // are true.
   164  func (c *SSHCommon) proxySSH() (bool, error) {
   165  	if !c.proxy {
   166  		return false, nil
   167  	}
   168  	if _, err := c.ensureAPIClient(); err != nil {
   169  		return false, err
   170  	}
   171  	var cfg *config.Config
   172  	attrs, err := c.apiClient.EnvironmentGet()
   173  	if err == nil {
   174  		cfg, err = config.New(config.NoDefaults, attrs)
   175  	}
   176  	if err != nil {
   177  		return false, err
   178  	}
   179  	logger.Debugf("proxy-ssh is %v", cfg.ProxySSH())
   180  	return cfg.ProxySSH(), nil
   181  }
   182  
   183  func (c *SSHCommon) ensureAPIClient() (sshAPIClient, error) {
   184  	if c.apiClient != nil {
   185  		return c.apiClient, nil
   186  	}
   187  	return c.initAPIClient()
   188  }
   189  
   190  // initAPIClient initialises the API connection.
   191  // It is the caller's responsibility to close the connection.
   192  func (c *SSHCommon) initAPIClient() (sshAPIClient, error) {
   193  	st, err := c.NewAPIRoot()
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	c.apiClient = st.Client()
   198  	c.apiAddr = st.Addr()
   199  	return c.apiClient, nil
   200  }
   201  
   202  type sshAPIClient interface {
   203  	EnvironmentGet() (map[string]interface{}, error)
   204  	PublicAddress(target string) (string, error)
   205  	PrivateAddress(target string) (string, error)
   206  	ServiceCharmRelations(service string) ([]string, error)
   207  	Close() error
   208  }
   209  
   210  // attemptStarter is an interface corresponding to utils.AttemptStrategy
   211  type attemptStarter interface {
   212  	Start() attempt
   213  }
   214  
   215  type attempt interface {
   216  	Next() bool
   217  }
   218  
   219  type attemptStrategy utils.AttemptStrategy
   220  
   221  func (s attemptStrategy) Start() attempt {
   222  	return utils.AttemptStrategy(s).Start()
   223  }
   224  
   225  var sshHostFromTargetAttemptStrategy attemptStarter = attemptStrategy{
   226  	Total: 5 * time.Second,
   227  	Delay: 500 * time.Millisecond,
   228  }
   229  
   230  func (c *SSHCommon) userHostFromTarget(target string) (user, host string, err error) {
   231  	if i := strings.IndexRune(target, '@'); i != -1 {
   232  		user = target[:i]
   233  		target = target[i+1:]
   234  	} else {
   235  		user = "ubuntu"
   236  	}
   237  
   238  	// If the target is neither a machine nor a unit,
   239  	// assume it's a hostname and try it directly.
   240  	if !names.IsValidMachine(target) && !names.IsValidUnit(target) {
   241  		return user, target, nil
   242  	}
   243  
   244  	// A target may not initially have an address (e.g. the
   245  	// address updater hasn't yet run), so we must do this in
   246  	// a loop.
   247  	if _, err := c.ensureAPIClient(); err != nil {
   248  		return "", "", err
   249  	}
   250  	for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); {
   251  		var addr string
   252  		if c.proxy {
   253  			addr, err = c.apiClient.PrivateAddress(target)
   254  		} else {
   255  			addr, err = c.apiClient.PublicAddress(target)
   256  		}
   257  		if err == nil {
   258  			return user, addr, nil
   259  		}
   260  	}
   261  	return "", "", err
   262  }
   263  
   264  // AllowInterspersedFlags for ssh/scp is set to false so that
   265  // flags after the unit name are passed through to ssh, for eg.
   266  // `juju ssh -v service-name/0 uname -a`.
   267  func (c *SSHCommon) AllowInterspersedFlags() bool {
   268  	return false
   269  }