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 }