github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/environs/sshstorage/storage_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package sshstorage
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"os"
    12  	"os/exec"
    13  	"path"
    14  	"path/filepath"
    15  	"regexp"
    16  	"runtime"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/juju/errors"
    21  	"github.com/juju/testing"
    22  	jc "github.com/juju/testing/checkers"
    23  	"github.com/juju/utils"
    24  	gc "gopkg.in/check.v1"
    25  
    26  	"github.com/juju/juju/environs/storage"
    27  	coretesting "github.com/juju/juju/testing"
    28  	"github.com/juju/juju/utils/ssh"
    29  )
    30  
    31  type storageSuite struct {
    32  	coretesting.BaseSuite
    33  	bin string
    34  }
    35  
    36  var _ = gc.Suite(&storageSuite{})
    37  
    38  func (s *storageSuite) sshCommand(c *gc.C, host string, command ...string) *ssh.Cmd {
    39  	script := []byte("#!/bin/bash\n" + strings.Join(command, " "))
    40  	err := ioutil.WriteFile(filepath.Join(s.bin, "ssh"), script, 0755)
    41  	c.Assert(err, jc.ErrorIsNil)
    42  	client, err := ssh.NewOpenSSHClient()
    43  	c.Assert(err, jc.ErrorIsNil)
    44  	return client.Command(host, command, nil)
    45  }
    46  
    47  func newSSHStorage(host, storageDir, tmpDir string) (*SSHStorage, error) {
    48  	params := NewSSHStorageParams{
    49  		Host:       host,
    50  		StorageDir: storageDir,
    51  		TmpDir:     tmpDir,
    52  	}
    53  	return NewSSHStorage(params)
    54  }
    55  
    56  // flockBin is the path to the original "flock" binary.
    57  var flockBin string
    58  
    59  func (s *storageSuite) SetUpSuite(c *gc.C) {
    60  	if runtime.GOOS == "windows" {
    61  		c.Skip("No flock on windows`")
    62  	}
    63  	s.BaseSuite.SetUpSuite(c)
    64  
    65  	var err error
    66  	flockBin, err = exec.LookPath("flock")
    67  	c.Assert(err, jc.ErrorIsNil)
    68  
    69  	s.bin = c.MkDir()
    70  	s.PatchEnvPathPrepend(s.bin)
    71  
    72  	// Create a "sudo" command which shifts away the "-n", sets
    73  	// SUDO_UID/SUDO_GID, and executes the remaining args.
    74  	err = ioutil.WriteFile(filepath.Join(s.bin, "sudo"), []byte(
    75  		"#!/bin/sh\nshift; export SUDO_UID=`id -u` SUDO_GID=`id -g`; exec \"$@\"",
    76  	), 0755)
    77  	c.Assert(err, jc.ErrorIsNil)
    78  	restoreSshCommand := testing.PatchValue(&sshCommand, func(host string, command ...string) *ssh.Cmd {
    79  		return s.sshCommand(c, host, command...)
    80  	})
    81  	s.AddSuiteCleanup(func(*gc.C) { restoreSshCommand() })
    82  
    83  	// Create a new "flock" which calls the original, but in non-blocking mode.
    84  	data := []byte(fmt.Sprintf("#!/bin/sh\nexec %s --nonblock \"$@\"", flockBin))
    85  	err = ioutil.WriteFile(filepath.Join(s.bin, "flock"), data, 0755)
    86  	c.Assert(err, jc.ErrorIsNil)
    87  }
    88  
    89  func (s *storageSuite) makeStorage(c *gc.C) (storage *SSHStorage, storageDir string) {
    90  	storageDir = c.MkDir()
    91  	storage, err := newSSHStorage("example.com", storageDir, storageDir+"-tmp")
    92  	c.Assert(err, jc.ErrorIsNil)
    93  	c.Assert(storage, gc.NotNil)
    94  	s.AddCleanup(func(*gc.C) { storage.Close() })
    95  	return storage, storageDir
    96  }
    97  
    98  // createFiles creates empty files in the storage directory
    99  // with the given storage names.
   100  func createFiles(c *gc.C, storageDir string, names ...string) {
   101  	for _, name := range names {
   102  		path := filepath.Join(storageDir, filepath.FromSlash(name))
   103  		dir := filepath.Dir(path)
   104  		if err := os.MkdirAll(dir, 0755); err != nil {
   105  			c.Assert(err, jc.Satisfies, os.IsExist)
   106  		}
   107  		err := ioutil.WriteFile(path, nil, 0644)
   108  		c.Assert(err, jc.ErrorIsNil)
   109  	}
   110  }
   111  
   112  func (s *storageSuite) TestnewSSHStorage(c *gc.C) {
   113  	storageDir := c.MkDir()
   114  	// Run this block twice to ensure newSSHStorage can reuse
   115  	// an existing storage location.
   116  	for i := 0; i < 2; i++ {
   117  		stor, err := newSSHStorage("example.com", storageDir, storageDir+"-tmp")
   118  		c.Assert(err, jc.ErrorIsNil)
   119  		c.Assert(stor, gc.NotNil)
   120  		c.Assert(stor.Close(), gc.IsNil)
   121  	}
   122  	err := os.RemoveAll(storageDir)
   123  	c.Assert(err, jc.ErrorIsNil)
   124  
   125  	// You must have permissions to create the directory.
   126  	storageDir = c.MkDir()
   127  	err = os.Chmod(storageDir, 0555)
   128  	c.Assert(err, jc.ErrorIsNil)
   129  	_, err = newSSHStorage("example.com", filepath.Join(storageDir, "subdir"), storageDir+"-tmp")
   130  	c.Assert(err, gc.ErrorMatches, "(.|\n)*cannot change owner and permissions of(.|\n)*")
   131  }
   132  
   133  func (s *storageSuite) TestPathValidity(c *gc.C) {
   134  	stor, storageDir := s.makeStorage(c)
   135  	err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
   136  	c.Assert(err, jc.ErrorIsNil)
   137  	createFiles(c, storageDir, "a/b")
   138  
   139  	for _, prefix := range []string{"..", "a/../.."} {
   140  		c.Logf("prefix: %q", prefix)
   141  		_, err := storage.List(stor, prefix)
   142  		c.Check(err, gc.ErrorMatches, regexp.QuoteMeta(fmt.Sprintf("%q escapes storage directory", prefix)))
   143  	}
   144  
   145  	// Paths are always relative, so a leading "/" may as well not be there.
   146  	names, err := storage.List(stor, "/")
   147  	c.Assert(err, jc.ErrorIsNil)
   148  	c.Assert(names, gc.DeepEquals, []string{"a/b"})
   149  
   150  	// Paths will be canonicalised.
   151  	names, err = storage.List(stor, "a/..")
   152  	c.Assert(err, jc.ErrorIsNil)
   153  	c.Assert(names, gc.DeepEquals, []string{"a/b"})
   154  }
   155  
   156  func (s *storageSuite) TestGet(c *gc.C) {
   157  	stor, storageDir := s.makeStorage(c)
   158  	data := []byte("abc\000def")
   159  	err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
   160  	c.Assert(err, jc.ErrorIsNil)
   161  	for _, name := range []string{"b", filepath.Join("a", "b")} {
   162  		err = ioutil.WriteFile(filepath.Join(storageDir, name), data, 0644)
   163  		c.Assert(err, jc.ErrorIsNil)
   164  		r, err := storage.Get(stor, name)
   165  		c.Assert(err, jc.ErrorIsNil)
   166  		out, err := ioutil.ReadAll(r)
   167  		c.Assert(err, jc.ErrorIsNil)
   168  		c.Assert(out, gc.DeepEquals, data)
   169  	}
   170  	_, err = storage.Get(stor, "notthere")
   171  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   172  }
   173  
   174  func (s *storageSuite) TestWriteFailure(c *gc.C) {
   175  	// Invocations:
   176  	//  1: first "install"
   177  	//  2: touch, Put
   178  	//  3: second "install"
   179  	//  4: touch
   180  	var invocations int
   181  	badSshCommand := func(host string, command ...string) *ssh.Cmd {
   182  		invocations++
   183  		switch invocations {
   184  		case 1, 3:
   185  			return s.sshCommand(c, host, "head -n 1 > /dev/null")
   186  		case 2:
   187  			// Note: must close stdin before responding the first time, or
   188  			// the second command will race with closing stdin, and may
   189  			// flush first.
   190  			return s.sshCommand(c, host, "head -n 1 > /dev/null; exec 0<&-; echo JUJU-RC: 0; echo blah blah; echo more")
   191  		case 4:
   192  			return s.sshCommand(c, host, `head -n 1 > /dev/null; echo "Hey it's JUJU-RC: , but not at the beginning of the line"; echo more`)
   193  		default:
   194  			c.Errorf("unexpected invocation: #%d, %s", invocations, command)
   195  			return nil
   196  		}
   197  	}
   198  	s.PatchValue(&sshCommand, badSshCommand)
   199  
   200  	stor, err := newSSHStorage("example.com", c.MkDir(), c.MkDir())
   201  	c.Assert(err, jc.ErrorIsNil)
   202  	defer stor.Close()
   203  	err = stor.Put("whatever", bytes.NewBuffer(nil), 0)
   204  	c.Assert(err, gc.ErrorMatches, `failed to write input: write \|1: broken pipe \(output: "blah blah\\nmore"\)`)
   205  
   206  	_, err = newSSHStorage("example.com", c.MkDir(), c.MkDir())
   207  	c.Assert(err, gc.ErrorMatches, `failed to locate "JUJU-RC: " \(output: "Hey it's JUJU-RC: , but not at the beginning of the line\\nmore"\)`)
   208  }
   209  
   210  func (s *storageSuite) TestPut(c *gc.C) {
   211  	stor, storageDir := s.makeStorage(c)
   212  	data := []byte("abc\000def")
   213  	for _, name := range []string{"b", filepath.Join("a", "b")} {
   214  		err := stor.Put(name, bytes.NewBuffer(data), int64(len(data)))
   215  		c.Assert(err, jc.ErrorIsNil)
   216  		out, err := ioutil.ReadFile(filepath.Join(storageDir, name))
   217  		c.Assert(err, jc.ErrorIsNil)
   218  		c.Assert(out, gc.DeepEquals, data)
   219  	}
   220  }
   221  
   222  func (s *storageSuite) assertList(c *gc.C, stor storage.StorageReader, prefix string, expected []string) {
   223  	c.Logf("List: %v", prefix)
   224  	names, err := storage.List(stor, prefix)
   225  	c.Assert(err, jc.ErrorIsNil)
   226  	c.Assert(names, gc.DeepEquals, expected)
   227  }
   228  
   229  func (s *storageSuite) TestList(c *gc.C) {
   230  	stor, storageDir := s.makeStorage(c)
   231  	s.assertList(c, stor, "", nil)
   232  
   233  	// Directories don't show up in List.
   234  	err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
   235  	c.Assert(err, jc.ErrorIsNil)
   236  	s.assertList(c, stor, "", nil)
   237  	s.assertList(c, stor, "a", nil)
   238  	createFiles(c, storageDir, "a/b1", "a/b2", "b")
   239  	s.assertList(c, stor, "", []string{"a/b1", "a/b2", "b"})
   240  	s.assertList(c, stor, "a", []string{"a/b1", "a/b2"})
   241  	s.assertList(c, stor, "a/b", []string{"a/b1", "a/b2"})
   242  	s.assertList(c, stor, "a/b1", []string{"a/b1"})
   243  	s.assertList(c, stor, "a/b3", nil)
   244  	s.assertList(c, stor, "a/b/c", nil)
   245  	s.assertList(c, stor, "b", []string{"b"})
   246  }
   247  
   248  func (s *storageSuite) TestRemove(c *gc.C) {
   249  	stor, storageDir := s.makeStorage(c)
   250  	err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
   251  	c.Assert(err, jc.ErrorIsNil)
   252  	createFiles(c, storageDir, "a/b1", "a/b2")
   253  	c.Assert(stor.Remove("a"), gc.ErrorMatches, "rm: cannot remove.*Is a directory")
   254  	s.assertList(c, stor, "", []string{"a/b1", "a/b2"})
   255  	c.Assert(stor.Remove("a/b"), gc.IsNil) // doesn't exist; not an error
   256  	s.assertList(c, stor, "", []string{"a/b1", "a/b2"})
   257  	c.Assert(stor.Remove("a/b2"), gc.IsNil)
   258  	s.assertList(c, stor, "", []string{"a/b1"})
   259  	c.Assert(stor.Remove("a/b1"), gc.IsNil)
   260  	s.assertList(c, stor, "", nil)
   261  }
   262  
   263  func (s *storageSuite) TestRemoveAll(c *gc.C) {
   264  	stor, storageDir := s.makeStorage(c)
   265  	err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
   266  	c.Assert(err, jc.ErrorIsNil)
   267  	createFiles(c, storageDir, "a/b1", "a/b2")
   268  	s.assertList(c, stor, "", []string{"a/b1", "a/b2"})
   269  	c.Assert(stor.RemoveAll(), gc.IsNil)
   270  	s.assertList(c, stor, "", nil)
   271  
   272  	// RemoveAll does not remove the base storage directory.
   273  	_, err = os.Stat(storageDir)
   274  	c.Assert(err, jc.ErrorIsNil)
   275  }
   276  
   277  func (s *storageSuite) TestURL(c *gc.C) {
   278  	stor, storageDir := s.makeStorage(c)
   279  	url, err := stor.URL("a/b")
   280  	c.Assert(err, jc.ErrorIsNil)
   281  	c.Assert(url, gc.Equals, "sftp://example.com/"+path.Join(storageDir, "a/b"))
   282  }
   283  
   284  func (s *storageSuite) TestDefaultConsistencyStrategy(c *gc.C) {
   285  	stor, _ := s.makeStorage(c)
   286  	c.Assert(stor.DefaultConsistencyStrategy(), gc.Equals, utils.AttemptStrategy{})
   287  }
   288  
   289  const defaultFlockTimeout = 5 * time.Second
   290  
   291  // flock is a test helper that flocks a file, executes "sleep" with the
   292  // specified duration, the command is terminated in the test tear down.
   293  func (s *storageSuite) flock(c *gc.C, mode flockmode, lockfile string) {
   294  	sleepcmd := fmt.Sprintf("echo started && sleep %vs", defaultFlockTimeout.Seconds())
   295  	cmd := exec.Command(flockBin, "--nonblock", "--close", string(mode), lockfile, "-c", sleepcmd)
   296  	stdout, err := cmd.StdoutPipe()
   297  	c.Assert(err, jc.ErrorIsNil)
   298  	c.Assert(cmd.Start(), gc.IsNil)
   299  	// Make sure the flock has been taken before returning by reading stdout waiting for "started"
   300  	_, err = io.ReadFull(stdout, make([]byte, len("started")))
   301  	c.Assert(err, jc.ErrorIsNil)
   302  	s.AddCleanup(func(*gc.C) {
   303  		cmd.Process.Kill()
   304  		cmd.Process.Wait()
   305  	})
   306  }
   307  
   308  func (s *storageSuite) TestCreateFailsIfFlockNotAvailable(c *gc.C) {
   309  	storageDir := c.MkDir()
   310  	s.flock(c, flockShared, storageDir)
   311  	// Creating storage requires an exclusive lock initially.
   312  	//
   313  	// flock exits with exit code 1 if it can't acquire the
   314  	// lock immediately in non-blocking mode (which the tests force).
   315  	_, err := newSSHStorage("example.com", storageDir, storageDir+"-tmp")
   316  	c.Assert(err, gc.ErrorMatches, "exit code 1")
   317  }
   318  
   319  func (s *storageSuite) TestWithSharedLocks(c *gc.C) {
   320  	stor, storageDir := s.makeStorage(c)
   321  
   322  	// Get and List should be able to proceed with a shared lock.
   323  	// All other methods should fail.
   324  	createFiles(c, storageDir, "a")
   325  
   326  	s.flock(c, flockShared, storageDir)
   327  	_, err := storage.Get(stor, "a")
   328  	c.Assert(err, jc.ErrorIsNil)
   329  	_, err = storage.List(stor, "")
   330  	c.Assert(err, jc.ErrorIsNil)
   331  	c.Assert(stor.Put("a", bytes.NewBuffer(nil), 0), gc.NotNil)
   332  	c.Assert(stor.Remove("a"), gc.NotNil)
   333  	c.Assert(stor.RemoveAll(), gc.NotNil)
   334  }
   335  
   336  func (s *storageSuite) TestWithExclusiveLocks(c *gc.C) {
   337  	stor, storageDir := s.makeStorage(c)
   338  	// None of the methods (apart from URL) should be able to do anything
   339  	// while an exclusive lock is held.
   340  	s.flock(c, flockExclusive, storageDir)
   341  	_, err := stor.URL("a")
   342  	c.Assert(err, jc.ErrorIsNil)
   343  	c.Assert(stor.Put("a", bytes.NewBuffer(nil), 0), gc.NotNil)
   344  	c.Assert(stor.Remove("a"), gc.NotNil)
   345  	c.Assert(stor.RemoveAll(), gc.NotNil)
   346  	_, err = storage.Get(stor, "a")
   347  	c.Assert(err, gc.NotNil)
   348  	_, err = storage.List(stor, "")
   349  	c.Assert(err, gc.NotNil)
   350  }
   351  
   352  func (s *storageSuite) TestPutLarge(c *gc.C) {
   353  	stor, _ := s.makeStorage(c)
   354  	buf := make([]byte, 1048576)
   355  	err := stor.Put("ohmy", bytes.NewBuffer(buf), int64(len(buf)))
   356  	c.Assert(err, jc.ErrorIsNil)
   357  }
   358  
   359  func (s *storageSuite) TestStorageDirBlank(c *gc.C) {
   360  	tmpdir := c.MkDir()
   361  	_, err := newSSHStorage("example.com", "", tmpdir)
   362  	c.Assert(err, gc.ErrorMatches, "storagedir must be specified and non-empty")
   363  }
   364  
   365  func (s *storageSuite) TestTmpDirBlank(c *gc.C) {
   366  	storageDir := c.MkDir()
   367  	_, err := newSSHStorage("example.com", storageDir, "")
   368  	c.Assert(err, gc.ErrorMatches, "tmpdir must be specified and non-empty")
   369  }
   370  
   371  func (s *storageSuite) TestTmpDirExists(c *gc.C) {
   372  	// If we explicitly set the temporary directory,
   373  	// it may already exist, but doesn't have to.
   374  	storageDir := c.MkDir()
   375  	tmpdirs := []string{storageDir, filepath.Join(storageDir, "subdir")}
   376  	for _, tmpdir := range tmpdirs {
   377  		stor, err := newSSHStorage("example.com", storageDir, tmpdir)
   378  		defer stor.Close()
   379  		c.Assert(err, jc.ErrorIsNil)
   380  		err = stor.Put("test-write", bytes.NewReader(nil), 0)
   381  		c.Assert(err, jc.ErrorIsNil)
   382  	}
   383  }
   384  
   385  func (s *storageSuite) TestTmpDirPermissions(c *gc.C) {
   386  	// newSSHStorage will fail if it can't create or change the
   387  	// permissions of the temporary directory.
   388  	storageDir := c.MkDir()
   389  	tmpdir := c.MkDir()
   390  	os.Chmod(tmpdir, 0400)
   391  	defer os.Chmod(tmpdir, 0755)
   392  	_, err := newSSHStorage("example.com", storageDir, filepath.Join(tmpdir, "subdir2"))
   393  	c.Assert(err, gc.ErrorMatches, ".*install: cannot create directory.*Permission denied.*")
   394  }
   395  
   396  func (s *storageSuite) TestPathCharacters(c *gc.C) {
   397  	storageDirBase := c.MkDir()
   398  	storageDir := filepath.Join(storageDirBase, "'")
   399  	tmpdir := filepath.Join(storageDirBase, `"`)
   400  	c.Assert(os.Mkdir(storageDir, 0755), gc.IsNil)
   401  	c.Assert(os.Mkdir(tmpdir, 0755), gc.IsNil)
   402  	_, err := newSSHStorage("example.com", storageDir, tmpdir)
   403  	c.Assert(err, jc.ErrorIsNil)
   404  }