Merge lp:~axwalk/juju-core/sshstorage-tmpdir into lp:~go-bot/juju-core/trunk
- sshstorage-tmpdir
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Penhey (community) | Needs Information | ||
Review via email: mp+185717@code.launchpad.net |
Commit message
environs/
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/
sshstorage and localstorage must agree on the
layout.
Now, instead of having storage/
the user can specify a separate temporary
directory. If left blank, storagedir+".tmp" will
be used.
Description of the change
environs/
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/
sshstorage and localstorage must agree on the
layout.
Now, instead of having storage/
the user can specify a separate temporary
directory. If left blank, storagedir+".tmp" will
be used.
Andrew Wilkins (axwalk) wrote : | # |
Tim Penhey (thumper) wrote : | # |
Mostly comments about test improvements.
https:/
File environs/
https:/
environs/
It isn't immediately obvious why you are doing this twice.
https:/
environs/
NewSSHStorage(
Same comment as for the filestorage, the "" here isn't obvious.
https:/
environs/
c.Assert(
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:/
environs/
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(
c.Assert(err, gc.IsNil)
s.AddCleanup
storage.
})
return storage
}
The tests then each get shorter, and logic is in one place.
https:/
environs/
c.Assert(
gc.IsNil)
This is also screaming out for a helper method.
You want the method to describe what is happening.
Andrew Wilkins (axwalk) wrote : | # |
https:/
File environs/
https:/
environs/
On 2013/09/18 04:39:41, thumper wrote:
> It isn't immediately obvious why you are doing this twice.
Added a comment.
https:/
environs/
NewSSHStorage(
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:/
environs/
c.Assert(
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(
https:/
environs/
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(
> 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:/
environs/
c.Assert(
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.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Tim Penhey (thumper) wrote : | # |
https:/
File environs/
https:/
environs/
Normally the tests would be in sshstorage_test package.
Is there any reason why this is a whitebox test?
https:/
environs/
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?
Tim Penhey (thumper) : | # |
Andrew Wilkins (axwalk) wrote : | # |
https:/
File environs/
https:/
environs/
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:/
environs/
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.
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:/
File environs/
https:/
environs/
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.
until it becomes
testbase.
Go Bot (go-bot) wrote : | # |
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.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
FAIL launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
Preview Diff
1 | === modified file 'environs/sshstorage/storage.go' |
2 | --- environs/sshstorage/storage.go 2013-09-18 22:54:32 +0000 |
3 | +++ environs/sshstorage/storage.go 2013-09-20 04:07:37 +0000 |
4 | @@ -21,18 +21,6 @@ |
5 | "launchpad.net/juju-core/utils" |
6 | ) |
7 | |
8 | -const ( |
9 | - // tmpdir is the name of the subdirectory |
10 | - // inside the remote storage directory where |
11 | - // temporary files are created. |
12 | - tmpdir = "tmp" |
13 | - |
14 | - // contentdir is the name of the subdirectory |
15 | - // inside the remote storage directory where |
16 | - // files are stored. |
17 | - contentdir = "content" |
18 | -) |
19 | - |
20 | // SSHStorage implements storage.Storage. |
21 | // |
22 | // The storage is created under sudo, and ownership given over to the |
23 | @@ -42,6 +30,7 @@ |
24 | type SSHStorage struct { |
25 | host string |
26 | remotepath string |
27 | + tmpdir string |
28 | |
29 | cmd *exec.Cmd |
30 | stdin io.WriteCloser |
31 | @@ -65,12 +54,24 @@ |
32 | flockExclusive flockmode = "-x" |
33 | ) |
34 | |
35 | +// UseDefaultTmpDir may be passed into NewSSHStorage |
36 | +// for the tmpdir argument, to signify that the default |
37 | +// value should be used. See NewSSHStorage for more. |
38 | +const UseDefaultTmpDir = "" |
39 | + |
40 | // NewSSHStorage creates a new SSHStorage, connected to the |
41 | // specified host, managing state under the specified remote path. |
42 | -func NewSSHStorage(host string, remotepath string) (*SSHStorage, error) { |
43 | - contentdir := path.Join(remotepath, contentdir) |
44 | - tmpdir := path.Join(remotepath, tmpdir) |
45 | - script := fmt.Sprintf("install -d -g $SUDO_GID -o $SUDO_UID %s %s", contentdir, tmpdir) |
46 | +// |
47 | +// A temporary directory may be specified, in which case it should |
48 | +// be a directory on the same filesystem as the storage directory |
49 | +// to ensure atomic writes. If left unspecified, tmpdir will be |
50 | +// assigned a value of storagedir+".tmp". |
51 | +// |
52 | +// If tmpdir == UseDefaultTmpDir, it will be created when Put is invoked, |
53 | +// and will be removed afterwards. If tmpdir != UseDefaultTmpDir, it must |
54 | +// already exist, and will never be removed. |
55 | +func NewSSHStorage(host, storagedir, tmpdir string) (*SSHStorage, error) { |
56 | + script := fmt.Sprintf("install -d -g $SUDO_GID -o $SUDO_UID %s", utils.ShQuote(storagedir)) |
57 | cmd := sshCommand(host, true, fmt.Sprintf("sudo bash -c '%s'", script)) |
58 | cmd.Stdin = os.Stdin |
59 | if out, err := cmd.CombinedOutput(); err != nil { |
60 | @@ -93,7 +94,8 @@ |
61 | } |
62 | stor := &SSHStorage{ |
63 | host: host, |
64 | - remotepath: remotepath, |
65 | + remotepath: storagedir, |
66 | + tmpdir: tmpdir, |
67 | cmd: cmd, |
68 | stdin: stdin, |
69 | stdout: stdout, |
70 | @@ -101,13 +103,8 @@ |
71 | } |
72 | cmd.Start() |
73 | |
74 | - // Verify we have write permissions, and set the temporary directory. |
75 | - _, err = stor.runf( |
76 | - flockExclusive, |
77 | - "touch %s && export TMPDIR=%s", |
78 | - utils.ShQuote(remotepath), |
79 | - utils.ShQuote(tmpdir), |
80 | - ) |
81 | + // Verify we have write permissions. |
82 | + _, err = stor.runf(flockExclusive, "touch %s", utils.ShQuote(storagedir)) |
83 | if err != nil { |
84 | stdin.Close() |
85 | stdout.Close() |
86 | @@ -164,9 +161,8 @@ |
87 | |
88 | // path returns a remote absolute path for a storage object name. |
89 | func (s *SSHStorage) path(name string) (string, error) { |
90 | - contentdir := path.Join(s.remotepath, contentdir) |
91 | - remotepath := path.Clean(path.Join(contentdir, name)) |
92 | - if !strings.HasPrefix(remotepath, contentdir) { |
93 | + remotepath := path.Clean(path.Join(s.remotepath, name)) |
94 | + if !strings.HasPrefix(remotepath, s.remotepath) { |
95 | return "", fmt.Errorf("%q escapes storage directory", name) |
96 | } |
97 | return remotepath, nil |
98 | @@ -209,10 +205,9 @@ |
99 | return nil, nil |
100 | } |
101 | var names []string |
102 | - contentdir := path.Join(s.remotepath, contentdir) |
103 | for _, name := range strings.Split(out, "\n") { |
104 | if strings.HasPrefix(name[len(dir):], prefix) { |
105 | - names = append(names, name[len(contentdir)+1:]) |
106 | + names = append(names, name[len(s.remotepath)+1:]) |
107 | } |
108 | } |
109 | sort.Strings(names) |
110 | @@ -250,9 +245,30 @@ |
111 | } |
112 | encoded := base64.StdEncoding.EncodeToString(buf) |
113 | path = utils.ShQuote(path) |
114 | + |
115 | + tmpdir := s.tmpdir |
116 | + if tmpdir == UseDefaultTmpDir { |
117 | + tmpdir = s.remotepath + ".tmp" |
118 | + } |
119 | + tmpdir = utils.ShQuote(tmpdir) |
120 | + |
121 | // Write to a temporary file ($TMPFILE), then mv atomically. |
122 | command := fmt.Sprintf("mkdir -p `dirname %s` && base64 -d > $TMPFILE", path) |
123 | - command = fmt.Sprintf("TMPFILE=`mktemp` && ((%s && mv $TMPFILE %s) || rm -f $TMPFILE)", command, path) |
124 | + command = fmt.Sprintf( |
125 | + "export TMPDIR=%s && TMPFILE=`mktemp` && ((%s && mv $TMPFILE %s) || rm -f $TMPFILE)", |
126 | + tmpdir, command, path, |
127 | + ) |
128 | + |
129 | + // If UseDefaultTmpDir is passed, then create the |
130 | + // temporary directory, and remove it afterwards. |
131 | + // Otherwise, the temporary directory is expected |
132 | + // to already exist, and is never removed. |
133 | + if s.tmpdir == UseDefaultTmpDir { |
134 | + installTmpdir := fmt.Sprintf("install -d -g $SUDO_GID -o $SUDO_UID %s", tmpdir) |
135 | + removeTmpdir := fmt.Sprintf("rm -fr %s", tmpdir) |
136 | + command = fmt.Sprintf("%s && (%s); rc=$?; %s; exit $rc", installTmpdir, command, removeTmpdir) |
137 | + } |
138 | + |
139 | command = fmt.Sprintf("(%s) << EOF\n%s\nEOF", command, encoded) |
140 | _, err = s.runf(flockExclusive, command+"\n") |
141 | return err |
142 | @@ -271,7 +287,6 @@ |
143 | |
144 | // RemoveAll implements storage.StorageWriter.RemoveAll |
145 | func (s *SSHStorage) RemoveAll() error { |
146 | - contentdir := path.Join(s.remotepath, contentdir) |
147 | - _, err := s.runf(flockExclusive, "rm -fr %s/*", utils.ShQuote(contentdir)) |
148 | + _, err := s.runf(flockExclusive, "rm -fr %s/*", utils.ShQuote(s.remotepath)) |
149 | return err |
150 | } |
151 | |
152 | === modified file 'environs/sshstorage/storage_test.go' |
153 | --- environs/sshstorage/storage_test.go 2013-09-20 02:53:59 +0000 |
154 | +++ environs/sshstorage/storage_test.go 2013-09-20 04:07:37 +0000 |
155 | @@ -27,7 +27,6 @@ |
156 | |
157 | type storageSuite struct { |
158 | testbase.LoggingSuite |
159 | - restoreEnv func() |
160 | } |
161 | |
162 | var _ = gc.Suite(&storageSuite{}) |
163 | @@ -36,8 +35,6 @@ |
164 | gc.TestingT(t) |
165 | } |
166 | |
167 | -var sshCommandOrig = sshCommand |
168 | - |
169 | func sshCommandTesting(host string, tty bool, command string) *exec.Cmd { |
170 | cmd := exec.Command("bash", "-c", command) |
171 | uid := fmt.Sprint(os.Getuid()) |
172 | @@ -59,50 +56,70 @@ |
173 | c.Assert(err, gc.IsNil) |
174 | |
175 | bin := c.MkDir() |
176 | - s.restoreEnv = testbase.PatchEnvironment("PATH", bin+":"+os.Getenv("PATH")) |
177 | + restoreEnv := testbase.PatchEnvironment("PATH", bin+":"+os.Getenv("PATH")) |
178 | + s.AddSuiteCleanup(func(*gc.C) { restoreEnv() }) |
179 | |
180 | // Create a "sudo" command which just executes its args. |
181 | - c.Assert(os.Symlink("/usr/bin/env", filepath.Join(bin, "sudo")), gc.IsNil) |
182 | - sshCommand = sshCommandTesting |
183 | + err = os.Symlink("/usr/bin/env", filepath.Join(bin, "sudo")) |
184 | + c.Assert(err, gc.IsNil) |
185 | + restoreSshCommand := testbase.PatchValue(&sshCommand, sshCommandTesting) |
186 | + s.AddSuiteCleanup(func(*gc.C) { restoreSshCommand() }) |
187 | |
188 | // Create a new "flock" which calls the original, but in non-blocking mode. |
189 | data := []byte(fmt.Sprintf("#!/bin/sh\nexec %s --nonblock \"$@\"", flockBin)) |
190 | - c.Assert(ioutil.WriteFile(filepath.Join(bin, "flock"), data, 0755), gc.IsNil) |
191 | -} |
192 | - |
193 | -func (s *storageSuite) TearDownSuite(c *gc.C) { |
194 | - sshCommand = sshCommandOrig |
195 | - s.restoreEnv() |
196 | - s.LoggingSuite.TearDownSuite(c) |
197 | + err = ioutil.WriteFile(filepath.Join(bin, "flock"), data, 0755) |
198 | + c.Assert(err, gc.IsNil) |
199 | +} |
200 | + |
201 | +func (s *storageSuite) makeStorage(c *gc.C) (storage *SSHStorage, storageDir string) { |
202 | + storageDir = c.MkDir() |
203 | + storage, err := NewSSHStorage("example.com", storageDir, "") |
204 | + c.Assert(err, gc.IsNil) |
205 | + c.Assert(storage, gc.NotNil) |
206 | + s.AddCleanup(func(*gc.C) { storage.Close() }) |
207 | + return storage, storageDir |
208 | +} |
209 | + |
210 | +// createFiles creates empty files in the storage directory |
211 | +// with the given storage names. |
212 | +func createFiles(c *gc.C, storageDir string, names ...string) { |
213 | + for _, name := range names { |
214 | + path := filepath.Join(storageDir, filepath.FromSlash(name)) |
215 | + dir := filepath.Dir(path) |
216 | + if err := os.MkdirAll(dir, 0755); err != nil { |
217 | + c.Assert(err, jc.Satisfies, os.IsExist) |
218 | + } |
219 | + err := ioutil.WriteFile(path, nil, 0644) |
220 | + c.Assert(err, gc.IsNil) |
221 | + } |
222 | } |
223 | |
224 | func (s *storageSuite) TestNewSSHStorage(c *gc.C) { |
225 | storageDir := c.MkDir() |
226 | + // Run this block twice to ensure NewSSHStorage can reuse |
227 | + // an existing storage location. |
228 | for i := 0; i < 2; i++ { |
229 | - stor, err := NewSSHStorage("example.com", storageDir) |
230 | + stor, err := NewSSHStorage("example.com", storageDir, UseDefaultTmpDir) |
231 | c.Assert(err, gc.IsNil) |
232 | c.Assert(stor, gc.NotNil) |
233 | c.Assert(stor.Close(), gc.IsNil) |
234 | } |
235 | - c.Assert(os.RemoveAll(storageDir), gc.IsNil) |
236 | + err := os.RemoveAll(storageDir) |
237 | + c.Assert(err, gc.IsNil) |
238 | |
239 | // You must have permissions to create the directory. |
240 | storageDir = c.MkDir() |
241 | - c.Assert(os.Chmod(storageDir, 0555), gc.IsNil) |
242 | - _, err := NewSSHStorage("example.com", filepath.Join(storageDir)) |
243 | + err = os.Chmod(storageDir, 0555) |
244 | + c.Assert(err, gc.IsNil) |
245 | + _, err = NewSSHStorage("example.com", filepath.Join(storageDir, "subdir"), UseDefaultTmpDir) |
246 | c.Assert(err, gc.ErrorMatches, "(.|\n)*cannot change owner and permissions of(.|\n)*") |
247 | } |
248 | |
249 | func (s *storageSuite) TestPathValidity(c *gc.C) { |
250 | - storageDir := c.MkDir() |
251 | - stor, err := NewSSHStorage("example.com", storageDir) |
252 | - c.Assert(err, gc.IsNil) |
253 | - defer stor.Close() |
254 | - |
255 | - c.Assert(os.Mkdir(filepath.Join(storageDir, contentdir, "a"), 0755), gc.IsNil) |
256 | - f, err := os.Create(filepath.Join(storageDir, contentdir, "a", "b")) |
257 | - c.Assert(err, gc.IsNil) |
258 | - c.Assert(f.Close(), gc.IsNil) |
259 | + stor, storageDir := s.makeStorage(c) |
260 | + err := os.Mkdir(filepath.Join(storageDir, "a"), 0755) |
261 | + c.Assert(err, gc.IsNil) |
262 | + createFiles(c, storageDir, "a/b") |
263 | |
264 | for _, prefix := range []string{"..", "a/../.."} { |
265 | c.Logf("prefix: %q", prefix) |
266 | @@ -122,14 +139,12 @@ |
267 | } |
268 | |
269 | func (s *storageSuite) TestGet(c *gc.C) { |
270 | - storageDir := c.MkDir() |
271 | - stor, err := NewSSHStorage("example.com", storageDir) |
272 | + stor, storageDir := s.makeStorage(c) |
273 | + data := []byte("abc\000def") |
274 | + err := os.Mkdir(filepath.Join(storageDir, "a"), 0755) |
275 | c.Assert(err, gc.IsNil) |
276 | - defer stor.Close() |
277 | - data := []byte("abc\000def") |
278 | - c.Assert(os.Mkdir(filepath.Join(storageDir, contentdir, "a"), 0755), gc.IsNil) |
279 | for _, name := range []string{"b", filepath.Join("a", "b")} { |
280 | - err = ioutil.WriteFile(filepath.Join(storageDir, contentdir, name), data, 0644) |
281 | + err = ioutil.WriteFile(filepath.Join(storageDir, name), data, 0644) |
282 | c.Assert(err, gc.IsNil) |
283 | r, err := storage.Get(stor, name) |
284 | c.Assert(err, gc.IsNil) |
285 | @@ -137,21 +152,17 @@ |
286 | c.Assert(err, gc.IsNil) |
287 | c.Assert(out, gc.DeepEquals, data) |
288 | } |
289 | - |
290 | _, err = storage.Get(stor, "notthere") |
291 | c.Assert(err, jc.Satisfies, coreerrors.IsNotFoundError) |
292 | } |
293 | |
294 | func (s *storageSuite) TestPut(c *gc.C) { |
295 | - storageDir := c.MkDir() |
296 | - stor, err := NewSSHStorage("example.com", storageDir) |
297 | - c.Assert(err, gc.IsNil) |
298 | - defer stor.Close() |
299 | + stor, storageDir := s.makeStorage(c) |
300 | data := []byte("abc\000def") |
301 | for _, name := range []string{"b", filepath.Join("a", "b")} { |
302 | - err = stor.Put(name, bytes.NewBuffer(data), int64(len(data))) |
303 | + err := stor.Put(name, bytes.NewBuffer(data), int64(len(data))) |
304 | c.Assert(err, gc.IsNil) |
305 | - out, err := ioutil.ReadFile(filepath.Join(storageDir, contentdir, name)) |
306 | + out, err := ioutil.ReadFile(filepath.Join(storageDir, name)) |
307 | c.Assert(err, gc.IsNil) |
308 | c.Assert(out, gc.DeepEquals, data) |
309 | } |
310 | @@ -165,20 +176,15 @@ |
311 | } |
312 | |
313 | func (s *storageSuite) TestList(c *gc.C) { |
314 | - storageDir := c.MkDir() |
315 | - stor, err := NewSSHStorage("example.com", storageDir) |
316 | - c.Assert(err, gc.IsNil) |
317 | - defer stor.Close() |
318 | + stor, storageDir := s.makeStorage(c) |
319 | s.assertList(c, stor, "", nil) |
320 | |
321 | // Directories don't show up in List. |
322 | - contentDir := filepath.Join(storageDir, contentdir) |
323 | - c.Assert(os.Mkdir(filepath.Join(contentDir, "a"), 0755), gc.IsNil) |
324 | + err := os.Mkdir(filepath.Join(storageDir, "a"), 0755) |
325 | + c.Assert(err, gc.IsNil) |
326 | s.assertList(c, stor, "", nil) |
327 | s.assertList(c, stor, "a", nil) |
328 | - c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b1"), nil, 0), gc.IsNil) |
329 | - c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b2"), nil, 0), gc.IsNil) |
330 | - c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "b"), nil, 0), gc.IsNil) |
331 | + createFiles(c, storageDir, "a/b1", "a/b2", "b") |
332 | s.assertList(c, stor, "", []string{"a/b1", "a/b2", "b"}) |
333 | s.assertList(c, stor, "a", []string{"a/b1", "a/b2"}) |
334 | s.assertList(c, stor, "a/b", []string{"a/b1", "a/b2"}) |
335 | @@ -189,15 +195,10 @@ |
336 | } |
337 | |
338 | func (s *storageSuite) TestRemove(c *gc.C) { |
339 | - storageDir := c.MkDir() |
340 | - stor, err := NewSSHStorage("example.com", storageDir) |
341 | + stor, storageDir := s.makeStorage(c) |
342 | + err := os.Mkdir(filepath.Join(storageDir, "a"), 0755) |
343 | c.Assert(err, gc.IsNil) |
344 | - defer stor.Close() |
345 | - |
346 | - contentDir := filepath.Join(storageDir, contentdir) |
347 | - c.Assert(os.Mkdir(filepath.Join(contentDir, "a"), 0755), gc.IsNil) |
348 | - c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b1"), nil, 0), gc.IsNil) |
349 | - c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b2"), nil, 0), gc.IsNil) |
350 | + createFiles(c, storageDir, "a/b1", "a/b2") |
351 | c.Assert(stor.Remove("a"), gc.ErrorMatches, "rm: cannot remove.*Is a directory") |
352 | s.assertList(c, stor, "", []string{"a/b1", "a/b2"}) |
353 | c.Assert(stor.Remove("a/b"), gc.IsNil) // doesn't exist; not an error |
354 | @@ -209,39 +210,28 @@ |
355 | } |
356 | |
357 | func (s *storageSuite) TestRemoveAll(c *gc.C) { |
358 | - storageDir := c.MkDir() |
359 | - stor, err := NewSSHStorage("example.com", storageDir) |
360 | + stor, storageDir := s.makeStorage(c) |
361 | + err := os.Mkdir(filepath.Join(storageDir, "a"), 0755) |
362 | c.Assert(err, gc.IsNil) |
363 | - defer stor.Close() |
364 | - |
365 | - contentDir := filepath.Join(storageDir, contentdir) |
366 | - c.Assert(os.Mkdir(filepath.Join(contentDir, "a"), 0755), gc.IsNil) |
367 | - c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b1"), nil, 0), gc.IsNil) |
368 | - c.Assert(ioutil.WriteFile(filepath.Join(contentDir, "a", "b2"), nil, 0), gc.IsNil) |
369 | + createFiles(c, storageDir, "a/b1", "a/b2") |
370 | s.assertList(c, stor, "", []string{"a/b1", "a/b2"}) |
371 | c.Assert(stor.RemoveAll(), gc.IsNil) |
372 | s.assertList(c, stor, "", nil) |
373 | |
374 | // RemoveAll does not remove the base storage directory. |
375 | - _, err = os.Stat(contentDir) |
376 | + _, err = os.Stat(storageDir) |
377 | c.Assert(err, gc.IsNil) |
378 | } |
379 | |
380 | func (s *storageSuite) TestURL(c *gc.C) { |
381 | - storageDir := c.MkDir() |
382 | - stor, err := NewSSHStorage("example.com", storageDir) |
383 | - c.Assert(err, gc.IsNil) |
384 | - defer stor.Close() |
385 | + stor, storageDir := s.makeStorage(c) |
386 | url, err := stor.URL("a/b") |
387 | c.Assert(err, gc.IsNil) |
388 | - c.Assert(url, gc.Equals, "sftp://example.com/"+path.Join(storageDir, contentdir, "a/b")) |
389 | + c.Assert(url, gc.Equals, "sftp://example.com/"+path.Join(storageDir, "a/b")) |
390 | } |
391 | |
392 | -func (s *storageSuite) TestConsistencyStrategy(c *gc.C) { |
393 | - storageDir := c.MkDir() |
394 | - stor, err := NewSSHStorage("example.com", storageDir) |
395 | - c.Assert(err, gc.IsNil) |
396 | - defer stor.Close() |
397 | +func (s *storageSuite) TestDefaultConsistencyStrategy(c *gc.C) { |
398 | + stor, _ := s.makeStorage(c) |
399 | c.Assert(stor.DefaultConsistencyStrategy(), gc.Equals, utils.AttemptStrategy{}) |
400 | } |
401 | |
402 | @@ -271,22 +261,19 @@ |
403 | // |
404 | // flock exits with exit code 1 if it can't acquire the |
405 | // lock immediately in non-blocking mode (which the tests force). |
406 | - _, err := NewSSHStorage("example.com", storageDir) |
407 | + _, err := NewSSHStorage("example.com", storageDir, UseDefaultTmpDir) |
408 | c.Assert(err, gc.ErrorMatches, "exit code 1") |
409 | } |
410 | |
411 | func (s *storageSuite) TestWithSharedLocks(c *gc.C) { |
412 | - storageDir := c.MkDir() |
413 | - stor, err := NewSSHStorage("example.com", storageDir) |
414 | - c.Assert(err, gc.IsNil) |
415 | + stor, storageDir := s.makeStorage(c) |
416 | |
417 | // Get and List should be able to proceed with a shared lock. |
418 | // All other methods should fail. |
419 | - data := []byte("abc\000def") |
420 | - c.Assert(ioutil.WriteFile(filepath.Join(storageDir, contentdir, "a"), data, 0644), gc.IsNil) |
421 | + createFiles(c, storageDir, "a") |
422 | |
423 | s.flock(c, flockShared, storageDir) |
424 | - _, err = storage.Get(stor, "a") |
425 | + _, err := storage.Get(stor, "a") |
426 | c.Assert(err, gc.IsNil) |
427 | _, err = storage.List(stor, "") |
428 | c.Assert(err, gc.IsNil) |
429 | @@ -296,13 +283,11 @@ |
430 | } |
431 | |
432 | func (s *storageSuite) TestWithExclusiveLocks(c *gc.C) { |
433 | - storageDir := c.MkDir() |
434 | - stor, err := NewSSHStorage("example.com", storageDir) |
435 | - c.Assert(err, gc.IsNil) |
436 | + stor, storageDir := s.makeStorage(c) |
437 | // None of the methods (apart from URL) should be able to do anything |
438 | // while an exclusive lock is held. |
439 | s.flock(c, flockExclusive, storageDir) |
440 | - _, err = stor.URL("a") |
441 | + _, err := stor.URL("a") |
442 | c.Assert(err, gc.IsNil) |
443 | c.Assert(stor.Put("a", bytes.NewBuffer(nil), 0), gc.NotNil) |
444 | c.Assert(stor.Remove("a"), gc.NotNil) |
445 | @@ -312,3 +297,39 @@ |
446 | _, err = storage.List(stor, "") |
447 | c.Assert(err, gc.NotNil) |
448 | } |
449 | + |
450 | +func (s *storageSuite) TestPutTmpDir(c *gc.C) { |
451 | + stor, storageDir := s.makeStorage(c) |
452 | + |
453 | + // Put should create and clean up the temporary directory if |
454 | + // tmpdir==UseDefaultTmpDir. |
455 | + err := stor.Put("test-write", bytes.NewReader(nil), 0) |
456 | + c.Assert(err, gc.IsNil) |
457 | + _, err = os.Stat(storageDir + ".tmp") |
458 | + c.Assert(err, jc.Satisfies, os.IsNotExist) |
459 | + |
460 | + // To deal with recovering from hard failure, UseDefaultTmpDir |
461 | + // doesn't care if the temporary directory already exists. It |
462 | + // still removes it, though. |
463 | + err = os.Mkdir(storageDir+".tmp", 0755) |
464 | + c.Assert(err, gc.IsNil) |
465 | + err = stor.Put("test-write", bytes.NewReader(nil), 0) |
466 | + c.Assert(err, gc.IsNil) |
467 | + _, err = os.Stat(storageDir + ".tmp") |
468 | + c.Assert(err, jc.Satisfies, os.IsNotExist) |
469 | + |
470 | + // If we explicitly set the temporary directory, it must already exist. |
471 | + stor.Close() |
472 | + stor, err = NewSSHStorage("example.com", storageDir, storageDir+".tmp") |
473 | + defer stor.Close() |
474 | + c.Assert(err, gc.IsNil) |
475 | + err = stor.Put("test-write", bytes.NewReader(nil), 0) |
476 | + c.Assert(err, gc.ErrorMatches, "mktemp: failed to create file.*No such file or directory") |
477 | + err = os.Mkdir(storageDir+".tmp", 0755) |
478 | + c.Assert(err, gc.IsNil) |
479 | + err = stor.Put("test-write", bytes.NewReader(nil), 0) |
480 | + c.Assert(err, gc.IsNil) |
481 | + // Temporary directory should not have been moved. |
482 | + _, err = os.Stat(storageDir + ".tmp") |
483 | + c.Assert(err, gc.IsNil) |
484 | +} |
Reviewers: mp+185717_ code.launchpad. net,
Message:
Please take a look.
Description: sshstorage: revert storage layout
environs/
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 localstorage. Thus, both
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/
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): sshstorage/ storage. go sshstorage/ storage_ test.go
A [revision details]
M environs/
M environs/