github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/commands/ssh_common_test.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"github.com/juju/collections/set"
    14  	"github.com/juju/errors"
    15  	jc "github.com/juju/testing/checkers"
    16  	"github.com/juju/utils/ssh"
    17  	gc "gopkg.in/check.v1"
    18  
    19  	"github.com/juju/juju/juju/testing"
    20  	"github.com/juju/juju/network"
    21  	jujussh "github.com/juju/juju/network/ssh"
    22  	"github.com/juju/juju/state"
    23  )
    24  
    25  // argsSpec is a test helper which converts a number of options into
    26  // expected ssh/scp command lines.
    27  type argsSpec struct {
    28  	// hostKeyChecking specifies the expected StrictHostKeyChecking
    29  	// option.
    30  	hostKeyChecking string
    31  
    32  	// withProxy specifies if the juju ProxyCommand option is
    33  	// expected.
    34  	withProxy bool
    35  
    36  	// enablePty specifies if the forced PTY allocation switches are
    37  	// expected.
    38  	enablePty bool
    39  
    40  	// knownHosts may either be:
    41  	// a comma separated list of machine ids - the host keys for these
    42  	//    machines are expected in the UserKnownHostsFile
    43  	// "null" - the UserKnownHostsFile must be "/dev/null"
    44  	// empty - no UserKnownHostsFile option expected
    45  	knownHosts string
    46  
    47  	// args specifies any other command line arguments expected. This
    48  	// includes the SSH/SCP targets. Ignored if argsMatch is set as well.
    49  	args string
    50  
    51  	// argsMatch is like args, but instead of a literal string it's interpreted
    52  	// as a regular expression. When argsMatch is set, args is ignored.
    53  	argsMatch string
    54  }
    55  
    56  func (s *argsSpec) check(c *gc.C, output string) {
    57  	// The first line in the output from the fake ssh/scp is the
    58  	// command line. The remaining lines should contain the contents
    59  	// of the UserKnownHostsFile file provided (if any).
    60  	parts := strings.SplitN(output, "\n", 2)
    61  	actualCommandLine := parts[0]
    62  	actualKnownHosts := ""
    63  	if len(parts) == 2 {
    64  		actualKnownHosts = parts[1]
    65  	}
    66  
    67  	var expected []string
    68  	expect := func(part string) {
    69  		expected = append(expected, part)
    70  	}
    71  	if s.hostKeyChecking != "" {
    72  		expect("-o StrictHostKeyChecking " + s.hostKeyChecking)
    73  	}
    74  
    75  	if s.withProxy {
    76  		expect("-o ProxyCommand juju ssh " +
    77  			"--model=controller " +
    78  			"--proxy=false " +
    79  			"--no-host-key-checks " +
    80  			"--pty=false ubuntu@localhost -q \"nc %h %p\"")
    81  	}
    82  	expect("-o PasswordAuthentication no -o ServerAliveInterval 30")
    83  	if s.enablePty {
    84  		expect("-t -t")
    85  	}
    86  	if s.knownHosts == "null" {
    87  		expect(`-o UserKnownHostsFile /dev/null`)
    88  	} else if s.knownHosts == "" {
    89  		// No UserKnownHostsFile option expected.
    90  	} else {
    91  		expect(`-o UserKnownHostsFile \S+`)
    92  
    93  		// Check that the provided known_hosts file contained the
    94  		// expected keys.
    95  		c.Check(actualKnownHosts, gc.Matches, s.expectedKnownHosts())
    96  	}
    97  
    98  	if s.argsMatch != "" {
    99  		expect(s.argsMatch)
   100  	} else {
   101  		expect(regexp.QuoteMeta(s.args))
   102  	}
   103  
   104  	// Check the command line matches what is expected.
   105  	pattern := "^" + strings.Join(expected, " ") + "$"
   106  	c.Check(actualCommandLine, gc.Matches, pattern)
   107  }
   108  
   109  func (s *argsSpec) expectedKnownHosts() string {
   110  	out := ""
   111  	for _, id := range strings.Split(s.knownHosts, ",") {
   112  		out += fmt.Sprintf(".+ dsa-%s\n.+ rsa-%s\n", id, id)
   113  	}
   114  	return out
   115  }
   116  
   117  type SSHCommonSuite struct {
   118  	testing.JujuConnSuite
   119  	knownHostsDir string
   120  	binDir        string
   121  	hostChecker   jujussh.ReachableChecker
   122  }
   123  
   124  // Commands to patch
   125  var patchedCommands = []string{"ssh", "scp"}
   126  
   127  // fakecommand outputs its arguments to stdout for verification
   128  var fakecommand = `#!/bin/bash
   129  
   130  {
   131      echo "$@"
   132  
   133      # If a custom known_hosts file was passed, emit the contents of
   134      # that too.
   135      while (( "$#" )); do
   136          if [[ $1 = UserKnownHostsFile* ]]; then
   137              IFS=" " read -ra parts <<< $1
   138              cat "${parts[1]}"
   139              break
   140          fi
   141          shift
   142      done
   143  }| tee $0.args
   144  `
   145  
   146  type fakeHostChecker struct {
   147  	acceptedAddresses set.Strings
   148  }
   149  
   150  var _ jujussh.ReachableChecker = (*fakeHostChecker)(nil)
   151  
   152  func (f *fakeHostChecker) FindHost(hostPorts []network.HostPort, publicKeys []string) (network.HostPort, error) {
   153  	// TODO(jam): The real reachable checker won't give deterministic ordering
   154  	// for hostPorts, maybe we should do a random return value?
   155  	for _, hostPort := range hostPorts {
   156  		if f.acceptedAddresses.Contains(hostPort.Address.Value) {
   157  			return hostPort, nil
   158  		}
   159  	}
   160  	return network.HostPort{}, errors.Errorf("cannot connect to any address: %v", hostPorts)
   161  }
   162  
   163  func validAddresses(acceptedAddresses ...string) *fakeHostChecker {
   164  	return &fakeHostChecker{
   165  		acceptedAddresses: set.NewStrings(acceptedAddresses...),
   166  	}
   167  }
   168  
   169  func (s *SSHCommonSuite) SetUpTest(c *gc.C) {
   170  	s.JujuConnSuite.SetUpTest(c)
   171  	ssh.ClearClientKeys()
   172  	s.PatchValue(&getJujuExecutable, func() (string, error) { return "juju", nil })
   173  	s.setForceAPIv1(false)
   174  
   175  	s.binDir = c.MkDir()
   176  	s.PatchEnvPathPrepend(s.binDir)
   177  	for _, name := range patchedCommands {
   178  		f, err := os.OpenFile(filepath.Join(s.binDir, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
   179  		c.Assert(err, jc.ErrorIsNil)
   180  		_, err = f.Write([]byte(fakecommand))
   181  		c.Assert(err, jc.ErrorIsNil)
   182  		err = f.Close()
   183  		c.Assert(err, jc.ErrorIsNil)
   184  	}
   185  
   186  	client, _ := ssh.NewOpenSSHClient()
   187  	s.PatchValue(&ssh.DefaultClient, client)
   188  }
   189  
   190  func (s *SSHCommonSuite) setForceAPIv1(enabled bool) {
   191  	if enabled {
   192  		os.Setenv(jujuSSHClientForceAPIv1, "1")
   193  	} else {
   194  		os.Unsetenv(jujuSSHClientForceAPIv1)
   195  	}
   196  }
   197  
   198  func (s *SSHCommonSuite) setHostChecker(hostChecker jujussh.ReachableChecker) {
   199  	s.hostChecker = hostChecker
   200  }
   201  
   202  func (s *SSHCommonSuite) setupModel(c *gc.C) {
   203  	// Add machine-0 with a mysql application and mysql/0 unit
   204  	u := s.Factory.MakeUnit(c, nil)
   205  
   206  	// Set both the preferred public and private addresses for machine-0, add a
   207  	// couple of link-layer devices (loopback and ethernet) with addresses, and
   208  	// the ssh keys.
   209  	m := s.getMachineForUnit(c, u)
   210  	s.setAddresses(c, m)
   211  	s.setKeys(c, m)
   212  	s.setLinkLayerDevicesAddresses(c, m)
   213  
   214  	// machine-1 has no public host keys available.
   215  	m1 := s.Factory.MakeMachine(c, nil)
   216  	s.setAddresses(c, m1)
   217  
   218  	// machine-2 has IPv6 addresses
   219  	m2 := s.Factory.MakeMachine(c, nil)
   220  	s.setAddresses6(c, m2)
   221  	s.setKeys(c, m2)
   222  }
   223  
   224  func (s *SSHCommonSuite) getMachineForUnit(c *gc.C, u *state.Unit) *state.Machine {
   225  	machineId, err := u.AssignedMachineId()
   226  	c.Assert(err, jc.ErrorIsNil)
   227  	m, err := s.State.Machine(machineId)
   228  	c.Assert(err, jc.ErrorIsNil)
   229  	return m
   230  }
   231  
   232  func (s *SSHCommonSuite) setAddresses(c *gc.C, m *state.Machine) {
   233  	addrPub := network.NewScopedAddress(
   234  		fmt.Sprintf("%s.public", m.Id()),
   235  		network.ScopePublic,
   236  	)
   237  	addrPriv := network.NewScopedAddress(
   238  		fmt.Sprintf("%s.private", m.Id()),
   239  		network.ScopeCloudLocal,
   240  	)
   241  	err := m.SetProviderAddresses(addrPub, addrPriv)
   242  	c.Assert(err, jc.ErrorIsNil)
   243  }
   244  
   245  func (s *SSHCommonSuite) setLinkLayerDevicesAddresses(c *gc.C, m *state.Machine) {
   246  	devicesArgs := []state.LinkLayerDeviceArgs{{
   247  		Name: "lo",
   248  		Type: state.LoopbackDevice,
   249  	}, {
   250  		Name: "eth0",
   251  		Type: state.EthernetDevice,
   252  	}}
   253  	err := m.SetLinkLayerDevices(devicesArgs...)
   254  	c.Assert(err, jc.ErrorIsNil)
   255  
   256  	addressesArgs := []state.LinkLayerDeviceAddress{{
   257  		DeviceName:   "lo",
   258  		CIDRAddress:  "127.0.0.1/8", // will be filtered
   259  		ConfigMethod: state.LoopbackAddress,
   260  	}, {
   261  		DeviceName:   "eth0",
   262  		CIDRAddress:  "0.1.2.3/24", // needs to be a valid CIDR
   263  		ConfigMethod: state.StaticAddress,
   264  	}}
   265  	err = m.SetDevicesAddresses(addressesArgs...)
   266  	c.Assert(err, jc.ErrorIsNil)
   267  }
   268  
   269  func (s *SSHCommonSuite) setAddresses6(c *gc.C, m *state.Machine) {
   270  	addrPub := network.NewScopedAddress("2001:db8::1", network.ScopePublic)
   271  	addrPriv := network.NewScopedAddress("fc00:bbb::1", network.ScopeCloudLocal)
   272  	err := m.SetProviderAddresses(addrPub, addrPriv)
   273  	c.Assert(err, jc.ErrorIsNil)
   274  }
   275  
   276  func (s *SSHCommonSuite) setKeys(c *gc.C, m *state.Machine) {
   277  	id := m.Id()
   278  	keys := state.SSHHostKeys{"dsa-" + id, "rsa-" + id}
   279  	err := s.State.SetSSHHostKeys(m.MachineTag(), keys)
   280  	c.Assert(err, jc.ErrorIsNil)
   281  }