Merge lp:~axwalk/juju-core/sshstorage-tmpdir into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 1849
Proposed branch: lp:~axwalk/juju-core/sshstorage-tmpdir
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 484 lines (+154/-118)
2 files modified
environs/sshstorage/storage.go (+47/-32)
environs/sshstorage/storage_test.go (+107/-86)
To merge this branch: bzr merge lp:~axwalk/juju-core/sshstorage-tmpdir
Reviewer Review Type Date Requested Status
Tim Penhey (community) Needs Information
Review via email: mp+185717@code.launchpad.net

Commit message

environs/sshstorage: revert storage layout

In a lapse of sanity, I changed sshstorage's
layout so that the temporary directory was
contained within the designated storage
directory. This works fine in the context of
sshstorage, but not so well with how sshstorage
is intended to be used.

For bootstrapping the null provider, sshstorage
is used to operate on a specified remote
directory; later, when the machine agent is up,
it takes ownership of that directory and serves
it via environs/localstorage. Thus, both
sshstorage and localstorage must agree on the
layout.

Now, instead of having storage/{tmp,content},
the user can specify a separate temporary
directory. If left blank, storagedir+".tmp" will
be used.

https://codereview.appspot.com/13660047/

Description of the change

environs/sshstorage: revert storage layout

In a lapse of sanity, I changed sshstorage's
layout so that the temporary directory was
contained within the designated storage
directory. This works fine in the context of
sshstorage, but not so well with how sshstorage
is intended to be used.

For bootstrapping the null provider, sshstorage
is used to operate on a specified remote
directory; later, when the machine agent is up,
it takes ownership of that directory and serves
it via environs/localstorage. Thus, both
sshstorage and localstorage must agree on the
layout.

Now, instead of having storage/{tmp,content},
the user can specify a separate temporary
directory. If left blank, storagedir+".tmp" will
be used.

https://codereview.appspot.com/13660047/

To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote :

Reviewers: mp+185717_code.launchpad.net,

Message:
Please take a look.

Description:
environs/sshstorage: revert storage layout

In a lapse of sanity, I changed sshstorage's
layout so that the temporary directory was
contained within the designated storage
directory. This works fine in the context of
sshstorage, but not so well with how sshstorage
is intended to be used.

For bootstrapping the null provider, sshstorage
is used to operate on a specified remote
directory; later, when the machine agent is up,
it takes ownership of that directory and serves
it via environs/localstorage. Thus, both
sshstorage and localstorage must agree on the
layout.

Now, instead of having storage/{tmp,content},
the user can specify a separate temporary
directory. If left blank, storagedir+".tmp" will
be used.

https://code.launchpad.net/~axwalk/juju-core/sshstorage-tmpdir/+merge/185717

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/13660047/

Affected files (+48, -58 lines):
   A [revision details]
   M environs/sshstorage/storage.go
   M environs/sshstorage/storage_test.go

Revision history for this message
Tim Penhey (thumper) wrote :

Mostly comments about test improvements.

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go
File environs/sshstorage/storage_test.go (right):

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode80
environs/sshstorage/storage_test.go:80: for i := 0; i < 2; i++ {
It isn't immediately obvious why you are doing this twice.

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode81
environs/sshstorage/storage_test.go:81: storage, err :=
NewSSHStorage("example.com", storageDir, "")
Same comment as for the filestorage, the "" here isn't obvious.

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode101
environs/sshstorage/storage_test.go:101:
c.Assert(os.Mkdir(filepath.Join(storageDir, "a"), 0755), gc.IsNil)
I have to admit to finding putting the work inside the assert harder to
follow than having them separated out. To me it appears to confuse the
work and the test.

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode170
environs/sshstorage/storage_test.go:170: defer storage.Close()
Now that I've seen this same code three times... a function is in order.

func (s *storageSuite) makeStorage(c *gc.C) <storageType> {
   storageDir := c.MkDir()
   storage, err := NewSSHStorage("example.com", storageDir, "")
   c.Assert(err, gc.IsNil)
   s.AddCleanup(func (*gc.C) {
     storage.Close()
   })
   return storage
}

The tests then each get shorter, and logic is in one place.

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode177
environs/sshstorage/storage_test.go:177:
c.Assert(ioutil.WriteFile(filepath.Join(storageDir, "a", "b1"), nil, 0),
gc.IsNil)
This is also screaming out for a helper method.

You want the method to describe what is happening.

https://codereview.appspot.com/13660047/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go
File environs/sshstorage/storage_test.go (right):

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode80
environs/sshstorage/storage_test.go:80: for i := 0; i < 2; i++ {
On 2013/09/18 04:39:41, thumper wrote:
> It isn't immediately obvious why you are doing this twice.

Added a comment.

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode81
environs/sshstorage/storage_test.go:81: storage, err :=
NewSSHStorage("example.com", storageDir, "")
On 2013/09/18 04:39:41, thumper wrote:
> Same comment as for the filestorage, the "" here isn't obvious.

Done. Also updated Put to create/remove remove the default temporary
directory, though it's less of an issue here.

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode101
environs/sshstorage/storage_test.go:101:
c.Assert(os.Mkdir(filepath.Join(storageDir, "a"), 0755), gc.IsNil)
On 2013/09/18 04:39:41, thumper wrote:
> I have to admit to finding putting the work inside the assert harder
to follow
> than having them separated out. To me it appears to confuse the work
and the
> test.

I've separated the lengthier ones out to make it obvious what's the
code-under-test and what's the assertion; I've left many of the shorter
assertions alone, e.g. c.Assert(storage.Remove("a"), gc.NotNil).

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode170
environs/sshstorage/storage_test.go:170: defer storage.Close()
On 2013/09/18 04:39:41, thumper wrote:
> Now that I've seen this same code three times... a function is in
order.

> func (s *storageSuite) makeStorage(c *gc.C) <storageType> {
> storageDir := c.MkDir()
> storage, err := NewSSHStorage("example.com", storageDir, "")
> c.Assert(err, gc.IsNil)
> s.AddCleanup(func (*gc.C) {
> storage.Close()
> })
> return storage
> }

> The tests then each get shorter, and logic is in one place.

Done.

https://codereview.appspot.com/13660047/diff/1/environs/sshstorage/storage_test.go#newcode177
environs/sshstorage/storage_test.go:177:
c.Assert(ioutil.WriteFile(filepath.Join(storageDir, "a", "b1"), nil, 0),
gc.IsNil)
On 2013/09/18 04:39:41, thumper wrote:
> This is also screaming out for a helper method.

> You want the method to describe what is happening.

Yep, done. Added a "createFiles" test helper.

https://codereview.appspot.com/13660047/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Tim Penhey (thumper) wrote :

https://codereview.appspot.com/13660047/diff/7001/environs/sshstorage/storage_test.go
File environs/sshstorage/storage_test.go (right):

https://codereview.appspot.com/13660047/diff/7001/environs/sshstorage/storage_test.go#newcode4
environs/sshstorage/storage_test.go:4: package sshstorage
Normally the tests would be in sshstorage_test package.

Is there any reason why this is a whitebox test?

https://codereview.appspot.com/13660047/diff/7001/environs/sshstorage/storage_test.go#newcode74
environs/sshstorage/storage_test.go:74: func (s *storageSuite)
makeStorage(c *gc.C) (storage *SSHStorage, storageDir string) {
Why name the return parameters here? You aren't really gaining much. Is
it just to provide more assistance to the reader?

https://codereview.appspot.com/13660047/

Revision history for this message
Tim Penhey (thumper) :
review: Needs Information
Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/13660047/diff/7001/environs/sshstorage/storage_test.go
File environs/sshstorage/storage_test.go (right):

https://codereview.appspot.com/13660047/diff/7001/environs/sshstorage/storage_test.go#newcode4
environs/sshstorage/storage_test.go:4: package sshstorage
On 2013/09/18 21:30:56, thumper wrote:
> Normally the tests would be in sshstorage_test package.

> Is there any reason why this is a whitebox test?

Yes, so the tests can mock out "sshCommand". See the call to jc.Set in
SetUpSuite.

https://codereview.appspot.com/13660047/diff/7001/environs/sshstorage/storage_test.go#newcode74
environs/sshstorage/storage_test.go:74: func (s *storageSuite)
makeStorage(c *gc.C) (storage *SSHStorage, storageDir string) {
On 2013/09/18 21:30:56, thumper wrote:
> Why name the return parameters here? You aren't really gaining much.
Is it just
> to provide more assistance to the reader?

Yes, that was my rationale; I don't want to have to look at the
implementation to know what the result is.

It could be a doc comment, but in this case a self-documenting named
result is an easy win.

https://codereview.appspot.com/13660047/

Revision history for this message
Tim Penhey (thumper) wrote :

for tests that are whitebox tests, we have had an informal convention of
naming them _whitebox_test.go instead of _test.go

LGTM

https://codereview.appspot.com/13660047/diff/7001/environs/sshstorage/storage_test.go
File environs/sshstorage/storage_test.go (right):

https://codereview.appspot.com/13660047/diff/7001/environs/sshstorage/storage_test.go#newcode4
environs/sshstorage/storage_test.go:4: package sshstorage
On 2013/09/19 02:36:19, axw1 wrote:
> On 2013/09/18 21:30:56, thumper wrote:
> > Normally the tests would be in sshstorage_test package.
> >
> > Is there any reason why this is a whitebox test?

> Yes, so the tests can mock out "sshCommand". See the call to jc.Set in
> SetUpSuite.

jc.Set has moved, and is about to move again with my branch :-)

   testing.PatchValue
until it becomes
   testbase.PatchValue

https://codereview.appspot.com/13660047/

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (6.6 KiB)

The attempt to merge lp:~axwalk/juju-core/sshstorage-tmpdir into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core/agent 0.743s
ok launchpad.net/juju-core/agent/tools 0.275s
ok launchpad.net/juju-core/bzr 6.637s
ok launchpad.net/juju-core/cert 2.373s
ok launchpad.net/juju-core/charm 0.541s
? launchpad.net/juju-core/charm/hooks [no test files]
ok launchpad.net/juju-core/cloudinit 0.021s
ok launchpad.net/juju-core/cmd 0.244s
? launchpad.net/juju-core/cmd/builddb [no test files]
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 154.458s
ok launchpad.net/juju-core/cmd/jujud 39.250s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 1.840s
ok launchpad.net/juju-core/constraints 0.028s
ok launchpad.net/juju-core/container/lxc 0.351s
? launchpad.net/juju-core/container/lxc/mock [no test files]
ok launchpad.net/juju-core/downloader 5.288s
ok launchpad.net/juju-core/environs 1.844s
ok launchpad.net/juju-core/environs/bootstrap 4.872s
ok launchpad.net/juju-core/environs/cloudinit 0.426s
ok launchpad.net/juju-core/environs/config 0.799s
ok launchpad.net/juju-core/environs/configstore 0.063s
ok launchpad.net/juju-core/environs/filestorage 0.030s
ok launchpad.net/juju-core/environs/httpstorage 0.048s
ok launchpad.net/juju-core/environs/imagemetadata 0.489s
ok launchpad.net/juju-core/environs/instances 0.052s
ok launchpad.net/juju-core/environs/jujutest 0.214s
ok launchpad.net/juju-core/environs/manual 1.115s
ok launchpad.net/juju-core/environs/simplestreams 0.265s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
FAIL launchpad.net/juju-core/environs/sshstorage [build failed]
ok launchpad.net/juju-core/environs/storage 0.011s
ok launchpad.net/juju-core/environs/sync 26.073s
ok launchpad.net/juju-core/environs/testing 0.011s
ok launchpad.net/juju-core/environs/tools 10.424s
? launchpad.net/juju-core/environs/tools/testing [no test files]
? launchpad.net/juju-core/errors [no test files]
ok launchpad.net/juju-core/instance 0.022s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 20.195s
ok launchpad.net/juju-core/juju/osenv 0.018s
? launchpad.net/juju-core/juju/testing [no test files]
ok launchpad.net/juju-core/log 0.015s
ok launchpad.net/juju-core/log/syslog 0.022s
ok launchpad.net/juju-core/names 0.024s
ok launchpad.net/juju-core/provider 0.328s
? launchpad.net/juju-core/provider/all [no test files]
ok launchpad.net/juju-core/provider/azure 6.439s
ok launchpad.net/juju-core/provider/dummy 20.225s
ok launchpad.net/juju-core/provider/ec2 5.228s
ok launchpad.net/juju-core/provider/ec2/httpstorage 0.050s
ok launchpad.net/juju-core/provider/local 1.779s
? launchpad.net/juju-core/provider/local/storage [no test files]
ok launchpad.net/juju-core/provider/maas 3.733s
ok launchpad.net/juju-core/provider/openstack 8.343s
ok launchpad.net/juju-core/rpc 0.080s
ok launchpad.net/juju-core/rpc/jsoncodec 0.038s
ok launchpad.net/juju-core/schema 0....

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'environs/sshstorage/storage.go'
--- environs/sshstorage/storage.go 2013-09-18 22:54:32 +0000
+++ environs/sshstorage/storage.go 2013-09-20 04:07:37 +0000
@@ -21,18 +21,6 @@
21 "launchpad.net/juju-core/utils"21 "launchpad.net/juju-core/utils"
22)22)
2323
24const (
25 // tmpdir is the name of the subdirectory
26 // inside the remote storage directory where
27 // temporary files are created.
28 tmpdir = "tmp"
29
30 // contentdir is the name of the subdirectory
31 // inside the remote storage directory where
32 // files are stored.
33 contentdir = "content"
34)
35
36// SSHStorage implements storage.Storage.24// SSHStorage implements storage.Storage.
37//25//
38// The storage is created under sudo, and ownership given over to the26// The storage is created under sudo, and ownership given over to the
@@ -42,6 +30,7 @@
42type SSHStorage struct {30type SSHStorage struct {
43 host string31 host string
44 remotepath string32 remotepath string
33 tmpdir string
4534
46 cmd *exec.Cmd35 cmd *exec.Cmd
47 stdin io.WriteCloser36 stdin io.WriteCloser
@@ -65,12 +54,24 @@
65 flockExclusive flockmode = "-x"54 flockExclusive flockmode = "-x"
66)55)
6756
57// UseDefaultTmpDir may be passed into NewSSHStorage
58// for the tmpdir argument, to signify that the default
59// value should be used. See NewSSHStorage for more.
60const UseDefaultTmpDir = ""
61
68// NewSSHStorage creates a new SSHStorage, connected to the62// NewSSHStorage creates a new SSHStorage, connected to the
69// specified host, managing state under the specified remote path.63// specified host, managing state under the specified remote path.
70func NewSSHStorage(host string, remotepath string) (*SSHStorage, error) {64//
71 contentdir := path.Join(remotepath, contentdir)65// A temporary directory may be specified, in which case it should
72 tmpdir := path.Join(remotepath, tmpdir)66// be a directory on the same filesystem as the storage directory
73 script := fmt.Sprintf("install -d -g $SUDO_GID -o $SUDO_UID %s %s", contentdir, tmpdir)67// to ensure atomic writes. If left unspecified, tmpdir will be
68// assigned a value of storagedir+".tmp".
69//
70// If tmpdir == UseDefaultTmpDir, it will be created when Put is invoked,
71// and will be removed afterwards. If tmpdir != UseDefaultTmpDir, it must
72// already exist, and will never be removed.
73func NewSSHStorage(host, storagedir, tmpdir string) (*SSHStorage, error) {
74 script := fmt.Sprintf("install -d -g $SUDO_GID -o $SUDO_UID %s", utils.ShQuote(storagedir))
74 cmd := sshCommand(host, true, fmt.Sprintf("sudo bash -c '%s'", script))75 cmd := sshCommand(host, true, fmt.Sprintf("sudo bash -c '%s'", script))
75 cmd.Stdin = os.Stdin76 cmd.Stdin = os.Stdin
76 if out, err := cmd.CombinedOutput(); err != nil {77 if out, err := cmd.CombinedOutput(); err != nil {
@@ -93,7 +94,8 @@
93 }94 }
94 stor := &SSHStorage{95 stor := &SSHStorage{
95 host: host,96 host: host,
96 remotepath: remotepath,97 remotepath: storagedir,
98 tmpdir: tmpdir,
97 cmd: cmd,99 cmd: cmd,
98 stdin: stdin,100 stdin: stdin,
99 stdout: stdout,101 stdout: stdout,
@@ -101,13 +103,8 @@
101 }103 }
102 cmd.Start()104 cmd.Start()
103105
104 // Verify we have write permissions, and set the temporary directory.106 // Verify we have write permissions.
105 _, err = stor.runf(107 _, err = stor.runf(flockExclusive, "touch %s", utils.ShQuote(storagedir))
106 flockExclusive,
107 "touch %s && export TMPDIR=%s",
108 utils.ShQuote(remotepath),
109 utils.ShQuote(tmpdir),
110 )
111 if err != nil {108 if err != nil {
112 stdin.Close()109 stdin.Close()
113 stdout.Close()110 stdout.Close()
@@ -164,9 +161,8 @@
164161
165// path returns a remote absolute path for a storage object name.162// path returns a remote absolute path for a storage object name.
166func (s *SSHStorage) path(name string) (string, error) {163func (s *SSHStorage) path(name string) (string, error) {
167 contentdir := path.Join(s.remotepath, contentdir)164 remotepath := path.Clean(path.Join(s.remotepath, name))
168 remotepath := path.Clean(path.Join(contentdir, name))165 if !strings.HasPrefix(remotepath, s.remotepath) {
169 if !strings.HasPrefix(remotepath, contentdir) {
170 return "", fmt.Errorf("%q escapes storage directory", name)166 return "", fmt.Errorf("%q escapes storage directory", name)
171 }167 }
172 return remotepath, nil168 return remotepath, nil
@@ -209,10 +205,9 @@
209 return nil, nil205 return nil, nil
210 }206 }
211 var names []string207 var names []string
212 contentdir := path.Join(s.remotepath, contentdir)
213 for _, name := range strings.Split(out, "\n") {208 for _, name := range strings.Split(out, "\n") {
214 if strings.HasPrefix(name[len(dir):], prefix) {209 if strings.HasPrefix(name[len(dir):], prefix) {
215 names = append(names, name[len(contentdir)+1:])210 names = append(names, name[len(s.remotepath)+1:])
216 }211 }
217 }212 }
218 sort.Strings(names)213 sort.Strings(names)
@@ -250,9 +245,30 @@
250 }245 }
251 encoded := base64.StdEncoding.EncodeToString(buf)246 encoded := base64.StdEncoding.EncodeToString(buf)
252 path = utils.ShQuote(path)247 path = utils.ShQuote(path)
248
249 tmpdir := s.tmpdir
250 if tmpdir == UseDefaultTmpDir {
251 tmpdir = s.remotepath + ".tmp"
252 }
253 tmpdir = utils.ShQuote(tmpdir)
254
253 // Write to a temporary file ($TMPFILE), then mv atomically.255 // Write to a temporary file ($TMPFILE), then mv atomically.
254 command := fmt.Sprintf("mkdir -p `dirname %s` && base64 -d > $TMPFILE", path)256 command := fmt.Sprintf("mkdir -p `dirname %s` && base64 -d > $TMPFILE", path)
255 command = fmt.Sprintf("TMPFILE=`mktemp` && ((%s && mv $TMPFILE %s) || rm -f $TMPFILE)", command, path)257 command = fmt.Sprintf(
258 "export TMPDIR=%s && TMPFILE=`mktemp` && ((%s && mv $TMPFILE %s) || rm -f $TMPFILE)",
259 tmpdir, command, path,
260 )
261
262 // If UseDefaultTmpDir is passed, then create the
263 // temporary directory, and remove it afterwards.
264 // Otherwise, the temporary directory is expected
265 // to already exist, and is never removed.
266 if s.tmpdir == UseDefaultTmpDir {
267 installTmpdir := fmt.Sprintf("install -d -g $SUDO_GID -o $SUDO_UID %s", tmpdir)
268 removeTmpdir := fmt.Sprintf("rm -fr %s", tmpdir)
269 command = fmt.Sprintf("%s && (%s); rc=$?; %s; exit $rc", installTmpdir, command, removeTmpdir)
270 }
271
256 command = fmt.Sprintf("(%s) << EOF\n%s\nEOF", command, encoded)272 command = fmt.Sprintf("(%s) << EOF\n%s\nEOF", command, encoded)
257 _, err = s.runf(flockExclusive, command+"\n")273 _, err = s.runf(flockExclusive, command+"\n")
258 return err274 return err
@@ -271,7 +287,6 @@
271287
272// RemoveAll implements storage.StorageWriter.RemoveAll288// RemoveAll implements storage.StorageWriter.RemoveAll
273func (s *SSHStorage) RemoveAll() error {289func (s *SSHStorage) RemoveAll() error {
274 contentdir := path.Join(s.remotepath, contentdir)290 _, err := s.runf(flockExclusive, "rm -fr %s/*", utils.ShQuote(s.remotepath))
275 _, err := s.runf(flockExclusive, "rm -fr %s/*", utils.ShQuote(contentdir))
276 return err291 return err
277}292}
278293
=== modified file 'environs/sshstorage/storage_test.go'
--- environs/sshstorage/storage_test.go 2013-09-20 02:53:59 +0000
+++ environs/sshstorage/storage_test.go 2013-09-20 04:07:37 +0000
@@ -27,7 +27,6 @@
2727
28type storageSuite struct {28type storageSuite struct {
29 testbase.LoggingSuite29 testbase.LoggingSuite
30 restoreEnv func()
31}30}
3231
33var _ = gc.Suite(&storageSuite{})32var _ = gc.Suite(&storageSuite{})
@@ -36,8 +35,6 @@
36 gc.TestingT(t)35 gc.TestingT(t)
37}36}
3837
39var sshCommandOrig = sshCommand
40
41func sshCommandTesting(host string, tty bool, command string) *exec.Cmd {38func sshCommandTesting(host string, tty bool, command string) *exec.Cmd {
42 cmd := exec.Command("bash", "-c", command)39 cmd := exec.Command("bash", "-c", command)
43 uid := fmt.Sprint(os.Getuid())40 uid := fmt.Sprint(os.Getuid())
@@ -59,50 +56,70 @@
59 c.Assert(err, gc.IsNil)56 c.Assert(err, gc.IsNil)
6057
61 bin := c.MkDir()58 bin := c.MkDir()
62 s.restoreEnv = testbase.PatchEnvironment("PATH", bin+":"+os.Getenv("PATH"))59 restoreEnv := testbase.PatchEnvironment("PATH", bin+":"+os.Getenv("PATH"))
60 s.AddSuiteCleanup(func(*gc.C) { restoreEnv() })
6361
64 // Create a "sudo" command which just executes its args.62 // Create a "sudo" command which just executes its args.
65 c.Assert(os.Symlink("/usr/bin/env", filepath.Join(bin, "sudo")), gc.IsNil)63 err = os.Symlink("/usr/bin/env", filepath.Join(bin, "sudo"))
66 sshCommand = sshCommandTesting64 c.Assert(err, gc.IsNil)
65 restoreSshCommand := testbase.PatchValue(&sshCommand, sshCommandTesting)
66 s.AddSuiteCleanup(func(*gc.C) { restoreSshCommand() })
6767
68 // Create a new "flock" which calls the original, but in non-blocking mode.68 // Create a new "flock" which calls the original, but in non-blocking mode.
69 data := []byte(fmt.Sprintf("#!/bin/sh\nexec %s --nonblock \"$@\"", flockBin))69 data := []byte(fmt.Sprintf("#!/bin/sh\nexec %s --nonblock \"$@\"", flockBin))
70 c.Assert(ioutil.WriteFile(filepath.Join(bin, "flock"), data, 0755), gc.IsNil)70 err = ioutil.WriteFile(filepath.Join(bin, "flock"), data, 0755)
71}71 c.Assert(err, gc.IsNil)
7272}
73func (s *storageSuite) TearDownSuite(c *gc.C) {73
74 sshCommand = sshCommandOrig74func (s *storageSuite) makeStorage(c *gc.C) (storage *SSHStorage, storageDir string) {
75 s.restoreEnv()75 storageDir = c.MkDir()
76 s.LoggingSuite.TearDownSuite(c)76 storage, err := NewSSHStorage("example.com", storageDir, "")
77 c.Assert(err, gc.IsNil)
78 c.Assert(storage, gc.NotNil)
79 s.AddCleanup(func(*gc.C) { storage.Close() })
80 return storage, storageDir
81}
82
83// createFiles creates empty files in the storage directory
84// with the given storage names.
85func createFiles(c *gc.C, storageDir string, names ...string) {
86 for _, name := range names {
87 path := filepath.Join(storageDir, filepath.FromSlash(name))
88 dir := filepath.Dir(path)
89 if err := os.MkdirAll(dir, 0755); err != nil {
90 c.Assert(err, jc.Satisfies, os.IsExist)
91 }
92 err := ioutil.WriteFile(path, nil, 0644)
93 c.Assert(err, gc.IsNil)
94 }
77}95}
7896
79func (s *storageSuite) TestNewSSHStorage(c *gc.C) {97func (s *storageSuite) TestNewSSHStorage(c *gc.C) {
80 storageDir := c.MkDir()98 storageDir := c.MkDir()
99 // Run this block twice to ensure NewSSHStorage can reuse
100 // an existing storage location.
81 for i := 0; i < 2; i++ {101 for i := 0; i < 2; i++ {
82 stor, err := NewSSHStorage("example.com", storageDir)102 stor, err := NewSSHStorage("example.com", storageDir, UseDefaultTmpDir)
83 c.Assert(err, gc.IsNil)103 c.Assert(err, gc.IsNil)
84 c.Assert(stor, gc.NotNil)104 c.Assert(stor, gc.NotNil)
85 c.Assert(stor.Close(), gc.IsNil)105 c.Assert(stor.Close(), gc.IsNil)
86 }106 }
87 c.Assert(os.RemoveAll(storageDir), gc.IsNil)107 err := os.RemoveAll(storageDir)
108 c.Assert(err, gc.IsNil)
88109
89 // You must have permissions to create the directory.110 // You must have permissions to create the directory.
90 storageDir = c.MkDir()111 storageDir = c.MkDir()
91 c.Assert(os.Chmod(storageDir, 0555), gc.IsNil)112 err = os.Chmod(storageDir, 0555)
92 _, err := NewSSHStorage("example.com", filepath.Join(storageDir))113 c.Assert(err, gc.IsNil)
114 _, err = NewSSHStorage("example.com", filepath.Join(storageDir, "subdir"), UseDefaultTmpDir)
93 c.Assert(err, gc.ErrorMatches, "(.|\n)*cannot change owner and permissions of(.|\n)*")115 c.Assert(err, gc.ErrorMatches, "(.|\n)*cannot change owner and permissions of(.|\n)*")
94}116}
95117
96func (s *storageSuite) TestPathValidity(c *gc.C) {118func (s *storageSuite) TestPathValidity(c *gc.C) {
97 storageDir := c.MkDir()119 stor, storageDir := s.makeStorage(c)
98 stor, err := NewSSHStorage("example.com", storageDir)120 err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
99 c.Assert(err, gc.IsNil)121 c.Assert(err, gc.IsNil)
100 defer stor.Close()122 createFiles(c, storageDir, "a/b")
101
102 c.Assert(os.Mkdir(filepath.Join(storageDir, contentdir, "a"), 0755), gc.IsNil)
103 f, err := os.Create(filepath.Join(storageDir, contentdir, "a", "b"))
104 c.Assert(err, gc.IsNil)
105 c.Assert(f.Close(), gc.IsNil)
106123
107 for _, prefix := range []string{"..", "a/../.."} {124 for _, prefix := range []string{"..", "a/../.."} {
108 c.Logf("prefix: %q", prefix)125 c.Logf("prefix: %q", prefix)
@@ -122,14 +139,12 @@
122}139}
123140
124func (s *storageSuite) TestGet(c *gc.C) {141func (s *storageSuite) TestGet(c *gc.C) {
125 storageDir := c.MkDir()142 stor, storageDir := s.makeStorage(c)
126 stor, err := NewSSHStorage("example.com", storageDir)143 data := []byte("abc\000def")
144 err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
127 c.Assert(err, gc.IsNil)145 c.Assert(err, gc.IsNil)
128 defer stor.Close()
129 data := []byte("abc\000def")
130 c.Assert(os.Mkdir(filepath.Join(storageDir, contentdir, "a"), 0755), gc.IsNil)
131 for _, name := range []string{"b", filepath.Join("a", "b")} {146 for _, name := range []string{"b", filepath.Join("a", "b")} {
132 err = ioutil.WriteFile(filepath.Join(storageDir, contentdir, name), data, 0644)147 err = ioutil.WriteFile(filepath.Join(storageDir, name), data, 0644)
133 c.Assert(err, gc.IsNil)148 c.Assert(err, gc.IsNil)
134 r, err := storage.Get(stor, name)149 r, err := storage.Get(stor, name)
135 c.Assert(err, gc.IsNil)150 c.Assert(err, gc.IsNil)
@@ -137,21 +152,17 @@
137 c.Assert(err, gc.IsNil)152 c.Assert(err, gc.IsNil)
138 c.Assert(out, gc.DeepEquals, data)153 c.Assert(out, gc.DeepEquals, data)
139 }154 }
140
141 _, err = storage.Get(stor, "notthere")155 _, err = storage.Get(stor, "notthere")
142 c.Assert(err, jc.Satisfies, coreerrors.IsNotFoundError)156 c.Assert(err, jc.Satisfies, coreerrors.IsNotFoundError)
143}157}
144158
145func (s *storageSuite) TestPut(c *gc.C) {159func (s *storageSuite) TestPut(c *gc.C) {
146 storageDir := c.MkDir()160 stor, storageDir := s.makeStorage(c)
147 stor, err := NewSSHStorage("example.com", storageDir)
148 c.Assert(err, gc.IsNil)
149 defer stor.Close()
150 data := []byte("abc\000def")161 data := []byte("abc\000def")
151 for _, name := range []string{"b", filepath.Join("a", "b")} {162 for _, name := range []string{"b", filepath.Join("a", "b")} {
152 err = stor.Put(name, bytes.NewBuffer(data), int64(len(data)))163 err := stor.Put(name, bytes.NewBuffer(data), int64(len(data)))
153 c.Assert(err, gc.IsNil)164 c.Assert(err, gc.IsNil)
154 out, err := ioutil.ReadFile(filepath.Join(storageDir, contentdir, name))165 out, err := ioutil.ReadFile(filepath.Join(storageDir, name))
155 c.Assert(err, gc.IsNil)166 c.Assert(err, gc.IsNil)
156 c.Assert(out, gc.DeepEquals, data)167 c.Assert(out, gc.DeepEquals, data)
157 }168 }
@@ -165,20 +176,15 @@
165}176}
166177
167func (s *storageSuite) TestList(c *gc.C) {178func (s *storageSuite) TestList(c *gc.C) {
168 storageDir := c.MkDir()179 stor, storageDir := s.makeStorage(c)
169 stor, err := NewSSHStorage("example.com", storageDir)
170 c.Assert(err, gc.IsNil)
171 defer stor.Close()
172 s.assertList(c, stor, "", nil)180 s.assertList(c, stor, "", nil)
173181
174 // Directories don't show up in List.182 // Directories don't show up in List.
175 contentDir := filepath.Join(storageDir, contentdir)183 err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
176 c.Assert(os.Mkdir(filepath.Join(contentDir, "a"), 0755), gc.IsNil)184 c.Assert(err, gc.IsNil)
177 s.assertList(c, stor, "", nil)185 s.assertList(c, stor, "", nil)
178 s.assertList(c, stor, "a", nil)186 s.assertList(c, stor, "a", nil)
179 c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b1"), nil, 0), gc.IsNil)187 createFiles(c, storageDir, "a/b1", "a/b2", "b")
180 c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b2"), nil, 0), gc.IsNil)
181 c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "b"), nil, 0), gc.IsNil)
182 s.assertList(c, stor, "", []string{"a/b1", "a/b2", "b"})188 s.assertList(c, stor, "", []string{"a/b1", "a/b2", "b"})
183 s.assertList(c, stor, "a", []string{"a/b1", "a/b2"})189 s.assertList(c, stor, "a", []string{"a/b1", "a/b2"})
184 s.assertList(c, stor, "a/b", []string{"a/b1", "a/b2"})190 s.assertList(c, stor, "a/b", []string{"a/b1", "a/b2"})
@@ -189,15 +195,10 @@
189}195}
190196
191func (s *storageSuite) TestRemove(c *gc.C) {197func (s *storageSuite) TestRemove(c *gc.C) {
192 storageDir := c.MkDir()198 stor, storageDir := s.makeStorage(c)
193 stor, err := NewSSHStorage("example.com", storageDir)199 err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
194 c.Assert(err, gc.IsNil)200 c.Assert(err, gc.IsNil)
195 defer stor.Close()201 createFiles(c, storageDir, "a/b1", "a/b2")
196
197 contentDir := filepath.Join(storageDir, contentdir)
198 c.Assert(os.Mkdir(filepath.Join(contentDir, "a"), 0755), gc.IsNil)
199 c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b1"), nil, 0), gc.IsNil)
200 c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b2"), nil, 0), gc.IsNil)
201 c.Assert(stor.Remove("a"), gc.ErrorMatches, "rm: cannot remove.*Is a directory")202 c.Assert(stor.Remove("a"), gc.ErrorMatches, "rm: cannot remove.*Is a directory")
202 s.assertList(c, stor, "", []string{"a/b1", "a/b2"})203 s.assertList(c, stor, "", []string{"a/b1", "a/b2"})
203 c.Assert(stor.Remove("a/b"), gc.IsNil) // doesn't exist; not an error204 c.Assert(stor.Remove("a/b"), gc.IsNil) // doesn't exist; not an error
@@ -209,39 +210,28 @@
209}210}
210211
211func (s *storageSuite) TestRemoveAll(c *gc.C) {212func (s *storageSuite) TestRemoveAll(c *gc.C) {
212 storageDir := c.MkDir()213 stor, storageDir := s.makeStorage(c)
213 stor, err := NewSSHStorage("example.com", storageDir)214 err := os.Mkdir(filepath.Join(storageDir, "a"), 0755)
214 c.Assert(err, gc.IsNil)215 c.Assert(err, gc.IsNil)
215 defer stor.Close()216 createFiles(c, storageDir, "a/b1", "a/b2")
216
217 contentDir := filepath.Join(storageDir, contentdir)
218 c.Assert(os.Mkdir(filepath.Join(contentDir, "a"), 0755), gc.IsNil)
219 c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b1"), nil, 0), gc.IsNil)
220 c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b2"), nil, 0), gc.IsNil)
221 s.assertList(c, stor, "", []string{"a/b1", "a/b2"})217 s.assertList(c, stor, "", []string{"a/b1", "a/b2"})
222 c.Assert(stor.RemoveAll(), gc.IsNil)218 c.Assert(stor.RemoveAll(), gc.IsNil)
223 s.assertList(c, stor, "", nil)219 s.assertList(c, stor, "", nil)
224220
225 // RemoveAll does not remove the base storage directory.221 // RemoveAll does not remove the base storage directory.
226 _, err = os.Stat(contentDir)222 _, err = os.Stat(storageDir)
227 c.Assert(err, gc.IsNil)223 c.Assert(err, gc.IsNil)
228}224}
229225
230func (s *storageSuite) TestURL(c *gc.C) {226func (s *storageSuite) TestURL(c *gc.C) {
231 storageDir := c.MkDir()227 stor, storageDir := s.makeStorage(c)
232 stor, err := NewSSHStorage("example.com", storageDir)
233 c.Assert(err, gc.IsNil)
234 defer stor.Close()
235 url, err := stor.URL("a/b")228 url, err := stor.URL("a/b")
236 c.Assert(err, gc.IsNil)229 c.Assert(err, gc.IsNil)
237 c.Assert(url, gc.Equals, "sftp://example.com/"+path.Join(storageDir, contentdir, "a/b"))230 c.Assert(url, gc.Equals, "sftp://example.com/"+path.Join(storageDir, "a/b"))
238}231}
239232
240func (s *storageSuite) TestConsistencyStrategy(c *gc.C) {233func (s *storageSuite) TestDefaultConsistencyStrategy(c *gc.C) {
241 storageDir := c.MkDir()234 stor, _ := s.makeStorage(c)
242 stor, err := NewSSHStorage("example.com", storageDir)
243 c.Assert(err, gc.IsNil)
244 defer stor.Close()
245 c.Assert(stor.DefaultConsistencyStrategy(), gc.Equals, utils.AttemptStrategy{})235 c.Assert(stor.DefaultConsistencyStrategy(), gc.Equals, utils.AttemptStrategy{})
246}236}
247237
@@ -271,22 +261,19 @@
271 //261 //
272 // flock exits with exit code 1 if it can't acquire the262 // flock exits with exit code 1 if it can't acquire the
273 // lock immediately in non-blocking mode (which the tests force).263 // lock immediately in non-blocking mode (which the tests force).
274 _, err := NewSSHStorage("example.com", storageDir)264 _, err := NewSSHStorage("example.com", storageDir, UseDefaultTmpDir)
275 c.Assert(err, gc.ErrorMatches, "exit code 1")265 c.Assert(err, gc.ErrorMatches, "exit code 1")
276}266}
277267
278func (s *storageSuite) TestWithSharedLocks(c *gc.C) {268func (s *storageSuite) TestWithSharedLocks(c *gc.C) {
279 storageDir := c.MkDir()269 stor, storageDir := s.makeStorage(c)
280 stor, err := NewSSHStorage("example.com", storageDir)
281 c.Assert(err, gc.IsNil)
282270
283 // Get and List should be able to proceed with a shared lock.271 // Get and List should be able to proceed with a shared lock.
284 // All other methods should fail.272 // All other methods should fail.
285 data := []byte("abc\000def")273 createFiles(c, storageDir, "a")
286 c.Assert(ioutil.WriteFile(filepath.Join(storageDir, contentdir, "a"), data, 0644), gc.IsNil)
287274
288 s.flock(c, flockShared, storageDir)275 s.flock(c, flockShared, storageDir)
289 _, err = storage.Get(stor, "a")276 _, err := storage.Get(stor, "a")
290 c.Assert(err, gc.IsNil)277 c.Assert(err, gc.IsNil)
291 _, err = storage.List(stor, "")278 _, err = storage.List(stor, "")
292 c.Assert(err, gc.IsNil)279 c.Assert(err, gc.IsNil)
@@ -296,13 +283,11 @@
296}283}
297284
298func (s *storageSuite) TestWithExclusiveLocks(c *gc.C) {285func (s *storageSuite) TestWithExclusiveLocks(c *gc.C) {
299 storageDir := c.MkDir()286 stor, storageDir := s.makeStorage(c)
300 stor, err := NewSSHStorage("example.com", storageDir)
301 c.Assert(err, gc.IsNil)
302 // None of the methods (apart from URL) should be able to do anything287 // None of the methods (apart from URL) should be able to do anything
303 // while an exclusive lock is held.288 // while an exclusive lock is held.
304 s.flock(c, flockExclusive, storageDir)289 s.flock(c, flockExclusive, storageDir)
305 _, err = stor.URL("a")290 _, err := stor.URL("a")
306 c.Assert(err, gc.IsNil)291 c.Assert(err, gc.IsNil)
307 c.Assert(stor.Put("a", bytes.NewBuffer(nil), 0), gc.NotNil)292 c.Assert(stor.Put("a", bytes.NewBuffer(nil), 0), gc.NotNil)
308 c.Assert(stor.Remove("a"), gc.NotNil)293 c.Assert(stor.Remove("a"), gc.NotNil)
@@ -312,3 +297,39 @@
312 _, err = storage.List(stor, "")297 _, err = storage.List(stor, "")
313 c.Assert(err, gc.NotNil)298 c.Assert(err, gc.NotNil)
314}299}
300
301func (s *storageSuite) TestPutTmpDir(c *gc.C) {
302 stor, storageDir := s.makeStorage(c)
303
304 // Put should create and clean up the temporary directory if
305 // tmpdir==UseDefaultTmpDir.
306 err := stor.Put("test-write", bytes.NewReader(nil), 0)
307 c.Assert(err, gc.IsNil)
308 _, err = os.Stat(storageDir + ".tmp")
309 c.Assert(err, jc.Satisfies, os.IsNotExist)
310
311 // To deal with recovering from hard failure, UseDefaultTmpDir
312 // doesn't care if the temporary directory already exists. It
313 // still removes it, though.
314 err = os.Mkdir(storageDir+".tmp", 0755)
315 c.Assert(err, gc.IsNil)
316 err = stor.Put("test-write", bytes.NewReader(nil), 0)
317 c.Assert(err, gc.IsNil)
318 _, err = os.Stat(storageDir + ".tmp")
319 c.Assert(err, jc.Satisfies, os.IsNotExist)
320
321 // If we explicitly set the temporary directory, it must already exist.
322 stor.Close()
323 stor, err = NewSSHStorage("example.com", storageDir, storageDir+".tmp")
324 defer stor.Close()
325 c.Assert(err, gc.IsNil)
326 err = stor.Put("test-write", bytes.NewReader(nil), 0)
327 c.Assert(err, gc.ErrorMatches, "mktemp: failed to create file.*No such file or directory")
328 err = os.Mkdir(storageDir+".tmp", 0755)
329 c.Assert(err, gc.IsNil)
330 err = stor.Put("test-write", bytes.NewReader(nil), 0)
331 c.Assert(err, gc.IsNil)
332 // Temporary directory should not have been moved.
333 _, err = os.Stat(storageDir + ".tmp")
334 c.Assert(err, gc.IsNil)
335}

Subscribers

People subscribed via source and target branches

to status/vote changes: