Merge lp:~themue/juju-core/009-local-environ-storage into lp:~juju/juju-core/trunk

Proposed by Frank Mueller
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
Reviewer Review Type Date Requested Status
The Go Language Gophers Pending
Review via email: mp+148183@code.launchpad.net

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>/<environ-name>/private and
<basedir>/<environ-name>/public to store files. Additionally
some clean-up and first implementation steps of the
interfaces.

https://codereview.appspot.com/7323059/

To post a comment you must log in.
889. By Frank Mueller

local: changed to use empty public storage

Revision history for this message
William Reade (fwereade) wrote :

A couple of comments - let me know your thoughts

https://codereview.appspot.com/7323059/diff/1/environs/local/config.go
File environs/local/config.go (right):

https://codereview.appspot.com/7323059/diff/1/environs/local/config.go#newcode10
environs/local/config.go:10: "port": schema.Int(),
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
File environs/local/local.go (right):

https://codereview.appspot.com/7323059/diff/1/environs/local/local.go#newcode73
environs/local/local.go:73: // TODO add arguments to specify type of new
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/

890. By Frank Mueller

config: changes after review

891. By Frank Mueller

environ: merging, consolidation, comment changing

Revision history for this message
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://codereview.appspot.com/7323059/diff/3013/environs/local/local.go
File environs/local/local.go (right):

https://codereview.appspot.com/7323059/diff/3013/environs/local/local.go#newcode100
environs/local/local.go:100: amazon:
Not amazon, please.

https://codereview.appspot.com/7323059/diff/3013/environs/local/local.go#newcode169
environs/local/local.go:169: // Instances returns the one instance
representing the host system.
s/./, if requested./ ?

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go
File environs/local/storage.go (right):

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode17
environs/local/storage.go:17: type storageBackend struct {
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://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode23
environs/local/storage.go:23: // newContainer returns a new container
using the given path.
s/newContainer/newStorageBackend/

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode173
environs/local/storage.go:173: if resp.StatusCode < 200 ||
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://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode193
environs/local/storage.go:193: if resp.StatusCode < 200 ||
resp.StatusCode > 299 {
As above.

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode211
environs/local/storage.go:211: return
fmt.Sprintf("http://localhost:%d/%s/%s", s.port, s.environName, name),
nil
This needs to work from inside containers.

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode225
environs/local/storage.go:225: if resp.StatusCode < 200 ||
resp.StatusCode > 299 {
As above.

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode247
environs/local/storage.go:247: if resp.StatusCode < 200 ||
resp.StatusCode > 299 {
As above.

https://codereview.appspot.com/7323059/

Revision history for this message
Roger Peppe (rogpeppe) wrote :
Download full text (3.3 KiB)

some comments; there might be more

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go
File environs/local/storage.go (right):

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode70
environs/local/storage.go:70: if isNotExist(err) {
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.StatusNotFound)

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode93
environs/local/storage.go:93: _, err := os.Stat(fp)
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://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode101
environs/local/storage.go:101: body, err := ioutil.ReadAll(req.Body)
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://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode119
environs/local/storage.go:119: if !isNotExist(err) {
if err := os.Remove(fp); err != nil && !os.IsNotExist(err) {

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode128
environs/local/storage.go:128: // to get, put, list and remove the files
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://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode130
environs/local/storage.go:130: backend := newStorageBackend(basepath,
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://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode131
environs/local/storage.go:131: if err := os.MkdirAll(backend.path,
0755); err != nil {
i wonder we should use 0777 here and let the caller's umask deal with
desired permissions.

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode136
environs/local/storage.go:136: panic(fmt.Errorf("cannot start listener:
%v", err))
return nil, err
(it's not an unlikely error if the port is not zero)

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode146
environs/local/storage.go:146: // storage implements the Storage
interface.
s/Storage/environs.Storage/
?

https://codereview.appspot.com/7323059/diff/3013/environs/local/storage.go#newcode174
environs/local/storage.go:174: return nil, fmt.Errorf(resp.Status)
s/fmt.Errorf/errors.NewError/

https://codereview.appsp...

Read more...

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 AddUnitSubordinateTo

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+}

Subscribers

People subscribed via source and target branches