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 }