github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/juju/commands/ssh.go (about)

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