Merge lp:~themue/juju-core/009-local-environ-storage into lp:~juju/juju-core/trunk
- 009-local-environ-storage
- Merge into trunk
Status: | Rejected |
---|---|
Rejected by: | Frank Mueller |
Proposed branch: | lp:~themue/juju-core/009-local-environ-storage |
Merge into: | lp:~juju/juju-core/trunk |
Diff against target: |
1214 lines (+738/-437) 8 files modified
environs/local/export_test.go (+15/-0) environs/local/local.go (+232/-0) environs/local/local_test.go (+231/-0) environs/local/lxc.go (+0/-64) environs/local/lxc_test.go (+0/-81) environs/local/network.go (+0/-148) environs/local/network_test.go (+0/-144) environs/local/storage.go (+260/-0) |
To merge this branch: | bzr merge lp:~themue/juju-core/009-local-environ-storage |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
The Go Language Gophers | Pending | ||
Review via email: mp+148183@code.launchpad.net |
Commit message
Description of the change
local: added storage for environment
The backend pf the local storage is based on an internal
Go HTTP server using <basedir>
<basedir>
some clean-up and first implementation steps of the
interfaces.
- 889. By Frank Mueller
-
local: changed to use empty public storage
William Reade (fwereade) wrote : | # |
- 890. By Frank Mueller
-
config: changes after review
- 891. By Frank Mueller
-
environ: merging, consolidation, comment changing
William Reade (fwereade) wrote : | # |
A few more thoughts follow; but I'm really confused by this patch set.
What's happened to config.go? Also: I don't see any tests for the
provider/environ at all.
https:/
File environs/
https:/
environs/
Not amazon, please.
https:/
environs/
representing the host system.
s/./, if requested./ ?
https:/
File environs/
https:/
environs/
This'll need to move, as discussed online. Would you put it in a
separate file please? I'm fine keeping it in this package for the very
short term, though.
https:/
environs/
using the given path.
s/newContainer/
https:/
environs/
resp.StatusCode > 299 {
These seem a bit loosely specified. We control the backend; don't we
know what codes we should be returning for various operations?
https:/
environs/
resp.StatusCode > 299 {
As above.
https:/
environs/
fmt.Sprintf("http://
nil
This needs to work from inside containers.
https:/
environs/
resp.StatusCode > 299 {
As above.
https:/
environs/
resp.StatusCode > 299 {
As above.
Roger Peppe (rogpeppe) wrote : | # |
some comments; there might be more
https:/
File environs/
https:/
environs/
i don't think we care much about the distinction between 404 and 500
here. i think we can just do:
http.Error(w, fmt.Sprintf("404 %v", err), http.StatusNotF
https:/
environs/
i think this operation should be PUT not post (there's nothing in the
API that says we can't write the same object twice); then we can get rid
of all the Stat and all error special casing here, and just return error
403 and the error.
https:/
environs/
there's no need to read everything into memory here.
out, err := os.Create(fp)
defer out.Close()
if err != nil {...}
err = io.Copy(out, req.Body)
if err != nil {...}
if we think it needs to be atomic (i don't
think it matters really), then write to
a temporary file in the same dir, then rename it
to the destination.
https:/
environs/
if err := os.Remove(fp); err != nil && !os.IsNotExist(err) {
https:/
environs/
in the state's storage.
// listen starts an HTTP listener to serve the
// provider storage.
?
(it doesn't seem worth listing the methods here)
https:/
environs/
environName)
as this code is closely bound to the details of storageBackend, i'd
either put this function closer to newStorageBackend, or (my preference)
inline the constructor
in this function and delete newStorageBackend.
then it's obvious what backend.uri is below (which is what prompted this
remark)
https:/
environs/
0755); err != nil {
i wonder we should use 0777 here and let the caller's umask deal with
desired permissions.
https:/
environs/
%v", err))
return nil, err
(it's not an unlikely error if the port is not zero)
https:/
environs/
interface.
s/Storage/
?
https:/
environs/
s/fmt.Errorf/
Unmerged revisions
- 891. By Frank Mueller
-
environ: merging, consolidation, comment changing
- 890. By Frank Mueller
-
config: changes after review
- 889. By Frank Mueller
-
local: changed to use empty public storage
- 888. By Frank Mueller
-
local: first steps, adding a local storage
- 887. By John A Meinel
-
testing/charms: Remove the series parameter
This change removes some of the symlinks in the working tree (testing/repo/*).
Following on to it, there were a bunch of apis in testing.Charms that took a
series parameter, but all callers were just passing "series" anyway.All the tests still pass, so I'm pretty happy with the changes.
R=dimitern, gz
CC=
https://codereview. appspot. com/7301073 - 886. By Gavin Panella
-
store, cmd/charm: Move them into lp:juju-store
Break out store and cmd/charm* into lp:juju-store
Red Squad are going to be working on the charm store, and it was
suggested that an early task would be to split the charm store out
into a separate project. That work has already been done - see
lp:juju-store - and this is the clean-up job.mgz helped me a lot in doing both these tasks.
Fwiw, juju-store has not been advertised, so feel free to suggest a
different name.R=fwereade
CC=
https://codereview. appspot. com/7058063/ - 885. By Roger Peppe
-
rpc: allow concurrent calls
The client side comes substantially from
the net/rpc package (no point in reinventing
the same wheel); the server side is a little
simpler, I hope.R=fwereade
CC=
https://codereview. appspot. com/7307090 - 884. By Roger Peppe
-
state/api: enable password checking
R=TheMue, dimitern, fwereade
CC=
https://codereview. appspot. com/7299066 - 883. By Roger Peppe
-
rpc: new package
The rpc package is styled after the standard
Go rpc package, but is a little more flexible
and closer to our requirements.We also change state/api to use this
package, so there's an idea of how it
will look in practice.R=dimitern, TheMue, fwereade
CC=
https://codereview. appspot. com/6878052 - 882. By William Reade
-
state: remove AddUnitSubordin
ateTo This involved entirely replacing the machine units watcher tests. They're
noticeably simplified; I don't think that I have reduced coverage, because
all the important considerations are still handled; I've just dropped all
the redundant coverage of multiple changes for each operation. If I've
missed something, please let me know.R=dimitern, jameinel
CC=
https://codereview. appspot. com/7308080
Preview Diff
1 | === added file 'environs/local/export_test.go' |
2 | --- environs/local/export_test.go 1970-01-01 00:00:00 +0000 |
3 | +++ environs/local/export_test.go 2013-02-14 18:53:23 +0000 |
4 | @@ -0,0 +1,15 @@ |
5 | +package local |
6 | + |
7 | +import ( |
8 | + "net" |
9 | + |
10 | + "launchpad.net/juju-core/environs" |
11 | +) |
12 | + |
13 | +func Listen(basepath, environName, ip string, port int) (net.Listener, error) { |
14 | + return listen(basepath, environName, ip, port) |
15 | +} |
16 | + |
17 | +func NewStorage(port int, environName string) environs.Storage { |
18 | + return newStorage(port, environName) |
19 | +} |
20 | |
21 | === added file 'environs/local/local.go' |
22 | --- environs/local/local.go 1970-01-01 00:00:00 +0000 |
23 | +++ environs/local/local.go 2013-02-14 18:53:23 +0000 |
24 | @@ -0,0 +1,232 @@ |
25 | +package local |
26 | + |
27 | +import ( |
28 | + "fmt" |
29 | + "sync" |
30 | + |
31 | + "launchpad.net/juju-core/environs" |
32 | + "launchpad.net/juju-core/environs/config" |
33 | + "launchpad.net/juju-core/log" |
34 | + "launchpad.net/juju-core/schema" |
35 | + "launchpad.net/juju-core/state" |
36 | + "launchpad.net/juju-core/state/api" |
37 | +) |
38 | + |
39 | +const storagePort = 60006 |
40 | +const dataDir = "/var/lib/juju/local-data" |
41 | + |
42 | +// init registers the local provider. |
43 | +func init() { |
44 | + environs.RegisterProvider("local", environProvider{}) |
45 | +} |
46 | + |
47 | +var configChecker = schema.StrictFieldMap( |
48 | + schema.Fields{ |
49 | + "storage-port": schema.Int(), |
50 | + "data-dir": schema.String(), |
51 | + }, |
52 | + schema.Defaults{ |
53 | + "storage-port": storagePort, |
54 | + "data-dir": dataDir, |
55 | + }, |
56 | +) |
57 | + |
58 | +// environConfig extends the standard configuration by |
59 | +// the storage port and the data directory. |
60 | +type environConfig struct { |
61 | + *config.Config |
62 | + attrs map[string]interface{} |
63 | +} |
64 | + |
65 | +func (c *environConfig) storagePort() int { |
66 | + return c.attrs["storage-port"].(int) |
67 | +} |
68 | + |
69 | +func (c *environConfig) dataDir() string { |
70 | + return c.attrs["data-dir"].(string) |
71 | +} |
72 | + |
73 | +// environProvider represents the local provider environment |
74 | +// provider. |
75 | +type environProvider struct { |
76 | + mux sync.Mutex |
77 | +} |
78 | + |
79 | +func (p environProvider) newConfig(cfg *config.Config) (*environConfig, error) { |
80 | + valid, err := p.Validate(cfg, nil) |
81 | + if err != nil { |
82 | + return nil, err |
83 | + } |
84 | + return &environConfig{valid, valid.UnknownAttrs()}, nil |
85 | +} |
86 | + |
87 | +// Open checks if the environment exists and creates it otherwise. |
88 | +func (p environProvider) Open(cfg *config.Config) (environs.Environ, error) { |
89 | + p.mux.Lock() |
90 | + defer p.mux.Unlock() |
91 | + log.Printf("environs/local: opening environment %q", cfg.Name()) |
92 | + |
93 | + name := cfg.Name() |
94 | + ecfg, err := p.newConfig(cfg) |
95 | + if err != nil { |
96 | + return nil, err |
97 | + } |
98 | + env := &environ{ |
99 | + name: name, |
100 | + ecfgUnlocked: ecfg, |
101 | + storageUnlocked: newStorage(ecfg.storagePort(), name), |
102 | + } |
103 | + return env, nil |
104 | +} |
105 | + |
106 | +// Validate ensures that config is a valid configuration for the |
107 | +// local provider. |
108 | +func (p environProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) { |
109 | + v, err := configChecker.Coerce(cfg.UnknownAttrs(), nil) |
110 | + if err != nil { |
111 | + return nil, err |
112 | + } |
113 | + ecfg := &environConfig{cfg, v.(map[string]interface{})} |
114 | + if ecfg.storagePort() < 0x0000 || ecfg.storagePort() > 0xFFFF { |
115 | + return nil, fmt.Errorf("invalid local storage port number: %d", ecfg.storagePort()) |
116 | + } |
117 | + // TODO(mue) Check if data-dir exists, otherwise create. |
118 | + return nil, nil |
119 | +} |
120 | + |
121 | +// BoilerplateConfig returns a default configuration. |
122 | +func (p environProvider) BoilerplateConfig() string { |
123 | + return ` |
124 | +amazon: |
125 | + type: local |
126 | + admin-secret: {{rand}} |
127 | + storage-port: 60006 |
128 | + # data-dir: /var/lib/juju/local-data |
129 | +`[1:] |
130 | +} |
131 | + |
132 | +// SecretAttrs returns nothing for the local environment. |
133 | +func (p environProvider) SecretAttrs(cfg *config.Config) (map[string]interface{}, error) { |
134 | + return nil, nil |
135 | +} |
136 | + |
137 | +// PublicAddress returns an error, because the local environment |
138 | +// does not provide a public address. |
139 | +func (p environProvider) PublicAddress() (string, error) { |
140 | + return "", fmt.Errorf("no public address for local environment") |
141 | +} |
142 | + |
143 | +// PrivateAddress returns this machine's private host name. |
144 | +func (p environProvider) PrivateAddress() (string, error) { |
145 | + panic("not yet implemented") |
146 | +} |
147 | + |
148 | +// environ represents a local environment. |
149 | +type environ struct { |
150 | + name string |
151 | + mux sync.Mutex |
152 | + ecfgUnlocked *environConfig |
153 | + storageUnlocked *storage |
154 | +} |
155 | + |
156 | +// Name returns the Environ's name. |
157 | +func (e *environ) Name() string { |
158 | + return e.name |
159 | +} |
160 | + |
161 | +// Bootstrap ... |
162 | +func (e *environ) Bootstrap(uploadTools bool, stateServerCert, stateServerKey []byte) error { |
163 | + panic("not yet implemented") |
164 | +} |
165 | + |
166 | +// StateInfo ... |
167 | +func (e *environ) StateInfo() (*state.Info, *api.Info, error) { |
168 | + panic("not yet implemented") |
169 | +} |
170 | + |
171 | +// Config returns the configuration of the environment. |
172 | +func (e *environ) Config() *config.Config { |
173 | + return e.ecfg().Config |
174 | +} |
175 | + |
176 | +// SetConfig allows no change of the configuration in the local environ. |
177 | +func (e *environ) SetConfig(cfg *config.Config) error { |
178 | + return fmt.Errorf("change of configuration is not allowed") |
179 | +} |
180 | + |
181 | +// StartInstance returns an error as the only instance running |
182 | +// is the LXC host. |
183 | +func (e *environ) StartInstance(machineId string, info *state.Info, |
184 | + apiInfo *api.Info, tools *state.Tools) (environs.Instance, error) { |
185 | + return nil, fmt.Errorf("LXC host is already running") |
186 | +} |
187 | + |
188 | +// StopInstances returns an error as the LXC host cannot be stopped. |
189 | +func (e *environ) StopInstances([]environs.Instance) error { |
190 | + return fmt.Errorf("LXC host cannot be stopped") |
191 | +} |
192 | + |
193 | +// Instances returns the one instance representing the host system. |
194 | +func (e *environ) Instances(ids []state.InstanceId) ([]environs.Instance, error) { |
195 | + panic("not yet implemented") |
196 | +} |
197 | + |
198 | +// AllInstances returns the one instance representing the host system. |
199 | +func (e *environ) AllInstances() ([]environs.Instance, error) { |
200 | + panic("not yet implemented") |
201 | +} |
202 | + |
203 | +// Storage returns access to the local storage of the |
204 | +// environment. |
205 | +func (e *environ) Storage() environs.Storage { |
206 | + e.mux.Lock() |
207 | + storage := e.storageUnlocked |
208 | + e.mux.Unlock() |
209 | + return storage |
210 | +} |
211 | + |
212 | +// PublicStorage returns an empty storage as it isn't used |
213 | +// in the local environment. |
214 | +func (e *environ) PublicStorage() environs.StorageReader { |
215 | + return environs.EmptyStorage |
216 | +} |
217 | + |
218 | +// Destroy cleans the complete environment. |
219 | +func (e *environ) Destroy(insts []environs.Instance) error { |
220 | + panic("not yet implemented") |
221 | +} |
222 | + |
223 | +// AssignmentPolicy returns the local assignment policy. |
224 | +func (e *environ) AssignmentPolicy() state.AssignmentPolicy { |
225 | + return state.AssignLocal |
226 | +} |
227 | + |
228 | +// OpenPorts returns an error, there are no ports in |
229 | +// local environment. |
230 | +func (e *environ) OpenPorts(ports []state.Port) error { |
231 | + return fmt.Errorf("cannot open ports %v in local environment", ports) |
232 | +} |
233 | + |
234 | +// ClosePorts returns an error, there are no ports in |
235 | +// local environment. |
236 | +func (e *environ) ClosePorts(ports []state.Port) error { |
237 | + return fmt.Errorf("cannot close ports %v in local environment", ports) |
238 | +} |
239 | + |
240 | +// Ports returns an error, there are no ports in |
241 | +// local environment. |
242 | +func (e *environ) Ports() ([]state.Port, error) { |
243 | + return nil, fmt.Errorf("no ports in local environment") |
244 | +} |
245 | + |
246 | +// Provider ... |
247 | +func (e *environ) Provider() environs.EnvironProvider { |
248 | + panic("not yet implemented") |
249 | +} |
250 | + |
251 | +func (e *environ) ecfg() *environConfig { |
252 | + e.mux.Lock() |
253 | + ecfg := e.ecfgUnlocked |
254 | + e.mux.Unlock() |
255 | + return ecfg |
256 | +} |
257 | |
258 | === added file 'environs/local/local_test.go' |
259 | --- environs/local/local_test.go 1970-01-01 00:00:00 +0000 |
260 | +++ environs/local/local_test.go 2013-02-14 18:53:23 +0000 |
261 | @@ -0,0 +1,231 @@ |
262 | +package local_test |
263 | + |
264 | +import ( |
265 | + "bytes" |
266 | + "io/ioutil" |
267 | + . "launchpad.net/gocheck" |
268 | + "net" |
269 | + "path/filepath" |
270 | + "testing" |
271 | + |
272 | + "launchpad.net/juju-core/environs" |
273 | + "launchpad.net/juju-core/environs/local" |
274 | +) |
275 | + |
276 | +func TestLocal(t *testing.T) { |
277 | + TestingT(t) |
278 | +} |
279 | + |
280 | +type storageSuite struct { |
281 | + listener net.Listener |
282 | + storage environs.Storage |
283 | +} |
284 | + |
285 | +var _ = Suite(&storageSuite{}) |
286 | + |
287 | +func (s *storageSuite) SetUpSuite(c *C) { |
288 | + var err error |
289 | + basepath := c.MkDir() |
290 | + environName := "test-environ" |
291 | + s.listener, err = local.Listen(basepath, environName, "127.0.0.1", 60006) |
292 | + c.Assert(err, IsNil) |
293 | + s.storage = local.NewStorage(60006, environName) |
294 | + |
295 | + createTestData(c, basepath) |
296 | +} |
297 | + |
298 | +func (s *storageSuite) TearDownSuite(c *C) { |
299 | + s.listener.Close() |
300 | +} |
301 | + |
302 | +type getTest struct { |
303 | + name string |
304 | + content string |
305 | + err string |
306 | +} |
307 | + |
308 | +var getTests = []getTest{ |
309 | + { |
310 | + name: "foo", |
311 | + content: "this is file 'foo'", |
312 | + }, |
313 | + { |
314 | + name: "bar", |
315 | + content: "this is file 'bar'", |
316 | + }, |
317 | + { |
318 | + name: "baz", |
319 | + content: "this is file 'baz'", |
320 | + }, |
321 | + { |
322 | + name: "yadda", |
323 | + content: "this is file 'yadda'", |
324 | + }, |
325 | + { |
326 | + name: "dummy", |
327 | + err: "404 Not Found", |
328 | + }, |
329 | + { |
330 | + name: "../dummy", |
331 | + err: "404 Not Found", |
332 | + }, |
333 | +} |
334 | + |
335 | +func (s *storageSuite) TestGet(c *C) { |
336 | + // Test retrieving a file from a storage. |
337 | + check := func(gt getTest) { |
338 | + r, err := s.storage.Get(gt.name) |
339 | + if gt.err != "" { |
340 | + c.Assert(err, ErrorMatches, gt.err) |
341 | + return |
342 | + } |
343 | + defer r.Close() |
344 | + c.Assert(err, IsNil) |
345 | + var buf bytes.Buffer |
346 | + _, err = buf.ReadFrom(r) |
347 | + c.Assert(err, IsNil) |
348 | + c.Assert(buf.String(), Equals, gt.content) |
349 | + } |
350 | + for _, gt := range getTests { |
351 | + check(gt) |
352 | + } |
353 | +} |
354 | + |
355 | +type listTest struct { |
356 | + prefix string |
357 | + found []string |
358 | + err string |
359 | +} |
360 | + |
361 | +var listTests = []listTest{ |
362 | + { |
363 | + prefix: "foo", |
364 | + found: []string{"foo"}, |
365 | + }, |
366 | + { |
367 | + prefix: "ba", |
368 | + found: []string{"bar", "baz"}, |
369 | + }, |
370 | + { |
371 | + prefix: "", |
372 | + found: []string{"bar", "baz", "foo", "yadda"}, |
373 | + }, |
374 | + { |
375 | + prefix: "zzz", |
376 | + found: []string{}, |
377 | + }, |
378 | + { |
379 | + prefix: "../", |
380 | + err: "404 Not Found", |
381 | + }, |
382 | +} |
383 | + |
384 | +func (s *storageSuite) TestList(c *C) { |
385 | + // Test listing file of a storage. |
386 | + check := func(lt listTest) { |
387 | + l, err := s.storage.List(lt.prefix) |
388 | + if lt.err != "" { |
389 | + c.Assert(err, ErrorMatches, lt.err) |
390 | + return |
391 | + } |
392 | + c.Assert(err, IsNil) |
393 | + c.Assert(l, DeepEquals, lt.found) |
394 | + } |
395 | + for _, lt := range listTests { |
396 | + check(lt) |
397 | + } |
398 | +} |
399 | + |
400 | +type putTest struct { |
401 | + name string |
402 | + content string |
403 | + err string |
404 | +} |
405 | + |
406 | +var putTests = []putTest{ |
407 | + { |
408 | + name: "porterhouse", |
409 | + content: "this is the sent file 'porterhouse'", |
410 | + }, |
411 | + { |
412 | + name: "porterhouse", |
413 | + content: "this is the second sent file 'porterhouse'", |
414 | + err: "409 Conflict", |
415 | + }, |
416 | + { |
417 | + name: "../no-way", |
418 | + err: "301 Moved Permanently", |
419 | + }, |
420 | + { |
421 | + name: "cambridge", |
422 | + content: "this is the sent file 'cambridge'", |
423 | + }, |
424 | +} |
425 | + |
426 | +func (s *storageSuite) TestPut(c *C) { |
427 | + // Test sending a file to the storage. |
428 | + check := func(pt putTest) { |
429 | + err := s.storage.Put(pt.name, bytes.NewBufferString(pt.content), 0) |
430 | + if pt.err != "" { |
431 | + c.Assert(err, ErrorMatches, pt.err) |
432 | + return |
433 | + } |
434 | + c.Assert(err, IsNil) |
435 | + r, err := s.storage.Get(pt.name) |
436 | + c.Assert(err, IsNil) |
437 | + defer r.Close() |
438 | + var buf bytes.Buffer |
439 | + _, err = buf.ReadFrom(r) |
440 | + c.Assert(err, IsNil) |
441 | + c.Assert(buf.String(), Equals, pt.content) |
442 | + } |
443 | + for _, pt := range putTests { |
444 | + check(pt) |
445 | + } |
446 | +} |
447 | + |
448 | +type removeTest struct { |
449 | + name string |
450 | + content string |
451 | + err string |
452 | +} |
453 | + |
454 | +var removeTests = []removeTest{ |
455 | + { |
456 | + name: "fox", |
457 | + content: "the quick brown fox yadda yadda", |
458 | + }, |
459 | + { |
460 | + name: "dog", |
461 | + }, |
462 | +} |
463 | + |
464 | +func (s *storageSuite) TestRemove(c *C) { |
465 | + // Test removing a file in the storage. |
466 | + check := func(rt removeTest) { |
467 | + if rt.content != "" { |
468 | + err := s.storage.Put(rt.name, bytes.NewBufferString(rt.content), 0) |
469 | + c.Assert(err, IsNil) |
470 | + } |
471 | + err := s.storage.Remove(rt.name) |
472 | + c.Assert(err, IsNil) |
473 | + } |
474 | + for _, rt := range removeTests { |
475 | + check(rt) |
476 | + } |
477 | +} |
478 | + |
479 | +func createTestData(c *C, basepath string) { |
480 | + writeData := func(dir, name, data string) { |
481 | + fn := filepath.Join(dir, name) |
482 | + err := ioutil.WriteFile(fn, []byte(data), 0644) |
483 | + c.Assert(err, IsNil) |
484 | + } |
485 | + |
486 | + dir := filepath.Join(basepath, "test-environ") |
487 | + |
488 | + writeData(dir, "foo", "this is file 'foo'") |
489 | + writeData(dir, "bar", "this is file 'bar'") |
490 | + writeData(dir, "baz", "this is file 'baz'") |
491 | + writeData(dir, "yadda", "this is file 'yadda'") |
492 | +} |
493 | |
494 | === removed file 'environs/local/lxc.go' |
495 | --- environs/local/lxc.go 2012-03-15 02:15:16 +0000 |
496 | +++ environs/local/lxc.go 1970-01-01 00:00:00 +0000 |
497 | @@ -1,64 +0,0 @@ |
498 | -package local |
499 | - |
500 | -import ( |
501 | - "bytes" |
502 | - "os/exec" |
503 | - "strings" |
504 | -) |
505 | - |
506 | -// container represents an LXC container with the given name. |
507 | -type container struct { |
508 | - Name string |
509 | -} |
510 | - |
511 | -// runLXCCommand runs an LXC command with the given arguments, |
512 | -// strip outing the usage message. |
513 | -func runLXCCommand(args ...string) ([]byte, error) { |
514 | - output, err := exec.Command(args[0], args...).CombinedOutput() |
515 | - if i := bytes.Index(bytes.ToLower(output), []byte("\nusage: ")); i > 0 { |
516 | - output = output[:i] |
517 | - } |
518 | - return output, err |
519 | -} |
520 | - |
521 | -// rootPath returns the LXC container root filesystem path. |
522 | -func (c *container) rootPath() string { |
523 | - return "/var/lib/lxc/" + c.Name + "/rootfs/" |
524 | -} |
525 | - |
526 | -// create creates the LXC container. |
527 | -func (c *container) create() ([]byte, error) { |
528 | - return runLXCCommand("sudo", "lxc-create", "-n", c.Name) |
529 | -} |
530 | - |
531 | -// start starts the LXC container. |
532 | -func (c *container) start() ([]byte, error) { |
533 | - return runLXCCommand("sudo", "lxc-start", "--daemon", "-n", c.Name) |
534 | -} |
535 | - |
536 | -// stop stops the LXC container. |
537 | -func (c *container) stop() ([]byte, error) { |
538 | - return runLXCCommand("sudo", "lxc-stop", "-n", c.Name) |
539 | -} |
540 | - |
541 | -// destroy destroys the LXC container. |
542 | -func (c *container) destroy() ([]byte, error) { |
543 | - return runLXCCommand("sudo", "lxc-destroy", "-n", c.Name) |
544 | -} |
545 | - |
546 | -// running returns true if the container name is in the |
547 | -// list of the containers that are running. |
548 | -func (c *container) running() bool { |
549 | - for _, containerName := range list() { |
550 | - if containerName == c.Name { |
551 | - return true |
552 | - } |
553 | - } |
554 | - return false |
555 | -} |
556 | - |
557 | -// list returns a list with the names of containers. |
558 | -func list() []string { |
559 | - output, _ := runLXCCommand("sudo", "lxc-ls") |
560 | - return strings.Fields(string(output)) |
561 | -} |
562 | |
563 | === removed file 'environs/local/lxc_test.go' |
564 | --- environs/local/lxc_test.go 2012-03-14 14:13:43 +0000 |
565 | +++ environs/local/lxc_test.go 1970-01-01 00:00:00 +0000 |
566 | @@ -1,81 +0,0 @@ |
567 | -package local |
568 | - |
569 | -import ( |
570 | - "flag" |
571 | - . "launchpad.net/gocheck" |
572 | - "testing" |
573 | -) |
574 | - |
575 | -const Defaultcontainer = "lxc_test" |
576 | - |
577 | -func Test(t *testing.T) { TestingT(t) } |
578 | - |
579 | -type S struct{} |
580 | - |
581 | -var _ = Suite(&S{}) |
582 | - |
583 | -var lxcEnabled = flag.Bool("lxc", false, "enable LXC tests that require sudo") |
584 | - |
585 | -func (s *S) SetUpSuite(c *C) { |
586 | - if !*lxcEnabled { |
587 | - c.Skip("lxc tests need sudo access (-lxc to enable)") |
588 | - } |
589 | -} |
590 | - |
591 | -func (s *S) TestCreate(c *C) { |
592 | - var container container |
593 | - container.Name = Defaultcontainer |
594 | - |
595 | - _, err := container.create() |
596 | - c.Assert(err, IsNil) |
597 | - |
598 | - c.Assert(container.running(), Equals, true) |
599 | - |
600 | - _, err = container.destroy() |
601 | - c.Assert(err, IsNil) |
602 | - |
603 | - c.Assert(container.running(), Equals, false) |
604 | -} |
605 | - |
606 | -func (s *S) TestStart(c *C) { |
607 | - var container container |
608 | - container.Name = Defaultcontainer |
609 | - |
610 | - _, err := container.create() |
611 | - c.Assert(err, IsNil) |
612 | - |
613 | - _, err = container.start() |
614 | - c.Assert(err, IsNil) |
615 | - |
616 | - _, err = container.stop() |
617 | - c.Assert(err, IsNil) |
618 | - |
619 | - _, err = container.destroy() |
620 | - c.Assert(err, IsNil) |
621 | -} |
622 | - |
623 | -func (s *S) TestIsRunningWhencontainerIsCreated(c *C) { |
624 | - var container container |
625 | - container.Name = Defaultcontainer |
626 | - |
627 | - _, err := container.create() |
628 | - c.Assert(err, IsNil) |
629 | - |
630 | - c.Assert(container.running(), Equals, true) |
631 | - |
632 | - _, err = container.destroy() |
633 | - c.Assert(err, IsNil) |
634 | -} |
635 | - |
636 | -func (s *S) TestIsNotRunningWhencontainerIsNotCreated(c *C) { |
637 | - var container container |
638 | - container.Name = Defaultcontainer |
639 | - |
640 | - c.Assert(container.running(), Equals, false) |
641 | -} |
642 | - |
643 | -func (s *S) TestRootPath(c *C) { |
644 | - var container container |
645 | - container.Name = Defaultcontainer |
646 | - c.Assert(container.rootPath(), Equals, "/var/lib/lxc/"+Defaultcontainer+"/rootfs/") |
647 | -} |
648 | |
649 | === removed file 'environs/local/network.go' |
650 | --- environs/local/network.go 2012-05-24 17:10:58 +0000 |
651 | +++ environs/local/network.go 1970-01-01 00:00:00 +0000 |
652 | @@ -1,148 +0,0 @@ |
653 | -package local |
654 | - |
655 | -import ( |
656 | - "encoding/xml" |
657 | - "io/ioutil" |
658 | - "os" |
659 | - "os/exec" |
660 | - "strings" |
661 | - "text/template" |
662 | -) |
663 | - |
664 | -// network represents a local virtual network. |
665 | -type network struct { |
666 | - XMLName xml.Name `xml:"network"` |
667 | - Name string `xml:"name"` |
668 | - Bridge bridge `xml:"bridge"` |
669 | - Ip ip `xml:"ip"` |
670 | - Subnet int |
671 | -} |
672 | - |
673 | -// ip represents an ip with the given address and netmask. |
674 | -type ip struct { |
675 | - Ip string `xml:"address,attr"` |
676 | - Mask string `xml:"netmask,attr"` |
677 | -} |
678 | - |
679 | -// bridge represents a briddge with the given name. |
680 | -type bridge struct { |
681 | - Name string `xml:"name,attr"` |
682 | -} |
683 | - |
684 | -// newNetwork returns a started network. |
685 | -func newNetwork(name string, subnet int) (*network, error) { |
686 | - n := network{Name: name, Subnet: subnet} |
687 | - err := n.start() |
688 | - if err != nil { |
689 | - return nil, err |
690 | - } |
691 | - err = n.loadAttributes() |
692 | - if err != nil { |
693 | - return nil, err |
694 | - } |
695 | - return &n, nil |
696 | -} |
697 | - |
698 | -// loadAttributes loads the attributes for a network. |
699 | -func (n *network) loadAttributes() error { |
700 | - output, err := exec.Command("virsh", "net-dumpxml", n.Name).Output() |
701 | - if err != nil { |
702 | - return err |
703 | - } |
704 | - return xml.Unmarshal(output, &n) |
705 | -} |
706 | - |
707 | -// running returns true if network name is in the |
708 | -// list of networks and is active. |
709 | -func (n *network) running() (bool, error) { |
710 | - networks, err := listNetworks() |
711 | - if err != nil { |
712 | - return false, err |
713 | - } |
714 | - return networks[n.Name], nil |
715 | -} |
716 | - |
717 | -// exists returns true if network name is in the |
718 | -// list of networks. |
719 | -func (n *network) exists() (bool, error) { |
720 | - networks, err := listNetworks() |
721 | - if err != nil { |
722 | - return false, err |
723 | - } |
724 | - _, exists := networks[n.Name] |
725 | - return exists, nil |
726 | -} |
727 | - |
728 | -// virsh replaces %d with an auto increment |
729 | -// number to make the bridge name unique |
730 | -var libVirtNetworkTemplate = template.Must(template.New("").Parse(` |
731 | -<network> |
732 | - <name>{{.Name}}</name> |
733 | - <bridge name='vbr-{{.Name}}-%d' /> |
734 | - <forward/> |
735 | - <ip address='192.168.{{.Subnet}}.1' netmask='255.255.255.0'> |
736 | - <dhcp> |
737 | - <range start='192.168.{{.Subnet}}.2' end='192.168.{{.Subnet}}.254' /> |
738 | - </dhcp> |
739 | - </ip> |
740 | -</network> |
741 | -`)) |
742 | - |
743 | -// start ensures that the network is started. |
744 | -// If the network name does not exist, it is created. |
745 | -func (n *network) start() error { |
746 | - exists, err := n.exists() |
747 | - if err != nil { |
748 | - return err |
749 | - } |
750 | - if exists { |
751 | - running, err := n.running() |
752 | - if err != nil { |
753 | - return err |
754 | - } |
755 | - if running { |
756 | - return nil |
757 | - } |
758 | - return exec.Command("virsh", "net-start", n.Name).Run() |
759 | - } |
760 | - file, err := ioutil.TempFile(os.TempDir(), "network") |
761 | - if err != nil { |
762 | - return err |
763 | - } |
764 | - defer file.Close() |
765 | - defer os.Remove(file.Name()) |
766 | - err = libVirtNetworkTemplate.Execute(file, n) |
767 | - if err != nil { |
768 | - return err |
769 | - } |
770 | - err = exec.Command("virsh", "net-define", file.Name()).Run() |
771 | - if err != nil { |
772 | - return err |
773 | - } |
774 | - return exec.Command("virsh", "net-start", n.Name).Run() |
775 | -} |
776 | - |
777 | -// listNetworks returns a map from network name to active status. |
778 | -func listNetworks() (map[string]bool, error) { |
779 | - output, err := exec.Command("virsh", "net-list", "--all").Output() |
780 | - if err != nil { |
781 | - return nil, err |
782 | - } |
783 | - // Remove the header. |
784 | - networks := map[string]bool{} |
785 | - lines := strings.Split(string(output), "\n") |
786 | - if len(lines) < 3 { |
787 | - return networks, nil |
788 | - } |
789 | - lines = lines[2:] |
790 | - for _, line := range lines { |
791 | - if line == "" { |
792 | - continue |
793 | - } |
794 | - fields := strings.Fields(line) |
795 | - if len(fields) > 2 { |
796 | - networks[fields[0]] = fields[1] == "active" |
797 | - } |
798 | - } |
799 | - return networks, nil |
800 | -} |
801 | |
802 | === removed file 'environs/local/network_test.go' |
803 | --- environs/local/network_test.go 2012-05-24 17:10:58 +0000 |
804 | +++ environs/local/network_test.go 1970-01-01 00:00:00 +0000 |
805 | @@ -1,144 +0,0 @@ |
806 | -package local |
807 | - |
808 | -import ( |
809 | - . "launchpad.net/gocheck" |
810 | - "os" |
811 | -) |
812 | - |
813 | -var scriptText = `#!/bin/bash |
814 | -#!/bin/bash |
815 | - |
816 | -case "$1" in |
817 | -net-list) |
818 | -cat <<END |
819 | -Name State Autostart |
820 | ------------------------------------------ |
821 | -default active yes |
822 | -juju-test active yes |
823 | -foobar inactive no |
824 | -END |
825 | -;; |
826 | - |
827 | -net-dumpxml) |
828 | -cat <<END |
829 | -<network> |
830 | -<name>default</name> |
831 | -<uuid>7f5d45e4-2fa2-f713-0229-fb1fea419e3b</uuid> |
832 | -<forward mode='nat'/> |
833 | -<bridge name='virbr0' stp='on' delay='0' /> |
834 | -<ip address='192.168.122.1' netmask='255.255.255.0'> |
835 | -<dhcp> |
836 | -<range start='192.168.122.2' end='192.168.122.254' /> |
837 | -</dhcp> |
838 | -</ip> |
839 | -</network> |
840 | -END |
841 | -;; |
842 | - |
843 | -net-start) |
844 | -echo "net-start" |
845 | -;; |
846 | - |
847 | -net-define) |
848 | -echo "net-define" |
849 | -;; |
850 | - |
851 | -esac |
852 | -` |
853 | - |
854 | -type networkSuite struct { |
855 | - oldPath string |
856 | -} |
857 | - |
858 | -var _ = Suite(&networkSuite{}) |
859 | - |
860 | -func (s *networkSuite) SetUpSuite(c *C) { |
861 | - s.oldPath = os.Getenv("PATH") |
862 | - dir := c.MkDir() |
863 | - os.Setenv("PATH", dir+":"+s.oldPath) |
864 | - writeScript(c, dir) |
865 | -} |
866 | - |
867 | -func (s *networkSuite) TearDownSuite(c *C) { |
868 | - os.Setenv("PATH", s.oldPath) |
869 | -} |
870 | - |
871 | -func (s *networkSuite) TestStartNetwork(c *C) { |
872 | - // start a network that already exists |
873 | - n := network{Name: "default"} |
874 | - err := n.start() |
875 | - c.Assert(err, IsNil) |
876 | - |
877 | - // start a new network |
878 | - n = network{Name: "newnet"} |
879 | - err = n.start() |
880 | - c.Assert(err, IsNil) |
881 | -} |
882 | - |
883 | -func (s *networkSuite) TestNewNetwork(c *C) { |
884 | - _, err := newNetwork("name", 133) |
885 | - c.Assert(err, IsNil) |
886 | -} |
887 | - |
888 | -func (s *networkSuite) TestLoadAttributes(c *C) { |
889 | - n := network{Name: "default"} |
890 | - err := n.loadAttributes() |
891 | - c.Assert(err, IsNil) |
892 | - c.Assert(n.Name, Equals, "default") |
893 | - c.Assert(n.Bridge.Name, Equals, "virbr0") |
894 | - c.Assert(n.Ip.Ip, Equals, "192.168.122.1") |
895 | - c.Assert(n.Ip.Mask, Equals, "255.255.255.0") |
896 | -} |
897 | - |
898 | -func (s *networkSuite) TestRunning(c *C) { |
899 | - n := network{Name: "default"} |
900 | - running, err := n.running() |
901 | - c.Assert(err, IsNil) |
902 | - c.Assert(running, Equals, true) |
903 | - |
904 | - n = network{Name: "foobar"} |
905 | - running, err = n.running() |
906 | - c.Assert(err, IsNil) |
907 | - c.Assert(running, Equals, false) |
908 | - |
909 | - n = network{Name: "fakeName"} |
910 | - running, err = n.running() |
911 | - c.Assert(err, IsNil) |
912 | - c.Assert(running, Equals, false) |
913 | -} |
914 | - |
915 | -func (s *networkSuite) TestNetworkExists(c *C) { |
916 | - n := network{Name: "default"} |
917 | - exists, err := n.exists() |
918 | - c.Assert(err, IsNil) |
919 | - c.Assert(exists, Equals, true) |
920 | - |
921 | - n = network{Name: "foobar"} |
922 | - exists, err = n.exists() |
923 | - c.Assert(err, IsNil) |
924 | - c.Assert(exists, Equals, true) |
925 | - |
926 | - n = network{Name: "fakeName"} |
927 | - exists, err = n.exists() |
928 | - c.Assert(err, IsNil) |
929 | - c.Assert(exists, Equals, false) |
930 | -} |
931 | - |
932 | -func (s *networkSuite) TestListNetworks(c *C) { |
933 | - expected := map[string]bool{ |
934 | - "juju-test": true, |
935 | - "foobar": false, |
936 | - "default": true, |
937 | - } |
938 | - networks, err := listNetworks() |
939 | - c.Assert(err, IsNil) |
940 | - c.Assert(networks, DeepEquals, expected) |
941 | -} |
942 | - |
943 | -func writeScript(c *C, dir string) { |
944 | - f, err := os.OpenFile(dir+"/virsh", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777) |
945 | - c.Assert(err, IsNil) |
946 | - defer f.Close() |
947 | - _, err = f.WriteString(scriptText) |
948 | - c.Assert(err, IsNil) |
949 | -} |
950 | |
951 | === added file 'environs/local/storage.go' |
952 | --- environs/local/storage.go 1970-01-01 00:00:00 +0000 |
953 | +++ environs/local/storage.go 2013-02-14 18:53:23 +0000 |
954 | @@ -0,0 +1,260 @@ |
955 | +package local |
956 | + |
957 | +import ( |
958 | + "fmt" |
959 | + "io" |
960 | + "io/ioutil" |
961 | + "net" |
962 | + "net/http" |
963 | + "os" |
964 | + "path/filepath" |
965 | + "sort" |
966 | + "strings" |
967 | + "syscall" |
968 | +) |
969 | + |
970 | +// storageBackend provides HTTP access to a defined path. |
971 | +type storageBackend struct { |
972 | + environName string |
973 | + uri string |
974 | + path string |
975 | +} |
976 | + |
977 | +// newContainer returns a new container using the given path. |
978 | +func newStorageBackend(basepath, environName string) *storageBackend { |
979 | + sb := &storageBackend{ |
980 | + environName: environName, |
981 | + uri: fmt.Sprintf("/%s/", environName), |
982 | + path: filepath.Join(basepath, environName), |
983 | + } |
984 | + return sb |
985 | +} |
986 | + |
987 | +// ServeHTTP handles the HTTP requests to the container. |
988 | +func (s *storageBackend) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
989 | + switch req.Method { |
990 | + case "GET": |
991 | + if strings.HasSuffix(req.URL.Path, "*") { |
992 | + s.handleList(w, req) |
993 | + } else { |
994 | + s.handleGet(w, req) |
995 | + } |
996 | + case "POST": |
997 | + s.handlePost(w, req) |
998 | + case "DELETE": |
999 | + s.handleDelete(w, req) |
1000 | + default: |
1001 | + http.Error(w, "method "+req.Method+" is not supported", http.StatusMethodNotAllowed) |
1002 | + } |
1003 | +} |
1004 | + |
1005 | +// handleGet returns a storage file to the client. |
1006 | +func (s *storageBackend) handleGet(w http.ResponseWriter, req *http.Request) { |
1007 | + data, err := ioutil.ReadFile(filepath.Join(s.path, req.URL.Path)) |
1008 | + if err != nil { |
1009 | + if isNotExist(err) { |
1010 | + http.Error(w, fmt.Sprintf("404 '%s' Not Found", req.URL.Path), http.StatusNotFound) |
1011 | + return |
1012 | + } |
1013 | + http.Error(w, fmt.Sprintf("500 %v", err), http.StatusInternalServerError) |
1014 | + return |
1015 | + } |
1016 | + w.Header().Set("Content-Type", "application/octet-stream") |
1017 | + w.Write(data) |
1018 | +} |
1019 | + |
1020 | +// handleList returns the names in the storage to the client. |
1021 | +func (s *storageBackend) handleList(w http.ResponseWriter, req *http.Request) { |
1022 | + fis, err := ioutil.ReadDir(s.path) |
1023 | + if err != nil { |
1024 | + if isNotExist(err) { |
1025 | + http.Error(w, fmt.Sprintf("404 /%s not found", req.URL.Path), http.StatusNotFound) |
1026 | + return |
1027 | + } |
1028 | + http.Error(w, fmt.Sprintf("500 %v", err), http.StatusInternalServerError) |
1029 | + return |
1030 | + } |
1031 | + prefix := req.URL.Path[:len(req.URL.Path)-1] |
1032 | + names := []string{} |
1033 | + for _, fi := range fis { |
1034 | + name := fi.Name() |
1035 | + if strings.HasPrefix(name, prefix) { |
1036 | + names = append(names, name) |
1037 | + } |
1038 | + } |
1039 | + data := []byte(strings.Join(names, "\n")) |
1040 | + w.Header().Set("Content-Type", "application/octet-stream") |
1041 | + w.Write(data) |
1042 | +} |
1043 | + |
1044 | +// handlePost stores data from the client in the storage. |
1045 | +func (s *storageBackend) handlePost(w http.ResponseWriter, req *http.Request) { |
1046 | + fp := filepath.Join(s.path, req.URL.Path) |
1047 | + _, err := os.Stat(fp) |
1048 | + if err == nil { |
1049 | + http.Error(w, fmt.Sprintf("409 '%s' Already Exist", req.URL.Path), http.StatusConflict) |
1050 | + return |
1051 | + } else if !isNotExist(err) { |
1052 | + http.Error(w, fmt.Sprintf("500 %v", err), http.StatusInternalServerError) |
1053 | + return |
1054 | + } |
1055 | + body, err := ioutil.ReadAll(req.Body) |
1056 | + if err != nil { |
1057 | + http.Error(w, fmt.Sprintf("500 %c", err), http.StatusInternalServerError) |
1058 | + return |
1059 | + } |
1060 | + err = ioutil.WriteFile(fp, body, 0644) |
1061 | + if err != nil { |
1062 | + http.Error(w, fmt.Sprintf("500 %v", err), http.StatusInternalServerError) |
1063 | + return |
1064 | + } |
1065 | + w.WriteHeader(http.StatusOK) |
1066 | +} |
1067 | + |
1068 | +// handleDelete removes data from the storage. |
1069 | +func (s *storageBackend) handleDelete(w http.ResponseWriter, req *http.Request) { |
1070 | + fp := filepath.Join(s.path, req.URL.Path) |
1071 | + err := os.Remove(fp) |
1072 | + if err != nil { |
1073 | + if !isNotExist(err) { |
1074 | + http.Error(w, fmt.Sprintf("500 %v", err), http.StatusInternalServerError) |
1075 | + return |
1076 | + } |
1077 | + } |
1078 | + w.WriteHeader(http.StatusOK) |
1079 | +} |
1080 | + |
1081 | +// listen starts a network listener listening for HTTP requests |
1082 | +// to get, put, list and remove the files in the state's storage. |
1083 | +func listen(basepath, environName, ip string, port int) (net.Listener, error) { |
1084 | + backend := newStorageBackend(basepath, environName) |
1085 | + if err := os.MkdirAll(backend.path, 0755); err != nil { |
1086 | + return nil, err |
1087 | + } |
1088 | + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port)) |
1089 | + if err != nil { |
1090 | + panic(fmt.Errorf("cannot start listener: %v", err)) |
1091 | + } |
1092 | + mux := http.NewServeMux() |
1093 | + mux.Handle(backend.uri, http.StripPrefix(backend.uri, backend)) |
1094 | + |
1095 | + go http.Serve(listener, mux) |
1096 | + |
1097 | + return listener, nil |
1098 | +} |
1099 | + |
1100 | +// storage implements the Storage interface. |
1101 | +type storage struct { |
1102 | + port int |
1103 | + environName string |
1104 | +} |
1105 | + |
1106 | +// newStorage returns a new local storage. |
1107 | +func newStorage(port int, environName string) *storage { |
1108 | + return &storage{ |
1109 | + port: port, |
1110 | + environName: environName, |
1111 | + } |
1112 | +} |
1113 | + |
1114 | +// Get opens the given storage file and returns a ReadCloser |
1115 | +// that can be used to read its contents. It is the caller's |
1116 | +// responsibility to close it after use. If the name does not |
1117 | +// exist, it should return a *NotFoundError. |
1118 | +func (s *storage) Get(name string) (io.ReadCloser, error) { |
1119 | + url, err := s.URL(name) |
1120 | + if err != nil { |
1121 | + return nil, err |
1122 | + } |
1123 | + resp, err := http.Get(url) |
1124 | + if err != nil { |
1125 | + return nil, err |
1126 | + } |
1127 | + if resp.StatusCode < 200 || resp.StatusCode > 299 { |
1128 | + return nil, fmt.Errorf(resp.Status) |
1129 | + } |
1130 | + return resp.Body, nil |
1131 | +} |
1132 | + |
1133 | +// List lists all names in the storage with the given prefix, in |
1134 | +// alphabetical order. The names in the storage are considered |
1135 | +// to be in a flat namespace, so the prefix may include slashes |
1136 | +// and the names returned are the full names for the matching |
1137 | +// entries. |
1138 | +func (s *storage) List(prefix string) ([]string, error) { |
1139 | + url, err := s.URL(prefix) |
1140 | + if err != nil { |
1141 | + return nil, err |
1142 | + } |
1143 | + resp, err := http.Get(url + "*") |
1144 | + if err != nil { |
1145 | + return nil, err |
1146 | + } |
1147 | + if resp.StatusCode < 200 || resp.StatusCode > 299 { |
1148 | + return nil, fmt.Errorf(resp.Status) |
1149 | + } |
1150 | + defer resp.Body.Close() |
1151 | + body, err := ioutil.ReadAll(resp.Body) |
1152 | + if err != nil { |
1153 | + return nil, err |
1154 | + } |
1155 | + if string(body) == "" { |
1156 | + return []string{}, nil |
1157 | + } |
1158 | + names := strings.Split(string(body), "\n") |
1159 | + sort.Strings(names) |
1160 | + return names, nil |
1161 | +} |
1162 | + |
1163 | +// URL returns a URL that can be used to access the given storage file. |
1164 | +func (s *storage) URL(name string) (string, error) { |
1165 | + return fmt.Sprintf("http://localhost:%d/%s/%s", s.port, s.environName, name), nil |
1166 | +} |
1167 | + |
1168 | +// Put reads from r and writes to the given storage file. |
1169 | +// The length must give the total length of the file. |
1170 | +func (s *storage) Put(name string, r io.Reader, length int64) error { |
1171 | + url, err := s.URL(name) |
1172 | + if err != nil { |
1173 | + return err |
1174 | + } |
1175 | + resp, err := http.Post(url, "application/octet-stream", r) |
1176 | + if err != nil { |
1177 | + return err |
1178 | + } |
1179 | + if resp.StatusCode < 200 || resp.StatusCode > 299 { |
1180 | + return fmt.Errorf(resp.Status) |
1181 | + } |
1182 | + return nil |
1183 | +} |
1184 | + |
1185 | +// Remove removes the given file from the environment's |
1186 | +// storage. It should not return an error if the file does |
1187 | +// not exist. |
1188 | +func (s *storage) Remove(name string) error { |
1189 | + url, err := s.URL(name) |
1190 | + if err != nil { |
1191 | + return err |
1192 | + } |
1193 | + req, err := http.NewRequest("DELETE", url, nil) |
1194 | + if err != nil { |
1195 | + return err |
1196 | + } |
1197 | + resp, err := http.DefaultClient.Do(req) |
1198 | + if err != nil { |
1199 | + return err |
1200 | + } |
1201 | + if resp.StatusCode < 200 || resp.StatusCode > 299 { |
1202 | + return fmt.Errorf(resp.Status) |
1203 | + } |
1204 | + return nil |
1205 | +} |
1206 | + |
1207 | +// isNotExist checks if a returned error shows that a file |
1208 | +// does not exist. |
1209 | +func isNotExist(err error) bool { |
1210 | + if err, ok := err.(*os.PathError); ok && (os.IsNotExist(err.Err) || err.Err == syscall.ENOTDIR) { |
1211 | + return true |
1212 | + } |
1213 | + return false |
1214 | +} |
A couple of comments - let me know your thoughts
https:/ /codereview. appspot. com/7323059/ diff/1/ environs/ local/config. go local/config. go (right):
File environs/
https:/ /codereview. appspot. com/7323059/ diff/1/ environs/ local/config. go#newcode10 local/config. go:10: "port": schema.Int(),
environs/
storage-port, maybe? I have a suspicion we'll be needing an api-port too
(and a state-port in the meantime)... someone correct me if I'm crazy.
(hmm, should they actually be part of the base Config? I think they
might...)
https:/ /codereview. appspot. com/7323059/ diff/1/ environs/ local/local. go local/local. go (right):
File environs/
https:/ /codereview. appspot. com/7323059/ diff/1/ environs/ local/local. go#newcode73 local/local. go:73: // TODO add arguments to specify type of new
environs/
machine.
Consider writing more relevant comments for all the methods that we know
will act differently -- I'm not sure it's helpful to duplicate the
interface comments here when their accuracy/relevance is ...variable ;).
https:/ /codereview. appspot. com/7323059/