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