Merge lp:~maxim-perenesenko/juju-core/cloudsigma into lp:~go-bot/juju-core/trunk

Proposed by Maxim Perenesenko
Status: Needs review
Proposed branch: lp:~maxim-perenesenko/juju-core/cloudsigma
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 3777 lines (+3644/-1)
23 files modified
cmd/jujud/machine.go (+3/-1)
dependencies.tsv (+1/-0)
provider/all/all.go (+1/-0)
provider/cloudsigma/client.go (+307/-0)
provider/cloudsigma/client_test.go (+316/-0)
provider/cloudsigma/config.go (+165/-0)
provider/cloudsigma/config_test.go (+329/-0)
provider/cloudsigma/constraints.go (+95/-0)
provider/cloudsigma/constraints_test.go (+136/-0)
provider/cloudsigma/environ.go (+164/-0)
provider/cloudsigma/environ_test.go (+96/-0)
provider/cloudsigma/environcaps.go (+50/-0)
provider/cloudsigma/environfirewall.go (+32/-0)
provider/cloudsigma/environinstance.go (+291/-0)
provider/cloudsigma/environinstance_test.go (+206/-0)
provider/cloudsigma/instance.go (+148/-0)
provider/cloudsigma/instance_test.go (+328/-0)
provider/cloudsigma/provider.go (+140/-0)
provider/cloudsigma/provider_test.go (+94/-0)
provider/cloudsigma/storage.go (+252/-0)
provider/cloudsigma/storage_test.go (+373/-0)
provider/cloudsigma/storageconfig.go (+59/-0)
provider/cloudsigma/storageconfig_test.go (+58/-0)
To merge this branch: bzr merge lp:~maxim-perenesenko/juju-core/cloudsigma
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+221540@code.launchpad.net

Description of the change

Added provider for CloudSigma cloud.

Provider for CloudSigma (https://www.cloudsigma.com) uses CloudSigma API v2.0
(https://cloudsigma-docs.readthedocs.org/en/2.10/).

Minimal client for communicating with the CloudSigma Web API from Go program was implemented as
separate library github.com/Altoros/gosigma. This library is published under AGPLv3 license.

gosigma library is added as dependency to dependencies.tsv file.

https://codereview.appspot.com/101970045/

To post a comment you must log in.
Revision history for this message
William Reade (fwereade) wrote :

I made a *very* few first-pass comments, but it's really going to be
hard to review all this in one go. Would you please break it up a
little? I'd recommend starting with the config type first and leaving
the Environ implementation out for a while...

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config.go
File provider/cloudsigma/config.go (right):

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config.go#newcode63
provider/cloudsigma/config.go:63: "storage-auth-key",
I'm pretty sure region should be immutable.

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config.go#newcode125
provider/cloudsigma/config.go:125: // to ensure the object we return
icfgs internally consistent.
s/icfgs/is/

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config_test.go
File provider/cloudsigma/config_test.go (right):

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config_test.go#newcode62
provider/cloudsigma/config_test.go:62: var newConfigTests = []struct {
Ideally, put these definitions inside the test method. We've found
there's a distressing tendency for people to abuse them when they're
global :(.

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config_test.go#newcode71
provider/cloudsigma/config_test.go:71: err: ".* must not be empty.*",
Please don't use .* in tests except when you *really* have to. It masks
all kinds of embarrassments ;).

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config_test.go#newcode159
provider/cloudsigma/config_test.go:159: info: "can change region",
I don't think this is meaningful.

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config_test.go#newcode237
provider/cloudsigma/config_test.go:237: fail :=
failReader{fmt.Errorf("error")}
This sort of test makes me happy, thank you.

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config_test.go#newcode286
provider/cloudsigma/config_test.go:286: err: ".* must not be
empty.*",
as above, please avoid unless *really* necessary

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/config_test.go#newcode319
provider/cloudsigma/config_test.go:319: c.Errorf("secrect field %s not
found in configFields", field)
s/secrect/secret/

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/constraints_test.go
File provider/cloudsigma/constraints_test.go (right):

https://codereview.appspot.com/101970045/diff/20001/provider/cloudsigma/constraints_test.go#newcode94
provider/cloudsigma/constraints_test.go:94: c.Logf("test (%d): %+v", i,
t)
It's generally more convenient to log all the tests as they go past...
and it's nice to get the test description *before* the failure.

https://codereview.appspot.com/101970045/

2676. By Maxim Perenesenko

merged r2823

2677. By Maxim Perenesenko

fix tests after merge & issues from https://codereview.appspot.com/101970045/#msg2

Unmerged revisions

2677. By Maxim Perenesenko

fix tests after merge & issues from https://codereview.appspot.com/101970045/#msg2

2676. By Maxim Perenesenko

merged r2823

2675. By Maxim Perenesenko

dependencies.tsv updated, added entry for gosigma library

2674. By Maxim Perenesenko

merged r2811

2673. By Maxim Perenesenko

vet & lint

2672. By Maxim Perenesenko

cloudsigma provider: changed value of default cpu-power

2671. By Maxim Perenesenko

cloudsigma constraints tests

2670. By Maxim Perenesenko

cloudsigma test for new remote storage

2669. By Maxim Perenesenko

cloudsigma tests added

2668. By Maxim Perenesenko

merged r2799

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/jujud/machine.go'
2--- cmd/jujud/machine.go 2014-06-02 21:26:40 +0000
3+++ cmd/jujud/machine.go 2014-06-09 11:10:33 +0000
4@@ -433,7 +433,9 @@
5 // Take advantage of special knowledge here in that we will only ever want
6 // the storage provider on one machine, and that is the "bootstrap" node.
7 providerType := agentConfig.Value(agent.ProviderType)
8- if (providerType == provider.Local || provider.IsManual(providerType)) && m.Id() == bootstrapMachineId {
9+ if (providerType == provider.Local ||
10+ provider.IsManual(providerType) ||
11+ providerType == "cloudsigma") && m.Id() == bootstrapMachineId {
12 a.startWorkerAfterUpgrade(runner, "local-storage", func() (worker.Worker, error) {
13 // TODO(axw) 2013-09-24 bug #1229507
14 // Make another job to enable storage.
15
16=== modified file 'dependencies.tsv'
17--- dependencies.tsv 2014-06-02 15:47:39 +0000
18+++ dependencies.tsv 2014-06-09 11:10:33 +0000
19@@ -1,5 +1,6 @@
20 code.google.com/p/go.crypto hg 6478cc9340cbbe6c04511280c5007722269108e9 184
21 code.google.com/p/go.net hg c17ad62118ea511e1051721b429779fa40bddc74 116
22+github.com/Altoros/gosigma git c56c52eb785e48ec0f7fcc0da385c85210c7ff3d
23 github.com/binary132/gojsonpointer git 57ab5e9c764219a3e0c4d7759797fefdcab22e9c
24 github.com/binary132/gojsonreference git 75785fb7b21f9bf2051dca600da83ff57bc6582a
25 github.com/binary132/gojsonschema git 640782bf48d45ba2b22fd1e8a80842667c52af00
26
27=== modified file 'provider/all/all.go'
28--- provider/all/all.go 2014-04-02 11:35:49 +0000
29+++ provider/all/all.go 2014-06-09 11:10:33 +0000
30@@ -6,6 +6,7 @@
31 // Register all the available providers.
32 import (
33 _ "launchpad.net/juju-core/provider/azure"
34+ _ "launchpad.net/juju-core/provider/cloudsigma"
35 _ "launchpad.net/juju-core/provider/ec2"
36 _ "launchpad.net/juju-core/provider/joyent"
37 _ "launchpad.net/juju-core/provider/local"
38
39=== added directory 'provider/cloudsigma'
40=== added file 'provider/cloudsigma/client.go'
41--- provider/cloudsigma/client.go 1970-01-01 00:00:00 +0000
42+++ provider/cloudsigma/client.go 2014-06-09 11:10:33 +0000
43@@ -0,0 +1,307 @@
44+// Copyright 2014 Canonical Ltd.
45+// Licensed under the AGPLv3, see LICENCE file for details.
46+
47+package cloudsigma
48+
49+import (
50+ "fmt"
51+ "launchpad.net/juju-core/environs"
52+ "launchpad.net/juju-core/instance"
53+ "launchpad.net/juju-core/utils"
54+ "strings"
55+
56+ "github.com/Altoros/gosigma"
57+ "github.com/juju/loggo"
58+)
59+
60+// This file contains implementation of CloudSigma client.
61+type environClient struct {
62+ conn *gosigma.Client
63+ region string
64+ username string
65+ password string
66+ name string
67+ storage *environStorage
68+}
69+
70+type tracer struct{}
71+
72+func (tracer) Logf(format string, args ...interface{}) {
73+ logger.Tracef(format, args...)
74+}
75+
76+// newClient creates new CloudSigma client connection.
77+var newClient = func(cfg *environConfig) (*environClient, error) {
78+
79+ // fetch and validate configuration
80+ region, err := gosigma.ResolveEndpoint(cfg.region())
81+ if err != nil {
82+ region = cfg.region()
83+ }
84+ username := cfg.username()
85+ password := cfg.password()
86+ name := cfg.Name()
87+
88+ logger.Debugf("creating CloudSigma client: region=%s, user=%s, password=%s, name=%q",
89+ region, username, strings.Repeat("*", len(password)), name)
90+
91+ // create connection to CloudSigma
92+ conn, err := gosigma.NewClient(region, username, password, nil)
93+ if err != nil {
94+ return nil, err
95+ }
96+
97+ // configure trace logger
98+ if logger.LogLevel() <= loggo.TRACE {
99+ conn.Logger(&tracer{})
100+ }
101+
102+ c := &environClient{
103+ conn: conn,
104+ region: region,
105+ name: name,
106+ username: username,
107+ password: password,
108+ }
109+
110+ return c, nil
111+}
112+
113+// configChanged checks if CloudSigma client environment configuration is changed
114+func (c environClient) configChanged(cfg *environConfig) bool {
115+ // fetch configuration
116+ region, err := gosigma.ResolveEndpoint(cfg.region())
117+ if err != nil {
118+ return true
119+ }
120+ username := cfg.username()
121+ password := cfg.password()
122+ name := cfg.Name()
123+
124+ // compare
125+ if region != c.region || username != c.username ||
126+ password != c.password || name != c.name {
127+ return true
128+ }
129+
130+ return false
131+}
132+
133+const (
134+ jujuMetaInstance = "juju-instance"
135+ jujuMetaInstanceStateServer = "state-server"
136+ jujuMetaInstanceServer = "server"
137+
138+ jujuMetaEnvironment = "juju-environment"
139+)
140+
141+func (c environClient) isMyEnvironment(s gosigma.Server) bool {
142+ if v, _ := s.Get(jujuMetaEnvironment); c.name == v {
143+ return true
144+ }
145+ return false
146+}
147+
148+func (c environClient) isMyServer(s gosigma.Server) bool {
149+ if _, ok := s.Get(jujuMetaInstance); ok {
150+ return c.isMyEnvironment(s)
151+ }
152+ return false
153+}
154+
155+func (c environClient) isMyStateServer(s gosigma.Server) bool {
156+ if v, ok := s.Get(jujuMetaInstance); ok && v == jujuMetaInstanceStateServer {
157+ return c.isMyEnvironment(s)
158+ }
159+ return false
160+}
161+
162+// instances of servers at CloudSigma account
163+func (c environClient) instances() ([]gosigma.Server, error) {
164+ return c.conn.ServersFiltered(gosigma.RequestDetail, c.isMyServer)
165+}
166+
167+// instanceMap of server ids to servers at CloudSigma account
168+func (c environClient) instanceMap() (map[string]gosigma.Server, error) {
169+ servers, err := c.conn.ServersFiltered(gosigma.RequestDetail, c.isMyServer)
170+ if err != nil {
171+ return nil, err
172+ }
173+
174+ m := make(map[string]gosigma.Server, len(servers))
175+ for _, s := range servers {
176+ m[s.UUID()] = s
177+ }
178+
179+ return m, nil
180+}
181+
182+func (c environClient) stateServerAddress() (string, string, bool) {
183+ logger.Debugf("query state...")
184+
185+ servers, err := c.conn.ServersFiltered(gosigma.RequestDetail, c.isMyStateServer)
186+ if err != nil {
187+ return "", "", false
188+ }
189+
190+ logger.Debugf("...servers count: %d", len(servers))
191+ if logger.LogLevel() <= loggo.TRACE {
192+ for _, s := range servers {
193+ logger.Tracef("... %s", s)
194+ }
195+ }
196+
197+ for _, s := range servers {
198+ if s.Status() != gosigma.ServerRunning {
199+ continue
200+ }
201+ if addrs := s.IPv4(); len(addrs) != 0 {
202+ return s.UUID(), addrs[0], true
203+ }
204+ }
205+
206+ return "", "", false
207+}
208+
209+// stop instance
210+func (c environClient) stopInstance(id instance.Id) error {
211+ uuid := string(id)
212+ if uuid == "" {
213+ return fmt.Errorf("invalid instance id")
214+ }
215+
216+ var err error
217+
218+ s, err := c.conn.Server(uuid)
219+ if err != nil {
220+ return err
221+ }
222+
223+ if c.storage != nil && c.isMyStateServer(s) {
224+ c.storage.onStateInstanceStop(uuid)
225+ }
226+
227+ err = s.StopWait()
228+ logger.Tracef("environClient.StopInstance - stop server, %q = %v", uuid, err)
229+
230+ err = s.Remove(gosigma.RecurseAllDrives)
231+ logger.Tracef("environClient.StopInstance - remove server, %q = %v", uuid, err)
232+
233+ return nil
234+}
235+
236+// start new instance
237+func (c environClient) newInstance(args environs.StartInstanceParams) (srv gosigma.Server, drv gosigma.Drive, err error) {
238+
239+ cleanup := func() {
240+ if err == nil {
241+ return
242+ }
243+ if srv != nil {
244+ srv.Remove(gosigma.RecurseAllDrives)
245+ } else if drv != nil {
246+ drv.Remove()
247+ }
248+ srv = nil
249+ drv = nil
250+ }
251+ defer cleanup()
252+
253+ if args.MachineConfig == nil {
254+ err = fmt.Errorf("invalid configuration for new instance")
255+ return
256+ }
257+
258+ logger.Tracef("Tools: %v", args.Tools.URLs())
259+ logger.Tracef("Juju Constraints:" + args.Constraints.String())
260+ logger.Tracef("MachineConfig: %#v", args.MachineConfig)
261+
262+ cs, err := newConstraints(args.MachineConfig.Bootstrap,
263+ args.Constraints, args.MachineConfig.Tools.Version.Series)
264+ if err != nil {
265+ return
266+ }
267+ logger.Debugf("CloudSigma Constraints: %v", cs)
268+
269+ originalDrive, err := c.conn.Drive(cs.driveTemplate, gosigma.LibraryMedia)
270+ if err != nil {
271+ err = fmt.Errorf("query drive template: %v", err)
272+ return
273+ }
274+
275+ baseName := "juju-" + c.name + "-" + args.MachineConfig.MachineId
276+
277+ cloneParams := gosigma.CloneParams{Name: baseName}
278+ if drv, err = originalDrive.CloneWait(cloneParams, nil); err != nil {
279+ err = fmt.Errorf("error cloning drive: %v", err)
280+ return
281+ }
282+
283+ if drv.Size() < cs.driveSize {
284+ if err = drv.ResizeWait(cs.driveSize); err != nil {
285+ err = fmt.Errorf("error resizing drive: %v", err)
286+ return
287+ }
288+ }
289+
290+ var cc gosigma.Components
291+ cc.SetName(baseName)
292+ cc.SetDescription(baseName)
293+
294+ if cs.cores != 0 {
295+ cc.SetSMP(cs.cores)
296+ }
297+
298+ if cs.power != 0 {
299+ cc.SetCPU(cs.power)
300+ }
301+
302+ if cs.mem != 0 {
303+ cc.SetMem(cs.mem)
304+ }
305+
306+ vncpass, err := utils.RandomPassword()
307+ if err != nil {
308+ err = fmt.Errorf("error generating password: %v", err)
309+ return
310+ }
311+ cc.SetVNCPassword(vncpass)
312+
313+ cc.SetSSHPublicKey(args.MachineConfig.AuthorizedKeys)
314+ cc.AttachDrive(1, "0:0", "virtio", drv.UUID())
315+ cc.NetworkDHCP4(gosigma.ModelVirtio)
316+
317+ if args.MachineConfig.Bootstrap {
318+ cc.SetMeta(jujuMetaInstance, jujuMetaInstanceStateServer)
319+ } else {
320+ cc.SetMeta(jujuMetaInstance, jujuMetaInstanceServer)
321+ }
322+
323+ if c.name != "" {
324+ cc.SetMeta(jujuMetaEnvironment, c.name)
325+ }
326+
327+ if srv, err = c.conn.CreateServer(cc); err != nil {
328+ err = fmt.Errorf("error creating new instance: %v", err)
329+ return
330+ }
331+
332+ if err = srv.StartWait(); err != nil {
333+ err = fmt.Errorf("error booting new instance: %v", err)
334+ return
335+ }
336+
337+ var ipaddr string
338+ instanceNetworkAvailable := func(s gosigma.Server) bool {
339+ ipaddr = sigmaInstance{s}.findIPv4()
340+ return ipaddr != ""
341+ }
342+ if err = srv.Wait(instanceNetworkAvailable); err != nil {
343+ err = fmt.Errorf("error waiting for instance IP address: %v", err)
344+ return
345+ }
346+
347+ logger.Tracef("instance ip %q", ipaddr)
348+
349+ return
350+}
351
352=== added file 'provider/cloudsigma/client_test.go'
353--- provider/cloudsigma/client_test.go 1970-01-01 00:00:00 +0000
354+++ provider/cloudsigma/client_test.go 2014-06-09 11:10:33 +0000
355@@ -0,0 +1,316 @@
356+// Copyright 2014 Canonical Ltd.
357+// Licensed under the AGPLv3, see LICENCE file for details.
358+
359+package cloudsigma
360+
361+import (
362+ "fmt"
363+ "strings"
364+ "time"
365+
366+ "github.com/Altoros/gosigma"
367+ "github.com/Altoros/gosigma/data"
368+ "github.com/Altoros/gosigma/mock"
369+ "github.com/juju/loggo"
370+ gc "launchpad.net/gocheck"
371+ "launchpad.net/juju-core/constraints"
372+ "launchpad.net/juju-core/environs"
373+ "launchpad.net/juju-core/environs/cloudinit"
374+ "launchpad.net/juju-core/instance"
375+ "launchpad.net/juju-core/testing"
376+ "launchpad.net/juju-core/tools"
377+ "launchpad.net/juju-core/version"
378+)
379+
380+type clientSuite struct {
381+ testing.BaseSuite
382+}
383+
384+var _ = gc.Suite(&clientSuite{})
385+
386+func (s *clientSuite) SetUpSuite(c *gc.C) {
387+ s.BaseSuite.SetUpSuite(c)
388+ mock.Start()
389+}
390+
391+func (s *clientSuite) TearDownSuite(c *gc.C) {
392+ mock.Stop()
393+ s.BaseSuite.TearDownSuite(c)
394+}
395+
396+func (s *clientSuite) SetUpTest(c *gc.C) {
397+ s.BaseSuite.SetUpTest(c)
398+
399+ ll := logger.LogLevel()
400+ logger.SetLogLevel(loggo.TRACE)
401+ s.AddCleanup(func(*gc.C) { logger.SetLogLevel(ll) })
402+
403+ mock.Reset()
404+}
405+
406+func (s *clientSuite) TearDownTest(c *gc.C) {
407+ mock.Reset()
408+ s.BaseSuite.TearDownTest(c)
409+}
410+
411+func testNewClient(c *gc.C, endpoint, username, password string) (*environClient, error) {
412+ ecfg := &environConfig{
413+ Config: newConfig(c, testing.Attrs{"name": "client-test"}),
414+ attrs: map[string]interface{}{
415+ "region": endpoint,
416+ "username": username,
417+ "password": password,
418+ },
419+ }
420+ return newClient(ecfg)
421+}
422+
423+func (s *clientSuite) TestClientNew(c *gc.C) {
424+ cli, err := testNewClient(c, "https://testing.invalid", "user", "password")
425+ c.Check(err, gc.IsNil)
426+ c.Check(cli, gc.NotNil)
427+
428+ cli, err = testNewClient(c, "http://testing.invalid", "user", "password")
429+ c.Check(err, gc.ErrorMatches, "endpoint must use https scheme")
430+ c.Check(cli, gc.IsNil)
431+
432+ cli, err = testNewClient(c, "https://testing.invalid", "", "password")
433+ c.Check(err, gc.ErrorMatches, "username is not allowed to be empty")
434+ c.Check(cli, gc.IsNil)
435+}
436+
437+func (s *clientSuite) TestClientConfigChanged(c *gc.C) {
438+ ecfg := &environConfig{
439+ Config: newConfig(c, testing.Attrs{"name": "client-test"}),
440+ attrs: map[string]interface{}{
441+ "region": "https://testing.invalid",
442+ "username": "user",
443+ "password": "password",
444+ },
445+ }
446+
447+ cli, err := newClient(ecfg)
448+ c.Check(err, gc.IsNil)
449+ c.Check(cli, gc.NotNil)
450+
451+ rc := cli.configChanged(ecfg)
452+ c.Check(rc, gc.Equals, false)
453+
454+ ecfg.attrs["region"] = ""
455+ rc = cli.configChanged(ecfg)
456+ c.Check(rc, gc.Equals, true)
457+
458+ ecfg.attrs["region"] = "https://testing.invalid"
459+ ecfg.attrs["username"] = "user1"
460+ rc = cli.configChanged(ecfg)
461+ c.Check(rc, gc.Equals, true)
462+
463+ ecfg.attrs["username"] = "user"
464+ ecfg.attrs["password"] = "password1"
465+ rc = cli.configChanged(ecfg)
466+ c.Check(rc, gc.Equals, true)
467+
468+ ecfg.attrs["password"] = "password"
469+ ecfg.Config = newConfig(c, testing.Attrs{"name": "changed"})
470+ rc = cli.configChanged(ecfg)
471+ c.Check(rc, gc.Equals, true)
472+}
473+
474+func addTestClientServer(c *gc.C, instance, env, ip string) string {
475+ json := `{"meta": {`
476+ if instance != "" {
477+ json += fmt.Sprintf(`"juju-instance": "%s"`, instance)
478+ if env != "" {
479+ json += fmt.Sprintf(`, "juju-environment": "%s"`, env)
480+ }
481+ }
482+ json += fmt.Sprintf(`}, "status": "running", "nics":[{
483+ "runtime": {
484+ "interface_type": "public",
485+ "ip_v4": {
486+ "resource_uri": "/api/2.0/ips/%s/",
487+ "uuid": "%s"
488+ }}}]}`, ip, ip)
489+ r := strings.NewReader(json)
490+ s, err := data.ReadServer(r)
491+ c.Assert(err, gc.IsNil)
492+ mock.AddServer(s)
493+ return s.UUID
494+}
495+
496+func (s *clientSuite) TestClientInstances(c *gc.C) {
497+ addTestClientServer(c, "", "", "")
498+ addTestClientServer(c, jujuMetaInstanceServer, "alien", "")
499+ addTestClientServer(c, jujuMetaInstanceStateServer, "alien", "")
500+ addTestClientServer(c, jujuMetaInstanceServer, "client-test", "1.1.1.1")
501+ addTestClientServer(c, jujuMetaInstanceServer, "client-test", "2.2.2.2")
502+ suuid := addTestClientServer(c, jujuMetaInstanceStateServer, "client-test", "3.3.3.3")
503+
504+ cli, err := testNewClient(c, mock.Endpoint(""), mock.TestUser, mock.TestPassword)
505+ c.Assert(err, gc.IsNil)
506+
507+ ss, err := cli.instances()
508+ c.Assert(err, gc.IsNil)
509+ c.Assert(ss, gc.NotNil)
510+ c.Check(ss, gc.HasLen, 3)
511+
512+ sm, err := cli.instanceMap()
513+ c.Assert(err, gc.IsNil)
514+ c.Assert(sm, gc.NotNil)
515+ c.Check(sm, gc.HasLen, 3)
516+
517+ uuid, ip, rc := cli.stateServerAddress()
518+ c.Check(uuid, gc.Equals, suuid)
519+ c.Check(ip, gc.Equals, "3.3.3.3")
520+ c.Check(rc, gc.Equals, true)
521+}
522+
523+func (s *clientSuite) TestClientStopStateInstance(c *gc.C) {
524+ addTestClientServer(c, "", "", "")
525+ addTestClientServer(c, jujuMetaInstanceServer, "alien", "")
526+ addTestClientServer(c, jujuMetaInstanceStateServer, "alien", "")
527+ addTestClientServer(c, jujuMetaInstanceServer, "client-test", "1.1.1.1")
528+ addTestClientServer(c, jujuMetaInstanceServer, "client-test", "2.2.2.2")
529+ suuid := addTestClientServer(c, jujuMetaInstanceStateServer, "client-test", "3.3.3.3")
530+
531+ cli, err := testNewClient(c, mock.Endpoint(""), mock.TestUser, mock.TestPassword)
532+ c.Assert(err, gc.IsNil)
533+
534+ cli.storage = &environStorage{uuid: suuid, tmp: true}
535+
536+ err = cli.stopInstance(instance.Id(suuid))
537+ c.Assert(err, gc.IsNil)
538+
539+ uuid, ip, rc := cli.stateServerAddress()
540+ c.Check(uuid, gc.Equals, "")
541+ c.Check(ip, gc.Equals, "")
542+ c.Check(rc, gc.Equals, false)
543+
544+ c.Check(cli.storage.tmp, gc.Equals, false)
545+}
546+
547+func (s *clientSuite) TestClientInvalidStopInstance(c *gc.C) {
548+ cli, err := testNewClient(c, mock.Endpoint(""), mock.TestUser, mock.TestPassword)
549+ c.Assert(err, gc.IsNil)
550+
551+ var id instance.Id
552+ err = cli.stopInstance(id)
553+ c.Check(err, gc.ErrorMatches, "invalid instance id")
554+
555+ err = cli.stopInstance("1234")
556+ c.Check(err, gc.ErrorMatches, "404 Not Found.*")
557+}
558+
559+func (s *clientSuite) TestClientInvalidServer(c *gc.C) {
560+ cli, err := testNewClient(c, "https://testing.invalid", mock.TestUser, mock.TestPassword)
561+ c.Assert(err, gc.IsNil)
562+
563+ cli.conn.ConnectTimeout(10 * time.Millisecond)
564+
565+ err = cli.stopInstance("1234")
566+ c.Check(err, gc.ErrorMatches, "broken connection")
567+
568+ _, err = cli.instanceMap()
569+ c.Check(err, gc.ErrorMatches, "broken connection")
570+
571+ uuid, ip, ok := cli.stateServerAddress()
572+ c.Check(uuid, gc.Equals, "")
573+ c.Check(ip, gc.Equals, "")
574+ c.Check(ok, gc.Equals, false)
575+}
576+
577+func (s *clientSuite) TestClientNewInstanceInvalidParams(c *gc.C) {
578+ cli, err := testNewClient(c, mock.Endpoint(""), mock.TestUser, mock.TestPassword)
579+ c.Assert(err, gc.IsNil)
580+
581+ params := environs.StartInstanceParams{
582+ Constraints: constraints.Value{},
583+ }
584+ server, drive, err := cli.newInstance(params)
585+ c.Check(server, gc.IsNil)
586+ c.Check(drive, gc.IsNil)
587+ c.Check(err, gc.ErrorMatches, "invalid configuration for new instance")
588+
589+ params.MachineConfig = &cloudinit.MachineConfig{
590+ Bootstrap: true,
591+ Tools: &tools.Tools{
592+ Version: version.Binary{
593+ Series: "series",
594+ },
595+ },
596+ }
597+ server, drive, err = cli.newInstance(params)
598+ c.Check(server, gc.IsNil)
599+ c.Check(drive, gc.IsNil)
600+ c.Check(err, gc.ErrorMatches, "series 'series' not supported")
601+
602+ var arch = "arch"
603+ params.Constraints.Arch = &arch
604+ server, drive, err = cli.newInstance(params)
605+ c.Check(server, gc.IsNil)
606+ c.Check(drive, gc.IsNil)
607+ c.Check(err, gc.ErrorMatches, "arch 'arch' not supported")
608+}
609+
610+func (s *clientSuite) TestClientNewInstanceInvalidTemplate(c *gc.C) {
611+ cli, err := testNewClient(c, mock.Endpoint(""), mock.TestUser, mock.TestPassword)
612+ c.Assert(err, gc.IsNil)
613+
614+ params := environs.StartInstanceParams{
615+ Constraints: constraints.Value{},
616+ MachineConfig: &cloudinit.MachineConfig{
617+ Bootstrap: true,
618+ Tools: &tools.Tools{
619+ Version: version.Binary{
620+ Series: "trusty",
621+ },
622+ },
623+ },
624+ }
625+ server, drive, err := cli.newInstance(params)
626+ c.Check(server, gc.IsNil)
627+ c.Check(drive, gc.IsNil)
628+ c.Check(err, gc.ErrorMatches, "query drive template: 404 Not Found.*")
629+}
630+
631+func (s *clientSuite) TestClientNewInstance(c *gc.C) {
632+ cli, err := testNewClient(c, mock.Endpoint(""), mock.TestUser, mock.TestPassword)
633+ c.Assert(err, gc.IsNil)
634+
635+ cli.conn.OperationTimeout(1 * time.Second)
636+
637+ params := environs.StartInstanceParams{
638+ Constraints: constraints.Value{},
639+ MachineConfig: &cloudinit.MachineConfig{
640+ Bootstrap: true,
641+ Tools: &tools.Tools{
642+ Version: version.Binary{
643+ Series: "trusty",
644+ },
645+ },
646+ },
647+ }
648+ cs, err := newConstraints(params.MachineConfig.Bootstrap,
649+ params.Constraints, params.MachineConfig.Tools.Version.Series)
650+ c.Assert(cs, gc.NotNil)
651+ c.Check(err, gc.IsNil)
652+
653+ templateDrive := &data.Drive{
654+ Resource: data.Resource{URI: "uri", UUID: cs.driveTemplate},
655+ LibraryDrive: data.LibraryDrive{
656+ Arch: "arch",
657+ ImageType: "image-type",
658+ OS: "os",
659+ Paid: true,
660+ },
661+ Size: 2200 * gosigma.Megabyte,
662+ Status: "unmounted",
663+ }
664+ mock.ResetDrives()
665+ mock.LibDrives.Add(templateDrive)
666+
667+ server, drive, err := cli.newInstance(params)
668+ c.Check(server, gc.NotNil)
669+ c.Check(drive, gc.NotNil)
670+ c.Check(err, gc.IsNil)
671+}
672
673=== added file 'provider/cloudsigma/config.go'
674--- provider/cloudsigma/config.go 1970-01-01 00:00:00 +0000
675+++ provider/cloudsigma/config.go 2014-06-09 11:10:33 +0000
676@@ -0,0 +1,165 @@
677+// Copyright 2014 Canonical Ltd.
678+// Licensed under the AGPLv3, see LICENCE file for details.
679+
680+package cloudsigma
681+
682+import (
683+ "fmt"
684+ "github.com/Altoros/gosigma"
685+
686+ "launchpad.net/juju-core/environs/config"
687+ "launchpad.net/juju-core/schema"
688+ "launchpad.net/juju-core/utils"
689+)
690+
691+// boilerplateConfig will be shown in help output, so please keep it up to
692+// date when you change environment configuration below.
693+var boilerplateConfig = `# https://juju.ubuntu.com/docs/config-cloudsigma.html
694+cloudsigma:
695+ type: cloudsigma
696+
697+ # region holds the cloudsigma region (zrh, lvs, ...).
698+ #
699+ # region: <your region>
700+
701+ # credentials for CloudSigma account
702+ #
703+ # username: <your username>
704+ # password: <secret>
705+
706+ # storage-port specifes the TCP port that the
707+ # bootstrap machine's Juju storage server will listen
708+ # on. It defaults to ` + fmt.Sprint(defaultStoragePort) + `
709+ #
710+ # storage-port: ` + fmt.Sprint(defaultStoragePort) + `
711+
712+`
713+
714+const defaultStoragePort = 8040
715+
716+var configFields = schema.Fields{
717+ "username": schema.String(),
718+ "password": schema.String(),
719+ "region": schema.String(),
720+
721+ "storage-port": schema.ForceInt(),
722+ "storage-auth-key": schema.String(),
723+}
724+
725+var configDefaultFields = schema.Defaults{
726+ "username": "",
727+ "password": "",
728+ "region": gosigma.DefaultRegion,
729+
730+ "storage-port": defaultStoragePort,
731+}
732+
733+var configSecretFields = []string{
734+ "storage-auth-key",
735+}
736+
737+var configImmutableFields = []string{
738+ "storage-port",
739+ "storage-auth-key",
740+ "region",
741+}
742+
743+func prepareConfig(cfg *config.Config) (*config.Config, error) {
744+ // Turn an incomplete config into a valid one, if possible.
745+ attrs := cfg.UnknownAttrs()
746+
747+ if _, ok := attrs["storage-auth-key"]; !ok {
748+ uuid, err := utils.NewUUID()
749+ if err != nil {
750+ return nil, err
751+ }
752+ attrs["storage-auth-key"] = uuid.String()
753+ }
754+
755+ return cfg.Apply(attrs)
756+}
757+
758+func validateConfig(cfg *config.Config, old *environConfig) (*environConfig, error) {
759+ // Check sanity of juju-level fields.
760+ var oldCfg *config.Config
761+ if old != nil {
762+ oldCfg = old.Config
763+ }
764+ if err := config.Validate(cfg, oldCfg); err != nil {
765+ return nil, err
766+ }
767+
768+ // Extract validated provider-specific fields. All of configFields will be
769+ // present in validated, and defaults will be inserted if necessary. If the
770+ // schema you passed in doesn't quite express what you need, you can make
771+ // whatever checks you need here, before continuing.
772+ // In particular, if you want to extract (say) credentials from the user's
773+ // shell environment variables, you'll need to allow missing values to pass
774+ // through the schema by setting a value of schema.Omit in the configFields
775+ // map, and then to set and check them at this point. These values *must* be
776+ // stored in newAttrs: a Config will be generated on the user's machine only
777+ // to begin with, and will subsequently be used on a different machine that
778+ // will probably not have those variables set.
779+ newAttrs, err := cfg.ValidateUnknownAttrs(configFields, configDefaultFields)
780+ if err != nil {
781+ return nil, err
782+ }
783+ for field := range configFields {
784+ if newAttrs[field] == "" {
785+ return nil, fmt.Errorf("%s: must not be empty", field)
786+ }
787+ }
788+
789+ // If an old config was supplied, check any immutable fields have not changed.
790+ if old != nil {
791+ for _, field := range configImmutableFields {
792+ if old.attrs[field] != newAttrs[field] {
793+ return nil, fmt.Errorf(
794+ "%s: cannot change from %v to %v",
795+ field, old.attrs[field], newAttrs[field],
796+ )
797+ }
798+ }
799+ }
800+
801+ // Merge the validated provider-specific fields into the original config,
802+ // to ensure the object we return is internally consistent.
803+ newCfg, err := cfg.Apply(newAttrs)
804+ if err != nil {
805+ return nil, err
806+ }
807+ ecfg := &environConfig{
808+ Config: newCfg,
809+ attrs: newAttrs,
810+ }
811+
812+ return ecfg, nil
813+}
814+
815+type environConfig struct {
816+ *config.Config
817+ attrs map[string]interface{}
818+}
819+
820+func (c environConfig) region() string {
821+ return c.attrs["region"].(string)
822+}
823+
824+func (c environConfig) username() string {
825+ return c.attrs["username"].(string)
826+}
827+
828+func (c environConfig) password() string {
829+ return c.attrs["password"].(string)
830+}
831+
832+func (c environConfig) storagePort() int {
833+ return c.attrs["storage-port"].(int)
834+}
835+
836+func (c environConfig) storageAuthKey() string {
837+ if v, ok := c.attrs["storage-auth-key"].(string); ok {
838+ return v
839+ }
840+ return ""
841+}
842
843=== added file 'provider/cloudsigma/config_test.go'
844--- provider/cloudsigma/config_test.go 1970-01-01 00:00:00 +0000
845+++ provider/cloudsigma/config_test.go 2014-06-09 11:10:33 +0000
846@@ -0,0 +1,329 @@
847+// Copyright 2014 Canonical Ltd.
848+// Licensed under the AGPLv3, see LICENCE file for details.
849+
850+package cloudsigma
851+
852+import (
853+ "crypto/rand"
854+ "fmt"
855+ gc "launchpad.net/gocheck"
856+
857+ "launchpad.net/juju-core/environs"
858+ "launchpad.net/juju-core/environs/config"
859+ "launchpad.net/juju-core/schema"
860+ "launchpad.net/juju-core/testing"
861+)
862+
863+func newConfig(c *gc.C, attrs testing.Attrs) *config.Config {
864+ attrs = testing.FakeConfig().Merge(attrs)
865+ cfg, err := config.New(config.UseDefaults, attrs)
866+ c.Assert(err, gc.IsNil)
867+ return cfg
868+}
869+
870+func validAttrs() testing.Attrs {
871+ return testing.FakeConfig().Merge(testing.Attrs{
872+ "type": "cloudsigma",
873+ "username": "user",
874+ "password": "password",
875+ "region": "zrh",
876+ "storage-port": 8040,
877+ "storage-auth-key": "ABCDEFGH",
878+ })
879+}
880+
881+type configSuite struct {
882+ testing.BaseSuite
883+}
884+
885+func (s *configSuite) SetUpSuite(c *gc.C) {
886+ s.BaseSuite.SetUpSuite(c)
887+}
888+
889+func (s *configSuite) SetUpTest(c *gc.C) {
890+ s.BaseSuite.SetUpTest(c)
891+ // speed up tests, do not create heavy stuff inside providers created withing this test suite
892+ s.PatchValue(&newClient, func(cfg *environConfig) (*environClient, error) {
893+ return nil, nil
894+ })
895+ s.PatchValue(&newStorage, func(ecfg *environConfig, client *environClient) (*environStorage, error) {
896+ return nil, nil
897+ })
898+}
899+
900+var _ = gc.Suite(&configSuite{})
901+
902+func (s *configSuite) TestNewEnvironConfig(c *gc.C) {
903+
904+ type checker struct {
905+ checker gc.Checker
906+ value interface{}
907+ }
908+
909+ var newConfigTests = []struct {
910+ info string
911+ insert testing.Attrs
912+ remove []string
913+ expect testing.Attrs
914+ err string
915+ }{{
916+ info: "username is required",
917+ remove: []string{"username"},
918+ err: "username: must not be empty",
919+ }, {
920+ info: "username cannot be empty",
921+ insert: testing.Attrs{"username": ""},
922+ err: "username: must not be empty",
923+ }, {
924+ info: "password is required",
925+ remove: []string{"password"},
926+ err: "password: must not be empty",
927+ }, {
928+ info: "password cannot be empty",
929+ insert: testing.Attrs{"password": ""},
930+ err: "password: must not be empty",
931+ }, {
932+ info: "region is inserted if missing",
933+ remove: []string{"region"},
934+ expect: testing.Attrs{"region": "zrh"},
935+ }, {
936+ info: "region must not be empty",
937+ insert: testing.Attrs{"region": ""},
938+ err: "region: must not be empty",
939+ }, {
940+ info: "storage-port is inserted if missing",
941+ remove: []string{"storage-port"},
942+ expect: testing.Attrs{"storage-port": 8040},
943+ }, {
944+ info: "storage-port must be number",
945+ insert: testing.Attrs{"storage-port": "abcd"},
946+ err: "storage-port: expected number, got string\\(\"abcd\"\\)",
947+ }, {
948+ info: "storage-auth-key is inserted if missing",
949+ remove: []string{"storage-auth-key"},
950+ expect: testing.Attrs{"storage-auth-key": checker{gc.HasLen, 36}},
951+ }, {
952+ info: "storage-auth-key must not be empty",
953+ insert: testing.Attrs{"storage-auth-key": ""},
954+ err: "storage-auth-key: must not be empty",
955+ }}
956+
957+ for i, test := range newConfigTests {
958+ c.Logf("test %d: %s", i, test.info)
959+ attrs := validAttrs().Merge(test.insert).Delete(test.remove...)
960+ testConfig := newConfig(c, attrs)
961+ environ, err := environs.New(testConfig)
962+ if test.err == "" {
963+ c.Check(err, gc.IsNil)
964+ attrs := environ.Config().AllAttrs()
965+ for field, value := range test.expect {
966+ if chk, ok := value.(checker); ok {
967+ c.Check(attrs[field], chk.checker, chk.value)
968+ } else {
969+ c.Check(attrs[field], gc.Equals, value)
970+ }
971+ }
972+ } else {
973+ c.Check(environ, gc.IsNil)
974+ c.Check(err, gc.ErrorMatches, test.err)
975+ }
976+ }
977+}
978+
979+var changeConfigTests = []struct {
980+ info string
981+ insert testing.Attrs
982+ remove []string
983+ expect testing.Attrs
984+ err string
985+}{{
986+ info: "no change, no error",
987+ expect: validAttrs(),
988+}, {
989+ info: "can change username",
990+ insert: testing.Attrs{"username": "cloudsigma_user"},
991+ expect: testing.Attrs{"username": "cloudsigma_user"},
992+}, {
993+ info: "can not change username to empty",
994+ insert: testing.Attrs{"username": ""},
995+ err: "username: must not be empty",
996+}, {
997+ info: "can change password",
998+ insert: testing.Attrs{"password": "cloudsigma_password"},
999+ expect: testing.Attrs{"password": "cloudsigma_password"},
1000+}, {
1001+ info: "can not change password to empty",
1002+ insert: testing.Attrs{"password": ""},
1003+ err: "password: must not be empty",
1004+}, {
1005+ info: "can change region",
1006+ insert: testing.Attrs{"region": "lvs"},
1007+ err: "region: cannot change from .* to .*",
1008+}, {
1009+ info: "can not change storage-port",
1010+ insert: testing.Attrs{"storage-port": 0},
1011+ err: "storage-port: cannot change from .* to .*",
1012+}, {
1013+ info: "can not change storage-auth-key",
1014+ insert: testing.Attrs{"storage-auth-key": "xxx"},
1015+ err: "storage-auth-key: cannot change from .* to .*",
1016+}}
1017+
1018+func (s *configSuite) TestValidateChange(c *gc.C) {
1019+
1020+ baseConfig := newConfig(c, validAttrs())
1021+ for i, test := range changeConfigTests {
1022+ c.Logf("test %d: %s", i, test.info)
1023+ attrs := validAttrs().Merge(test.insert).Delete(test.remove...)
1024+ testConfig := newConfig(c, attrs)
1025+ validatedConfig, err := providerInstance.Validate(testConfig, baseConfig)
1026+ if test.err == "" {
1027+ c.Check(err, gc.IsNil)
1028+ attrs := validatedConfig.AllAttrs()
1029+ for field, value := range test.expect {
1030+ c.Check(attrs[field], gc.Equals, value)
1031+ }
1032+ } else {
1033+ c.Check(validatedConfig, gc.IsNil)
1034+ c.Check(err, gc.ErrorMatches, "invalid config.*: "+test.err)
1035+ }
1036+
1037+ // reverse change
1038+ validatedConfig, err = providerInstance.Validate(baseConfig, testConfig)
1039+ if test.err == "" {
1040+ c.Check(err, gc.IsNil)
1041+ attrs := validatedConfig.AllAttrs()
1042+ for field, value := range validAttrs() {
1043+ c.Check(attrs[field], gc.Equals, value)
1044+ }
1045+ } else {
1046+ c.Check(validatedConfig, gc.IsNil)
1047+ c.Check(err, gc.ErrorMatches, "invalid .*config.*: "+test.err)
1048+ }
1049+ }
1050+}
1051+
1052+func (s *configSuite) TestSetConfig(c *gc.C) {
1053+ baseConfig := newConfig(c, validAttrs())
1054+ for i, test := range changeConfigTests {
1055+ c.Logf("test %d: %s", i, test.info)
1056+ environ, err := environs.New(baseConfig)
1057+ c.Assert(err, gc.IsNil)
1058+ attrs := validAttrs().Merge(test.insert).Delete(test.remove...)
1059+ testConfig := newConfig(c, attrs)
1060+ err = environ.SetConfig(testConfig)
1061+ newAttrs := environ.Config().AllAttrs()
1062+ if test.err == "" {
1063+ c.Check(err, gc.IsNil)
1064+ for field, value := range test.expect {
1065+ c.Check(newAttrs[field], gc.Equals, value)
1066+ }
1067+ } else {
1068+ c.Check(err, gc.ErrorMatches, test.err)
1069+ for field, value := range baseConfig.UnknownAttrs() {
1070+ c.Check(newAttrs[field], gc.Equals, value)
1071+ }
1072+ }
1073+ }
1074+}
1075+
1076+func (s *configSuite) TestConfigName(c *gc.C) {
1077+ baseConfig := newConfig(c, validAttrs().Merge(testing.Attrs{"name": "testname"}))
1078+ environ, err := environs.New(baseConfig)
1079+ c.Assert(err, gc.IsNil)
1080+ c.Check(environ.Name(), gc.Equals, "testname")
1081+}
1082+
1083+func (s *configSuite) TestBadUUIDGenerator(c *gc.C) {
1084+ fail := failReader{fmt.Errorf("error")}
1085+ s.PatchValue(&rand.Reader, &fail)
1086+
1087+ attrs := validAttrs().Delete("storage-auth-key")
1088+ testConfig := newConfig(c, attrs)
1089+ cfg, err := providerInstance.Prepare(nil, testConfig)
1090+
1091+ c.Check(cfg, gc.IsNil)
1092+ c.Check(err, gc.Equals, fail.err)
1093+}
1094+
1095+func (s *configSuite) TestEnvironConfig(c *gc.C) {
1096+ testConfig := newConfig(c, validAttrs())
1097+ ecfg, err := validateConfig(testConfig, nil)
1098+ c.Assert(ecfg, gc.NotNil)
1099+ c.Assert(err, gc.IsNil)
1100+ c.Check(ecfg.username(), gc.Equals, "user")
1101+ c.Check(ecfg.password(), gc.Equals, "password")
1102+ c.Check(ecfg.region(), gc.Equals, "zrh")
1103+ c.Check(ecfg.storagePort(), gc.Equals, 8040)
1104+ c.Check(ecfg.storageAuthKey(), gc.Equals, "ABCDEFGH")
1105+}
1106+
1107+func (s *configSuite) TestInvalidConfigChange(c *gc.C) {
1108+ oldAttrs := validAttrs().Merge(testing.Attrs{"name": "123"})
1109+ oldConfig := newConfig(c, oldAttrs)
1110+ newAttrs := validAttrs().Merge(testing.Attrs{"name": "321"})
1111+ newConfig := newConfig(c, newAttrs)
1112+
1113+ oldecfg, _ := providerInstance.Validate(oldConfig, nil)
1114+ c.Assert(oldecfg, gc.NotNil)
1115+
1116+ newecfg, err := providerInstance.Validate(newConfig, oldecfg)
1117+ c.Assert(newecfg, gc.IsNil)
1118+ c.Assert(err, gc.NotNil)
1119+}
1120+
1121+var secretAttrsConfigTests = []struct {
1122+ info string
1123+ insert testing.Attrs
1124+ remove []string
1125+ expect map[string]string
1126+ err string
1127+}{{
1128+ info: "no change, no error",
1129+ expect: map[string]string{"storage-auth-key": "ABCDEFGH"},
1130+}, {
1131+ info: "invalid config",
1132+ insert: testing.Attrs{"username": ""},
1133+ err: ".* must not be empty.*",
1134+}}
1135+
1136+func (s *configSuite) TestSecretAttrs(c *gc.C) {
1137+ for i, test := range secretAttrsConfigTests {
1138+ c.Logf("test %d: %s", i, test.info)
1139+ attrs := validAttrs().Merge(test.insert).Delete(test.remove...)
1140+ testConfig := newConfig(c, attrs)
1141+ sa, err := providerInstance.SecretAttrs(testConfig)
1142+ if test.err == "" {
1143+ c.Check(sa, gc.HasLen, len(test.expect))
1144+ for field, value := range test.expect {
1145+ c.Check(sa[field], gc.Equals, value)
1146+ }
1147+ c.Check(err, gc.IsNil)
1148+ } else {
1149+ c.Check(sa, gc.IsNil)
1150+ c.Check(err, gc.ErrorMatches, test.err)
1151+ }
1152+ }
1153+}
1154+
1155+func (s *configSuite) TestSecretAttrsAreStrings(c *gc.C) {
1156+ for i, field := range configSecretFields {
1157+ c.Logf("test %d: %s", i, field)
1158+ attrs := validAttrs().Merge(testing.Attrs{field: 0})
1159+
1160+ if v, ok := configFields[field]; ok {
1161+ configFields[field] = schema.ForceInt()
1162+ defer func(c schema.Checker) {
1163+ configFields[field] = c
1164+ }(v)
1165+ } else {
1166+ c.Errorf("secrect field %s not found in configFields", field)
1167+ continue
1168+ }
1169+
1170+ testConfig := newConfig(c, attrs)
1171+ sa, err := providerInstance.SecretAttrs(testConfig)
1172+ c.Check(sa, gc.IsNil)
1173+ c.Check(err, gc.ErrorMatches, "secret .* field must have a string value; got .*")
1174+ }
1175+}
1176
1177=== added file 'provider/cloudsigma/constraints.go'
1178--- provider/cloudsigma/constraints.go 1970-01-01 00:00:00 +0000
1179+++ provider/cloudsigma/constraints.go 2014-06-09 11:10:33 +0000
1180@@ -0,0 +1,95 @@
1181+// Copyright 2014 Canonical Ltd.
1182+// Licensed under the AGPLv3, see LICENCE file for details.
1183+
1184+package cloudsigma
1185+
1186+import (
1187+ "fmt"
1188+
1189+ "github.com/Altoros/gosigma"
1190+ "launchpad.net/juju-core/constraints"
1191+)
1192+
1193+// This file contains implementation of CloudSigma instance constraints
1194+type sigmaConstraints struct {
1195+ driveTemplate string
1196+ driveSize uint64
1197+ cores uint64
1198+ power uint64
1199+ mem uint64
1200+}
1201+
1202+const defaultCPUPower = 2000
1203+const driveUbuntuTrusty64 = "473adb38-3b64-43b2-93bd-f1a3443c19ea"
1204+
1205+// newConstraints creates new CloudSigma constraints from juju common constraints
1206+func newConstraints(bootstrap bool, jc constraints.Value, series string) (*sigmaConstraints, error) {
1207+ var sc sigmaConstraints
1208+
1209+ if a := jc.Arch; a != nil {
1210+ switch *a {
1211+ case "amd64":
1212+ switch series {
1213+ case "trusty":
1214+ sc.driveTemplate = driveUbuntuTrusty64
1215+ default:
1216+ return nil, fmt.Errorf("series '%s' not supported", series)
1217+ }
1218+ default:
1219+ return nil, fmt.Errorf("arch '%s' not supported", *a)
1220+ }
1221+ } else {
1222+ switch series {
1223+ case "trusty":
1224+ sc.driveTemplate = driveUbuntuTrusty64
1225+ default:
1226+ return nil, fmt.Errorf("series '%s' not supported", series)
1227+ }
1228+ }
1229+
1230+ if size := jc.RootDisk; bootstrap && size == nil {
1231+ sc.driveSize = 5 * gosigma.Gigabyte
1232+ } else if size != nil {
1233+ sc.driveSize = *size * gosigma.Megabyte
1234+ }
1235+
1236+ if c := jc.CpuCores; c != nil {
1237+ sc.cores = *c
1238+ } else {
1239+ sc.cores = 1
1240+ }
1241+
1242+ if p := jc.CpuPower; p != nil {
1243+ sc.power = *p
1244+ } else {
1245+ if sc.cores == 1 {
1246+ // The default of cpu power is 2000 Mhz
1247+ sc.power = defaultCPUPower
1248+ } else {
1249+ // The maximum amount of cpu per smp is 2300
1250+ sc.power = sc.cores * defaultCPUPower
1251+ }
1252+ }
1253+
1254+ if m := jc.Mem; m != nil {
1255+ sc.mem = *m * gosigma.Megabyte
1256+ } else {
1257+ sc.mem = 2 * gosigma.Gigabyte
1258+ }
1259+
1260+ return &sc, nil
1261+}
1262+
1263+func (c *sigmaConstraints) String() string {
1264+ s := fmt.Sprintf("template=%s,drive=%dG", c.driveTemplate, c.driveSize/gosigma.Gigabyte)
1265+ if c.cores > 0 {
1266+ s += fmt.Sprintf(",cores=%d", c.cores)
1267+ }
1268+ if c.power > 0 {
1269+ s += fmt.Sprintf(",power=%d", c.power)
1270+ }
1271+ if c.mem > 0 {
1272+ s += fmt.Sprintf(",mem=%dG", c.mem/gosigma.Gigabyte)
1273+ }
1274+ return s
1275+}
1276
1277=== added file 'provider/cloudsigma/constraints_test.go'
1278--- provider/cloudsigma/constraints_test.go 1970-01-01 00:00:00 +0000
1279+++ provider/cloudsigma/constraints_test.go 2014-06-09 11:10:33 +0000
1280@@ -0,0 +1,136 @@
1281+// Copyright 2014 Canonical Ltd.
1282+// Licensed under the AGPLv3, see LICENCE file for details.
1283+
1284+package cloudsigma
1285+
1286+import (
1287+ "github.com/Altoros/gosigma"
1288+ gc "launchpad.net/gocheck"
1289+ "launchpad.net/juju-core/constraints"
1290+ "launchpad.net/juju-core/testing"
1291+)
1292+
1293+type constraintsSuite struct {
1294+ testing.BaseSuite
1295+}
1296+
1297+var _ = gc.Suite(&constraintsSuite{})
1298+
1299+type strv struct{ v string }
1300+type uint64v struct{ v uint64 }
1301+
1302+var defConstraints = map[string]sigmaConstraints{
1303+ "bootstrap-trusty": sigmaConstraints{
1304+ driveTemplate: driveUbuntuTrusty64,
1305+ driveSize: 5 * gosigma.Gigabyte,
1306+ cores: 1,
1307+ power: 2000,
1308+ mem: 2 * gosigma.Gigabyte,
1309+ },
1310+ "trusty": sigmaConstraints{
1311+ driveTemplate: driveUbuntuTrusty64,
1312+ driveSize: 0,
1313+ cores: 1,
1314+ power: 2000,
1315+ mem: 2 * gosigma.Gigabyte,
1316+ },
1317+ "trusty-c2-p4000": sigmaConstraints{
1318+ driveTemplate: driveUbuntuTrusty64,
1319+ driveSize: 0,
1320+ cores: 2,
1321+ power: 4000,
1322+ mem: 2 * gosigma.Gigabyte,
1323+ },
1324+}
1325+
1326+var newConstraintTests = []struct {
1327+ bootstrap bool
1328+ arch *strv
1329+ cores *uint64v
1330+ power *uint64v
1331+ mem *uint64v
1332+ disk *uint64v
1333+ series string
1334+ expected sigmaConstraints
1335+ err *strv
1336+}{
1337+ {true, nil, nil, nil, nil, nil, "trusty", defConstraints["bootstrap-trusty"], nil},
1338+ {false, nil, nil, nil, nil, nil, "trusty", defConstraints["trusty"], nil},
1339+ {true, nil, nil, nil, nil, nil, "", sigmaConstraints{}, &strv{"series '.*' not supported"}},
1340+ {false, nil, nil, nil, nil, nil, "", sigmaConstraints{}, &strv{"series '.*' not supported"}},
1341+ {true, &strv{"amd64"}, nil, nil, nil, nil, "trusty", defConstraints["bootstrap-trusty"], nil},
1342+ {false, &strv{"amd64"}, nil, nil, nil, nil, "trusty", defConstraints["trusty"], nil},
1343+ {true, &strv{"amd64"}, nil, nil, nil, nil, "", sigmaConstraints{}, &strv{"series '.*' not supported"}},
1344+ {false, &strv{"amd64"}, nil, nil, nil, nil, "", sigmaConstraints{}, &strv{"series '.*' not supported"}},
1345+ {true, nil, &uint64v{1}, nil, nil, nil, "trusty", defConstraints["bootstrap-trusty"], nil},
1346+ {false, nil, &uint64v{1}, nil, nil, nil, "trusty", defConstraints["trusty"], nil},
1347+ {false, nil, &uint64v{2}, nil, nil, nil, "trusty", defConstraints["trusty-c2-p4000"], nil},
1348+ {false, nil, &uint64v{2}, &uint64v{4000}, nil, nil, "trusty", defConstraints["trusty-c2-p4000"], nil},
1349+ {false, nil, nil, nil, &uint64v{2 * 1024}, nil, "trusty", defConstraints["trusty"], nil},
1350+ {false, nil, nil, nil, nil, &uint64v{5 * 1024}, "trusty", defConstraints["bootstrap-trusty"], nil},
1351+}
1352+
1353+func (s *constraintsSuite) TestConstraints(c *gc.C) {
1354+ for i, t := range newConstraintTests {
1355+ var cv constraints.Value
1356+ if t.arch != nil {
1357+ cv.Arch = &t.arch.v
1358+ }
1359+ if t.cores != nil {
1360+ cv.CpuCores = &t.cores.v
1361+ }
1362+ if t.power != nil {
1363+ cv.CpuPower = &t.power.v
1364+ }
1365+ if t.mem != nil {
1366+ cv.Mem = &t.mem.v
1367+ }
1368+ if t.disk != nil {
1369+ cv.RootDisk = &t.disk.v
1370+ }
1371+ v, err := newConstraints(t.bootstrap, cv, t.series)
1372+ if t.err == nil {
1373+ if !c.Check(*v, gc.Equals, t.expected) {
1374+ c.Logf("test (%d): %+v", i, t)
1375+ }
1376+ } else {
1377+ if !c.Check(err, gc.ErrorMatches, t.err.v) {
1378+ c.Logf("test (%d): %+v", i, t)
1379+ }
1380+ }
1381+ }
1382+}
1383+
1384+func (s *constraintsSuite) TestConstraintsArch(c *gc.C) {
1385+ var cv constraints.Value
1386+ var expected = sigmaConstraints{
1387+ driveTemplate: driveUbuntuTrusty64,
1388+ driveSize: 5 * gosigma.Gigabyte,
1389+ cores: 1,
1390+ power: 2000,
1391+ mem: 2 * gosigma.Gigabyte,
1392+ }
1393+
1394+ sc, err := newConstraints(true, cv, "trusty")
1395+ c.Check(err, gc.IsNil)
1396+ c.Check(*sc, gc.Equals, expected)
1397+
1398+ sc, err = newConstraints(true, cv, "")
1399+ c.Check(err, gc.ErrorMatches, "series '.*' not supported")
1400+ c.Check(sc, gc.IsNil)
1401+
1402+ arch := "amd64"
1403+ cv.Arch = &arch
1404+ sc, err = newConstraints(true, cv, "trusty")
1405+ c.Check(err, gc.IsNil)
1406+ c.Check(*sc, gc.Equals, expected)
1407+
1408+ sc, err = newConstraints(true, cv, "")
1409+ c.Check(err, gc.ErrorMatches, "series '.*' not supported")
1410+ c.Check(sc, gc.IsNil)
1411+
1412+ arch = ""
1413+ sc, err = newConstraints(true, cv, "")
1414+ c.Check(err, gc.ErrorMatches, "arch '.*' not supported")
1415+ c.Check(sc, gc.IsNil)
1416+}
1417
1418=== added file 'provider/cloudsigma/environ.go'
1419--- provider/cloudsigma/environ.go 1970-01-01 00:00:00 +0000
1420+++ provider/cloudsigma/environ.go 2014-06-09 11:10:33 +0000
1421@@ -0,0 +1,164 @@
1422+// Copyright 2014 Canonical Ltd.
1423+// Licensed under the AGPLv3, see LICENCE file for details.
1424+
1425+package cloudsigma
1426+
1427+import (
1428+ "sync"
1429+
1430+ "github.com/Altoros/gosigma"
1431+ "launchpad.net/juju-core/constraints"
1432+ "launchpad.net/juju-core/environs"
1433+ "launchpad.net/juju-core/environs/config"
1434+ "launchpad.net/juju-core/environs/simplestreams"
1435+ "launchpad.net/juju-core/environs/storage"
1436+ "launchpad.net/juju-core/environs/tools"
1437+ "launchpad.net/juju-core/provider/common"
1438+ "launchpad.net/juju-core/state"
1439+ "launchpad.net/juju-core/state/api"
1440+)
1441+
1442+// This file contains the core of the Environ implementation.
1443+type environ struct {
1444+ name string
1445+
1446+ lock sync.Mutex
1447+
1448+ ecfg *environConfig
1449+ client *environClient
1450+ storage *environStorage
1451+}
1452+
1453+var _ environs.Environ = (*environ)(nil)
1454+var _ tools.SupportsCustomSources = (*environ)(nil)
1455+var _ simplestreams.HasRegion = (*environ)(nil)
1456+
1457+// Name returns the Environ's name.
1458+func (env environ) Name() string {
1459+ return env.name
1460+}
1461+
1462+// Provider returns the EnvironProvider that created this Environ.
1463+func (environ) Provider() environs.EnvironProvider {
1464+ return providerInstance
1465+}
1466+
1467+// SetConfig updates the Environ's configuration.
1468+//
1469+// Calls to SetConfig do not affect the configuration of values previously obtained
1470+// from Storage.
1471+func (env *environ) SetConfig(cfg *config.Config) error {
1472+ env.lock.Lock()
1473+ defer env.lock.Unlock()
1474+
1475+ ecfg, err := validateConfig(cfg, env.ecfg)
1476+ if err != nil {
1477+ return err
1478+ }
1479+
1480+ if env.client == nil || env.client.configChanged(ecfg) {
1481+ client, err := newClient(ecfg)
1482+ if err != nil {
1483+ return err
1484+ }
1485+
1486+ storage, err := newStorage(ecfg, client)
1487+ if err != nil {
1488+ return err
1489+ }
1490+
1491+ env.client = client
1492+ env.storage = storage
1493+ }
1494+
1495+ env.ecfg = ecfg
1496+
1497+ return nil
1498+}
1499+
1500+// Config returns the configuration data with which the Environ was created.
1501+// Note that this is not necessarily current; the canonical location
1502+// for the configuration data is stored in the state.
1503+func (env environ) Config() *config.Config {
1504+ return env.ecfg.Config
1505+}
1506+
1507+// Storage returns storage specific to the environment.
1508+func (env environ) Storage() storage.Storage {
1509+ return env.storage
1510+}
1511+
1512+// Bootstrap initializes the state for the environment, possibly
1513+// starting one or more instances. If the configuration's
1514+// AdminSecret is non-empty, the administrator password on the
1515+// newly bootstrapped state will be set to a hash of it (see
1516+// utils.PasswordHash), When first connecting to the
1517+// environment via the juju package, the password hash will be
1518+// automatically replaced by the real password.
1519+//
1520+// The supplied constraints are used to choose the initial instance
1521+// specification, and will be stored in the new environment's state.
1522+//
1523+// Bootstrap is responsible for selecting the appropriate tools,
1524+// and setting the agent-version configuration attribute prior to
1525+// bootstrapping the environment.
1526+func (env *environ) Bootstrap(ctx environs.BootstrapContext, params environs.BootstrapParams) error {
1527+ // You can probably ignore this method; the common implementation should work.
1528+ return common.Bootstrap(ctx, env, params)
1529+}
1530+
1531+// StateInfo returns information on the state initialized by Bootstrap.
1532+func (env *environ) StateInfo() (*state.Info, *api.Info, error) {
1533+ // You can probably ignore this method; the common implementation should work.
1534+ return common.StateInfo(env)
1535+}
1536+
1537+// Destroy shuts down all known machines and destroys the
1538+// rest of the environment. Note that on some providers,
1539+// very recently started instances may not be destroyed
1540+// because they are not yet visible.
1541+//
1542+// When Destroy has been called, any Environ referring to the
1543+// same remote environment may become invalid
1544+func (env *environ) Destroy() error {
1545+ // You can probably ignore this method; the common implementation should work.
1546+ return common.Destroy(env)
1547+}
1548+
1549+// PrecheckInstance performs a preflight check on the specified
1550+// series and constraints, ensuring that they are possibly valid for
1551+// creating an instance in this environment.
1552+//
1553+// PrecheckInstance is best effort, and not guaranteed to eliminate
1554+// all invalid parameters. If PrecheckInstance returns nil, it is not
1555+// guaranteed that the constraints are valid; if a non-nil error is
1556+// returned, then the constraints are definitely invalid.
1557+func (env environ) PrecheckInstance(series string, cons constraints.Value, placement string) error {
1558+ logger.Infof("cloudsigma:environ:PrecheckInstance")
1559+ return nil
1560+}
1561+
1562+// GetToolsSources returns a list of sources which are
1563+// used to search for simplestreams tools metadata.
1564+func (env environ) GetToolsSources() ([]simplestreams.DataSource, error) {
1565+ // Add the simplestreams source off private storage.
1566+ return []simplestreams.DataSource{
1567+ storage.NewStorageSimpleStreamsDataSource("cloud storage", env.Storage(), storage.BaseToolsPath),
1568+ }, nil
1569+}
1570+
1571+// Region is specified in the HasRegion interface.
1572+func (env environ) Region() (simplestreams.CloudSpec, error) {
1573+ return env.cloudSpec(env.ecfg.region())
1574+}
1575+
1576+func (env environ) cloudSpec(region string) (simplestreams.CloudSpec, error) {
1577+ endpoint, err := gosigma.ResolveEndpoint(region)
1578+ if err != nil {
1579+ return simplestreams.CloudSpec{}, err
1580+ }
1581+ return simplestreams.CloudSpec{
1582+ Region: region,
1583+ Endpoint: endpoint,
1584+ }, nil
1585+}
1586
1587=== added file 'provider/cloudsigma/environ_test.go'
1588--- provider/cloudsigma/environ_test.go 1970-01-01 00:00:00 +0000
1589+++ provider/cloudsigma/environ_test.go 2014-06-09 11:10:33 +0000
1590@@ -0,0 +1,96 @@
1591+// Copyright 2014 Canonical Ltd.
1592+// Licensed under the AGPLv3, see LICENCE file for details.
1593+
1594+package cloudsigma
1595+
1596+import (
1597+ gc "launchpad.net/gocheck"
1598+ "launchpad.net/juju-core/constraints"
1599+ "launchpad.net/juju-core/environs"
1600+ "launchpad.net/juju-core/environs/simplestreams"
1601+ "launchpad.net/juju-core/environs/tools"
1602+ "launchpad.net/juju-core/juju/arch"
1603+ "launchpad.net/juju-core/testing"
1604+)
1605+
1606+type environSuite struct {
1607+ testing.BaseSuite
1608+}
1609+
1610+var _ = gc.Suite(&environSuite{})
1611+
1612+func (s *environSuite) SetUpSuite(c *gc.C) {
1613+ s.BaseSuite.SetUpSuite(c)
1614+}
1615+
1616+func (s *environSuite) TearDownSuite(c *gc.C) {
1617+ s.BaseSuite.TearDownSuite(c)
1618+}
1619+
1620+func (s *environSuite) SetUpTest(c *gc.C) {
1621+ s.BaseSuite.SetUpTest(c)
1622+}
1623+
1624+func (s *environSuite) TearDownTest(c *gc.C) {
1625+ s.BaseSuite.TearDownTest(c)
1626+}
1627+
1628+func (s *environSuite) TestBase(c *gc.C) {
1629+ var emptyStorage environStorage
1630+
1631+ s.PatchValue(&newClient, func(*environConfig) (*environClient, error) {
1632+ return nil, nil
1633+ })
1634+ s.PatchValue(&newStorage, func(*environConfig, *environClient) (*environStorage, error) {
1635+ return &emptyStorage, nil
1636+ })
1637+
1638+ baseConfig := newConfig(c, validAttrs().Merge(testing.Attrs{"name": "testname"}))
1639+ environ, err := environs.New(baseConfig)
1640+ c.Assert(err, gc.IsNil)
1641+
1642+ cfg := environ.Config()
1643+ c.Assert(cfg, gc.NotNil)
1644+ c.Check(cfg.Name(), gc.Equals, "testname")
1645+
1646+ c.Check(environ.Storage(), gc.Equals, &emptyStorage)
1647+
1648+ c.Check(environ.PrecheckInstance("", constraints.Value{}, ""), gc.IsNil)
1649+
1650+ customSource, ok := environ.(tools.SupportsCustomSources)
1651+ c.Check(ok, gc.Equals, true)
1652+ c.Assert(customSource, gc.NotNil)
1653+
1654+ src, err := customSource.GetToolsSources()
1655+ c.Check(src, gc.NotNil)
1656+ c.Check(err, gc.IsNil)
1657+
1658+ hasRegion, ok := environ.(simplestreams.HasRegion)
1659+ c.Check(ok, gc.Equals, true)
1660+ c.Assert(hasRegion, gc.NotNil)
1661+
1662+ cloudSpec, err := hasRegion.Region()
1663+ c.Check(err, gc.IsNil)
1664+ c.Check(cloudSpec.Region, gc.Not(gc.Equals), "")
1665+ c.Check(cloudSpec.Endpoint, gc.Not(gc.Equals), "")
1666+
1667+ archs, err := environ.SupportedArchitectures()
1668+ c.Check(err, gc.IsNil)
1669+ c.Assert(archs, gc.NotNil)
1670+ c.Assert(archs, gc.HasLen, 1)
1671+ c.Check(archs[0], gc.Equals, arch.AMD64)
1672+
1673+ validator, err := environ.ConstraintsValidator()
1674+ c.Check(validator, gc.NotNil)
1675+ c.Check(err, gc.IsNil)
1676+
1677+ c.Check(environ.SupportNetworks(), gc.Equals, false)
1678+ c.Check(environ.SupportsUnitPlacement(), gc.IsNil)
1679+
1680+ c.Check(environ.OpenPorts(nil), gc.IsNil)
1681+ c.Check(environ.ClosePorts(nil), gc.IsNil)
1682+
1683+ ports, err := environ.Ports()
1684+ c.Check(ports, gc.IsNil)
1685+ c.Check(err, gc.IsNil)
1686+}
1687
1688=== added file 'provider/cloudsigma/environcaps.go'
1689--- provider/cloudsigma/environcaps.go 1970-01-01 00:00:00 +0000
1690+++ provider/cloudsigma/environcaps.go 2014-06-09 11:10:33 +0000
1691@@ -0,0 +1,50 @@
1692+// Copyright 2014 Canonical Ltd.
1693+// Licensed under the AGPLv3, see LICENCE file for details.
1694+
1695+package cloudsigma
1696+
1697+import (
1698+ "launchpad.net/juju-core/constraints"
1699+ "launchpad.net/juju-core/juju/arch"
1700+)
1701+
1702+// SupportedArchitectures returns the image architectures which can
1703+// be hosted by this environment.
1704+func (env *environ) SupportedArchitectures() ([]string, error) {
1705+ return []string{arch.AMD64}, nil
1706+}
1707+
1708+var unsupportedConstraints = []string{
1709+ constraints.Container,
1710+ constraints.InstanceType,
1711+ constraints.Tags,
1712+}
1713+
1714+// ConstraintsValidator returns a Validator instance which
1715+// is used to validate and merge constraints.
1716+func (env *environ) ConstraintsValidator() (constraints.Validator, error) {
1717+ validator := constraints.NewValidator()
1718+ validator.RegisterUnsupported(unsupportedConstraints)
1719+ supportedArches, err := env.SupportedArchitectures()
1720+ if err != nil {
1721+ return nil, err
1722+ }
1723+ validator.RegisterVocabulary(constraints.Arch, supportedArches)
1724+ return validator, nil
1725+}
1726+
1727+// SupportNetworks returns whether the environment has support to
1728+// specify networks for services and machines.
1729+func (env *environ) SupportNetworks() bool {
1730+ logger.Debugf("environ:SupportedNetworks")
1731+ return false
1732+}
1733+
1734+// SupportsUnitAssignment returns an error which, if non-nil, indicates
1735+// that the environment does not support unit placement. If the environment
1736+// does not support unit placement, then machines may not be created
1737+// without units, and units cannot be placed explcitly.
1738+func (env *environ) SupportsUnitPlacement() error {
1739+ logger.Debugf("cloudsigma:environ:SupportsUnitPlacement not implemented")
1740+ return nil
1741+}
1742
1743=== added file 'provider/cloudsigma/environfirewall.go'
1744--- provider/cloudsigma/environfirewall.go 1970-01-01 00:00:00 +0000
1745+++ provider/cloudsigma/environfirewall.go 2014-06-09 11:10:33 +0000
1746@@ -0,0 +1,32 @@
1747+// Copyright 2014 Canonical Ltd.
1748+// Licensed under the AGPLv3, see LICENCE file for details.
1749+
1750+package cloudsigma
1751+
1752+import (
1753+ "launchpad.net/juju-core/instance"
1754+)
1755+
1756+// Implementing the methods below (to do something other than return nil) will
1757+// cause `juju expose` to work when the firewall-mode is "global". If you
1758+// implement one of them, you should implement them all.
1759+
1760+// OpenPorts opens the given ports for the whole environment.
1761+// Must only be used if the environment was setup with the FwGlobal firewall mode.
1762+func (env environ) OpenPorts(ports []instance.Port) error {
1763+ logger.Warningf("pretending to open ports %v for all instances", ports)
1764+ return nil
1765+}
1766+
1767+// ClosePorts closes the given ports for the whole environment.
1768+// Must only be used if the environment was setup with the FwGlobal firewall mode.
1769+func (env environ) ClosePorts(ports []instance.Port) error {
1770+ logger.Warningf("pretending to close ports %v for all instances", ports)
1771+ return nil
1772+}
1773+
1774+// Ports returns the ports opened for the whole environment.
1775+// Must only be used if the environment was setup with the FwGlobal firewall mode.
1776+func (env environ) Ports() ([]instance.Port, error) {
1777+ return nil, nil
1778+}
1779
1780=== added file 'provider/cloudsigma/environinstance.go'
1781--- provider/cloudsigma/environinstance.go 1970-01-01 00:00:00 +0000
1782+++ provider/cloudsigma/environinstance.go 2014-06-09 11:10:33 +0000
1783@@ -0,0 +1,291 @@
1784+// Copyright 2014 Canonical Ltd.
1785+// Licensed under the AGPLv3, see LICENCE file for details.
1786+
1787+package cloudsigma
1788+
1789+import (
1790+ "fmt"
1791+ "os"
1792+ "path"
1793+ "strings"
1794+ "time"
1795+
1796+ "github.com/Altoros/gosigma"
1797+ "github.com/juju/errors"
1798+ "github.com/juju/loggo"
1799+ "launchpad.net/juju-core/agent"
1800+ coreCloudinit "launchpad.net/juju-core/cloudinit"
1801+ "launchpad.net/juju-core/cloudinit/sshinit"
1802+ "launchpad.net/juju-core/environs"
1803+ "launchpad.net/juju-core/environs/cloudinit"
1804+ "launchpad.net/juju-core/environs/network"
1805+ "launchpad.net/juju-core/instance"
1806+ "launchpad.net/juju-core/juju/arch"
1807+ "launchpad.net/juju-core/utils"
1808+ "launchpad.net/juju-core/utils/ssh"
1809+ "launchpad.net/juju-core/worker/localstorage"
1810+)
1811+
1812+//
1813+// Imlementation of InstanceBroker: methods for starting and stopping instances.
1814+//
1815+
1816+// StartInstance asks for a new instance to be created, associated with
1817+// the provided config in machineConfig. The given config describes the juju
1818+// state for the new instance to connect to. The config MachineNonce, which must be
1819+// unique within an environment, is used by juju to protect against the
1820+// consequences of multiple instances being started with the same machine id.
1821+func (env environ) StartInstance(args environs.StartInstanceParams) (
1822+ instance.Instance, *instance.HardwareCharacteristics, []network.Info, error) {
1823+ logger.Infof("sigmaEnviron.StartInstance...")
1824+
1825+ if args.MachineConfig == nil {
1826+ return nil, nil, nil, fmt.Errorf("machine configuration is nil")
1827+ }
1828+
1829+ if args.MachineConfig.HasNetworks() {
1830+ return nil, nil, nil, fmt.Errorf("starting instances with networks is not supported yet")
1831+ }
1832+
1833+ if len(args.Tools) == 0 {
1834+ return nil, nil, nil, fmt.Errorf("tools not found")
1835+ }
1836+
1837+ args.MachineConfig.Tools = args.Tools[0]
1838+ if err := environs.FinishMachineConfig(args.MachineConfig, env.Config(), args.Constraints); err != nil {
1839+ return nil, nil, nil, err
1840+ }
1841+
1842+ client := env.client
1843+ server, rootdrive, err := client.newInstance(args)
1844+ if err != nil {
1845+ return nil, nil, nil, fmt.Errorf("failed start instance: %v", err)
1846+ }
1847+
1848+ inst := sigmaInstance{server: server}
1849+ addr := inst.findIPv4()
1850+ if addr == "" {
1851+ return nil, nil, nil, fmt.Errorf("failed obtain instance IP address")
1852+ }
1853+
1854+ // prepare new instance: wait up and running, populate nonce file, etc
1855+ if err := env.prepareInstance(addr, args.MachineConfig); err != nil {
1856+ return nil, nil, nil, fmt.Errorf("failed prepare instanse: %v", err)
1857+ }
1858+
1859+ // provide additional agent config for localstorage, if any
1860+ if env.storage.tmp && args.MachineConfig.Bootstrap {
1861+ if err := env.prepareStorage(addr, args.MachineConfig); err != nil {
1862+ return nil, nil, nil, fmt.Errorf("failed prepare storage: %v", err)
1863+ }
1864+ }
1865+
1866+ // prepare hardware characteristics
1867+ hwch := inst.hardware()
1868+
1869+ // populate root drive hardware characteristics
1870+ switch rootdrive.Arch() {
1871+ case "64":
1872+ var a = arch.AMD64
1873+ hwch.Arch = &a
1874+ }
1875+
1876+ diskSpace := rootdrive.Size() / gosigma.Megabyte
1877+ if diskSpace > 0 {
1878+ hwch.RootDisk = &diskSpace
1879+ }
1880+
1881+ logger.Tracef("hardware: %v", hwch)
1882+
1883+ return inst, hwch, nil, nil
1884+}
1885+
1886+// AllInstances returns all instances currently known to the broker.
1887+func (env environ) AllInstances() ([]instance.Instance, error) {
1888+ // Please note that this must *not* return instances that have not been
1889+ // allocated as part of this environment -- if it does, juju will see they
1890+ // are not tracked in state, assume they're stale/rogue, and shut them down.
1891+
1892+ logger.Tracef("environ.AllInstances...")
1893+
1894+ servers, err := env.client.instances()
1895+ if err != nil {
1896+ logger.Tracef("environ.AllInstances failed: %v", err)
1897+ return nil, err
1898+ }
1899+
1900+ instances := make([]instance.Instance, 0, len(servers))
1901+ for _, server := range servers {
1902+ instance := sigmaInstance{server: server}
1903+ instances = append(instances, instance)
1904+ }
1905+
1906+ if logger.LogLevel() <= loggo.TRACE {
1907+ logger.Tracef("All instances, len = %d:", len(instances))
1908+ for _, instance := range instances {
1909+ logger.Tracef("... id: %q, status: %q", instance.Id(), instance.Status())
1910+ }
1911+ }
1912+
1913+ return instances, nil
1914+}
1915+
1916+// Instances returns a slice of instances corresponding to the
1917+// given instance ids. If no instances were found, but there
1918+// was no other error, it will return ErrNoInstances. If
1919+// some but not all the instances were found, the returned slice
1920+// will have some nil slots, and an ErrPartialInstances error
1921+// will be returned.
1922+func (env environ) Instances(ids []instance.Id) ([]instance.Instance, error) {
1923+ logger.Tracef("environ.Instances %#v", ids)
1924+ // Please note that this must *not* return instances that have not been
1925+ // allocated as part of this environment -- if it does, juju will see they
1926+ // are not tracked in state, assume they're stale/rogue, and shut them down.
1927+ // This advice applies even if an instance id passed in corresponds to a
1928+ // real instance that's not part of the environment -- the Environ should
1929+ // treat that no differently to a request for one that does not exist.
1930+
1931+ m, err := env.client.instanceMap()
1932+ if err != nil {
1933+ logger.Tracef("environ.Instances failed: %v", err)
1934+ return nil, err
1935+ }
1936+
1937+ var found int
1938+ r := make([]instance.Instance, len(ids))
1939+ for i, id := range ids {
1940+ if s, ok := m[string(id)]; ok {
1941+ r[i] = sigmaInstance{server: s}
1942+ found++
1943+ }
1944+ }
1945+
1946+ if found == 0 {
1947+ err = environs.ErrNoInstances
1948+ } else if found != len(ids) {
1949+ err = environs.ErrPartialInstances
1950+ }
1951+
1952+ return r, err
1953+}
1954+
1955+// StopInstances shuts down the given instances.
1956+func (env environ) StopInstances(instances ...instance.Id) error {
1957+ logger.Infof("stop instances %+v", instances)
1958+
1959+ var err error
1960+
1961+ for _, instance := range instances {
1962+ if e := env.client.stopInstance(instance); e != nil {
1963+ err = e
1964+ }
1965+ }
1966+
1967+ return err
1968+}
1969+
1970+func (env environ) prepareInstance(addr string, mcfg *cloudinit.MachineConfig) error {
1971+ host := "ubuntu@" + addr
1972+ logger.Debugf("Running prepare script on %s", host)
1973+
1974+ cmds := []string{"#!/bin/bash", "set -e"}
1975+ cmds = append(cmds, fmt.Sprintf("mkdir -p '%s'", mcfg.DataDir))
1976+
1977+ nonceFile := path.Join(mcfg.DataDir, cloudinit.NonceFile)
1978+ nonceContent := mcfg.MachineNonce
1979+ cmds = append(cmds, fmt.Sprintf("echo '%s' > %s", nonceContent, nonceFile))
1980+
1981+ script := strings.Join(cmds, "\n")
1982+
1983+ if !mcfg.Bootstrap {
1984+ cloudcfg := coreCloudinit.New()
1985+ if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil {
1986+ return err
1987+ }
1988+ configScript, err := sshinit.ConfigureScript(cloudcfg)
1989+ if err != nil {
1990+ return err
1991+ }
1992+
1993+ script += "\n\n"
1994+ script += configScript
1995+ }
1996+
1997+ logger.Tracef("Script:\n%s", script)
1998+
1999+ keyfile := path.Join(mcfg.DataDir, agent.SystemIdentity)
2000+ if _, err := os.Stat(keyfile); err != nil {
2001+ keyfile = ""
2002+ }
2003+ logger.Tracef("System identity file: %q", keyfile)
2004+
2005+ var retryAttempts = utils.AttemptStrategy{
2006+ Total: 120 * time.Second,
2007+ Delay: 300 * time.Millisecond,
2008+ }
2009+
2010+ var err error
2011+ for attempt := retryAttempts.Start(); attempt.Next(); {
2012+ var options ssh.Options
2013+ if keyfile != "" {
2014+ options.SetIdentities(keyfile)
2015+ }
2016+
2017+ cmd := ssh.Command(host, []string{"sudo", "/bin/bash"}, &options)
2018+ cmd.Stdin = strings.NewReader(script)
2019+
2020+ var logw = loggerWriter{logger.LogLevel()}
2021+ cmd.Stderr = &logw
2022+ cmd.Stdout = &logw
2023+
2024+ if err = cmd.Run(); err == nil {
2025+ break
2026+ }
2027+ }
2028+
2029+ if err != nil {
2030+ return fmt.Errorf("failed running prepare script: %v", err)
2031+ }
2032+
2033+ return nil
2034+}
2035+
2036+func (env environ) prepareStorage(addr string, mcfg *cloudinit.MachineConfig) error {
2037+ storagePort := env.ecfg.storagePort()
2038+ storageDir := mcfg.DataDir + "/" + storageSubdir
2039+
2040+ logger.Debugf("Moving local temporary storage to %s:%d (%s)...", addr, storagePort, storageDir)
2041+ if err := env.storage.MoveToSSH("ubuntu", addr); err != nil {
2042+ return err
2043+ }
2044+
2045+ if strings.Contains(mcfg.Tools.URL, "%s") {
2046+ mcfg.Tools.URL = fmt.Sprintf(mcfg.Tools.URL, "file://"+storageDir)
2047+ logger.Tracef("Tools URL patched to %q", mcfg.Tools.URL)
2048+ }
2049+
2050+ // prepare configuration for local storage at bootstrap host
2051+ storageConfig := storageConfig{
2052+ ecfg: env.ecfg,
2053+ storageDir: storageDir,
2054+ storageAddr: addr,
2055+ storagePort: storagePort,
2056+ }
2057+
2058+ agentEnv, err := localstorage.StoreConfig(&storageConfig)
2059+ if err != nil {
2060+ return err
2061+ }
2062+
2063+ for k, v := range agentEnv {
2064+ mcfg.AgentEnvironment[k] = v
2065+ }
2066+
2067+ return nil
2068+}
2069+
2070+// AllocateAddress requests a new address to be allocated for the
2071+// given instance on the given network.
2072+func (env environ) AllocateAddress(instID instance.Id, netID network.Id) (instance.Address, error) {
2073+ return instance.Address{}, errors.NotSupportedf("AllocateAddress")
2074+}
2075
2076=== added file 'provider/cloudsigma/environinstance_test.go'
2077--- provider/cloudsigma/environinstance_test.go 1970-01-01 00:00:00 +0000
2078+++ provider/cloudsigma/environinstance_test.go 2014-06-09 11:10:33 +0000
2079@@ -0,0 +1,206 @@
2080+// Copyright 2014 Canonical Ltd.
2081+// Licensed under the AGPLv3, see LICENCE file for details.
2082+
2083+package cloudsigma
2084+
2085+import (
2086+ "time"
2087+
2088+ "github.com/Altoros/gosigma/mock"
2089+ "github.com/juju/loggo"
2090+ gc "launchpad.net/gocheck"
2091+ "launchpad.net/juju-core/environs"
2092+ "launchpad.net/juju-core/environs/cloudinit"
2093+ "launchpad.net/juju-core/environs/config"
2094+ "launchpad.net/juju-core/environs/network"
2095+ "launchpad.net/juju-core/instance"
2096+ "launchpad.net/juju-core/state/api"
2097+ "launchpad.net/juju-core/testing"
2098+ "launchpad.net/juju-core/tools"
2099+)
2100+
2101+type environInstanceSuite struct {
2102+ testing.BaseSuite
2103+ baseConfig *config.Config
2104+}
2105+
2106+var _ = gc.Suite(&environInstanceSuite{})
2107+
2108+func (s *environInstanceSuite) SetUpSuite(c *gc.C) {
2109+ s.BaseSuite.SetUpSuite(c)
2110+
2111+ mock.Start()
2112+
2113+ attrs := testing.Attrs{
2114+ "name": "testname",
2115+ "region": mock.Endpoint(""),
2116+ "username": mock.TestUser,
2117+ "password": mock.TestPassword,
2118+ }
2119+ s.baseConfig = newConfig(c, validAttrs().Merge(attrs))
2120+}
2121+
2122+func (s *environInstanceSuite) TearDownSuite(c *gc.C) {
2123+ mock.Stop()
2124+ s.BaseSuite.TearDownSuite(c)
2125+}
2126+
2127+func (s *environInstanceSuite) SetUpTest(c *gc.C) {
2128+ s.BaseSuite.SetUpTest(c)
2129+
2130+ ll := logger.LogLevel()
2131+ logger.SetLogLevel(loggo.TRACE)
2132+ s.AddCleanup(func(*gc.C) { logger.SetLogLevel(ll) })
2133+
2134+ mock.Reset()
2135+}
2136+
2137+func (s *environInstanceSuite) TearDownTest(c *gc.C) {
2138+ mock.Reset()
2139+ s.BaseSuite.TearDownTest(c)
2140+}
2141+
2142+func (s *environInstanceSuite) createEnviron(c *gc.C, cfg *config.Config) environs.Environ {
2143+ var emptyStorage environStorage
2144+ s.PatchValue(&newStorage, func(*environConfig, *environClient) (*environStorage, error) {
2145+ return &emptyStorage, nil
2146+ })
2147+ if cfg == nil {
2148+ cfg = s.baseConfig
2149+ }
2150+ environ, err := environs.New(cfg)
2151+ c.Assert(environ, gc.NotNil)
2152+ c.Assert(err, gc.IsNil)
2153+ return environ
2154+}
2155+
2156+func (s *environInstanceSuite) TestInstances(c *gc.C) {
2157+ environ := s.createEnviron(c, nil)
2158+
2159+ instances, err := environ.AllInstances()
2160+ c.Assert(instances, gc.NotNil)
2161+ c.Assert(err, gc.IsNil)
2162+ c.Check(instances, gc.HasLen, 0)
2163+
2164+ uuid0 := addTestClientServer(c, jujuMetaInstanceServer, "testname", "1.1.1.1")
2165+ uuid1 := addTestClientServer(c, jujuMetaInstanceStateServer, "testname", "2.2.2.2")
2166+ addTestClientServer(c, jujuMetaInstanceServer, "other-env", "0.1.1.1")
2167+ addTestClientServer(c, jujuMetaInstanceStateServer, "other-env", "0.2.2.2")
2168+
2169+ instances, err = environ.AllInstances()
2170+ c.Assert(instances, gc.NotNil)
2171+ c.Assert(err, gc.IsNil)
2172+ c.Check(instances, gc.HasLen, 2)
2173+
2174+ ids := []instance.Id{instance.Id(uuid0), instance.Id(uuid1)}
2175+ instances, err = environ.Instances(ids)
2176+ c.Assert(instances, gc.NotNil)
2177+ c.Assert(err, gc.IsNil)
2178+ c.Check(instances, gc.HasLen, 2)
2179+
2180+ ids = append(ids, instance.Id("fake-instance"))
2181+ instances, err = environ.Instances(ids)
2182+ c.Assert(instances, gc.NotNil)
2183+ c.Assert(err, gc.Equals, environs.ErrPartialInstances)
2184+ c.Check(instances, gc.HasLen, 3)
2185+ c.Check(instances[0], gc.NotNil)
2186+ c.Check(instances[1], gc.NotNil)
2187+ c.Check(instances[2], gc.IsNil)
2188+
2189+ err = environ.StopInstances(ids...)
2190+ c.Assert(err, gc.ErrorMatches, "404 Not Found.*")
2191+
2192+ instances, err = environ.Instances(ids)
2193+ c.Assert(instances, gc.NotNil)
2194+ c.Assert(err, gc.Equals, environs.ErrNoInstances)
2195+ c.Check(instances, gc.HasLen, 3)
2196+ c.Check(instances[0], gc.IsNil)
2197+ c.Check(instances[1], gc.IsNil)
2198+ c.Check(instances[2], gc.IsNil)
2199+}
2200+
2201+func (s *environInstanceSuite) TestInstancesFail(c *gc.C) {
2202+ attrs := testing.Attrs{
2203+ "name": "testname",
2204+ "region": "https://0.1.2.3:2000/api/2.0/",
2205+ "username": mock.TestUser,
2206+ "password": mock.TestPassword,
2207+ }
2208+ baseConfig := newConfig(c, validAttrs().Merge(attrs))
2209+
2210+ newClientFunc := newClient
2211+ s.PatchValue(&newClient, func(cfg *environConfig) (*environClient, error) {
2212+ cli, err := newClientFunc(cfg)
2213+ if cli != nil {
2214+ cli.conn.ConnectTimeout(10 * time.Millisecond)
2215+ }
2216+ return cli, err
2217+ })
2218+
2219+ environ := s.createEnviron(c, baseConfig)
2220+
2221+ instances, err := environ.AllInstances()
2222+ c.Assert(instances, gc.IsNil)
2223+ c.Assert(err, gc.NotNil)
2224+
2225+ instances, err = environ.Instances([]instance.Id{instance.Id("123"), instance.Id("321")})
2226+ c.Assert(instances, gc.IsNil)
2227+ c.Assert(err, gc.NotNil)
2228+}
2229+
2230+func (s *environInstanceSuite) TestAllocateAddress(c *gc.C) {
2231+ environ := s.createEnviron(c, nil)
2232+
2233+ addr, err := environ.AllocateAddress(instance.Id(""), network.Id(""))
2234+ c.Check(addr, gc.Equals, instance.Address{})
2235+ c.Check(err, gc.ErrorMatches, "AllocateAddress not supported")
2236+}
2237+
2238+func (s *environInstanceSuite) TestStartInstanceError(c *gc.C) {
2239+ environ := s.createEnviron(c, nil)
2240+
2241+ inst, hw, ni, err := environ.StartInstance(environs.StartInstanceParams{})
2242+ c.Check(inst, gc.IsNil)
2243+ c.Check(hw, gc.IsNil)
2244+ c.Check(ni, gc.IsNil)
2245+ c.Check(err, gc.ErrorMatches, "machine configuration is nil")
2246+
2247+ inst, hw, ni, err = environ.StartInstance(environs.StartInstanceParams{
2248+ MachineConfig: &cloudinit.MachineConfig{
2249+ IncludeNetworks: []string{"value"},
2250+ },
2251+ })
2252+ c.Check(inst, gc.IsNil)
2253+ c.Check(hw, gc.IsNil)
2254+ c.Check(ni, gc.IsNil)
2255+ c.Check(err, gc.ErrorMatches, "starting instances with networks is not supported yet")
2256+
2257+ inst, hw, ni, err = environ.StartInstance(environs.StartInstanceParams{
2258+ MachineConfig: &cloudinit.MachineConfig{},
2259+ })
2260+ c.Check(inst, gc.IsNil)
2261+ c.Check(hw, gc.IsNil)
2262+ c.Check(ni, gc.IsNil)
2263+ c.Check(err, gc.ErrorMatches, "tools not found")
2264+
2265+ inst, hw, ni, err = environ.StartInstance(environs.StartInstanceParams{
2266+ Tools: tools.List{&tools.Tools{}},
2267+ MachineConfig: &cloudinit.MachineConfig{
2268+ Bootstrap: true,
2269+ APIInfo: &api.Info{},
2270+ },
2271+ })
2272+ c.Check(inst, gc.IsNil)
2273+ c.Check(hw, gc.IsNil)
2274+ c.Check(ni, gc.IsNil)
2275+ c.Check(err, gc.ErrorMatches, "cannot complete machine configuration:.*")
2276+
2277+ inst, hw, ni, err = environ.StartInstance(environs.StartInstanceParams{
2278+ Tools: tools.List{&tools.Tools{}},
2279+ MachineConfig: &cloudinit.MachineConfig{},
2280+ })
2281+ c.Check(inst, gc.IsNil)
2282+ c.Check(hw, gc.IsNil)
2283+ c.Check(ni, gc.IsNil)
2284+ c.Check(err, gc.ErrorMatches, "failed start instance:.*")
2285+}
2286
2287=== added file 'provider/cloudsigma/instance.go'
2288--- provider/cloudsigma/instance.go 1970-01-01 00:00:00 +0000
2289+++ provider/cloudsigma/instance.go 2014-06-09 11:10:33 +0000
2290@@ -0,0 +1,148 @@
2291+// Copyright 2014 Canonical Ltd.
2292+// Licensed under the AGPLv3, see LICENCE file for details.
2293+
2294+package cloudsigma
2295+
2296+import (
2297+ "fmt"
2298+
2299+ "github.com/Altoros/gosigma"
2300+ "launchpad.net/juju-core/instance"
2301+ "launchpad.net/juju-core/provider/common"
2302+)
2303+
2304+type sigmaInstance struct {
2305+ server gosigma.Server
2306+}
2307+
2308+// Id returns a provider-generated identifier for the Instance.
2309+func (i sigmaInstance) Id() instance.Id {
2310+ if i.server == nil {
2311+ return instance.Id("")
2312+ }
2313+ id := instance.Id(i.server.UUID())
2314+ logger.Tracef("sigmaInstance.Id: %s", id)
2315+ return id
2316+}
2317+
2318+// Status returns the provider-specific status for the instance.
2319+func (i sigmaInstance) Status() string {
2320+ if i.server == nil {
2321+ return ""
2322+ }
2323+ status := i.server.Status()
2324+ logger.Tracef("sigmaInstance.Status: %s", status)
2325+ return status
2326+}
2327+
2328+// Refresh refreshes local knowledge of the instance from the provider.
2329+func (i sigmaInstance) Refresh() error {
2330+ if i.server == nil {
2331+ return fmt.Errorf("invalid instance")
2332+ }
2333+ err := i.server.Refresh()
2334+ logger.Tracef("sigmaInstance.Refresh: %s", err)
2335+ return err
2336+}
2337+
2338+// Addresses returns a list of hostnames or ip addresses
2339+// associated with the instance. This will supercede DNSName
2340+// which can be implemented by selecting a preferred address.
2341+func (i sigmaInstance) Addresses() ([]instance.Address, error) {
2342+
2343+ ip := i.findIPv4()
2344+
2345+ if ip == "" {
2346+ logger.Tracef("sigmaInstance.Addresses: IPv4 address not found")
2347+ return nil, instance.ErrNoDNSName
2348+ }
2349+
2350+ addr := instance.Address{
2351+ Value: ip,
2352+ Type: instance.Ipv4Address,
2353+ NetworkScope: instance.NetworkPublic,
2354+ }
2355+
2356+ logger.Tracef("sigmaInstance.Addresses: %v", addr)
2357+
2358+ return []instance.Address{addr}, nil
2359+}
2360+
2361+// DNSName returns the DNS name for the instance.
2362+// If the name is not yet allocated, it will return
2363+// an ErrNoDNSName error.
2364+func (i sigmaInstance) DNSName() (string, error) {
2365+ ip := i.findIPv4()
2366+
2367+ if ip == "" {
2368+ logger.Tracef("sigmaInstance.DNSName: IPv4 address not found, refreshing...")
2369+ if err := i.Refresh(); err != nil {
2370+ return "", err
2371+ }
2372+
2373+ ip = i.findIPv4()
2374+ if ip == "" {
2375+ return "", instance.ErrNoDNSName
2376+ }
2377+ }
2378+
2379+ logger.Infof("sigmaInstance.DNSName: %s", ip)
2380+
2381+ return ip, nil
2382+}
2383+
2384+// WaitDNSName returns the DNS name for the instance,
2385+// waiting until it is allocated if necessary.
2386+// TODO: We may not need this in the interface any more. All
2387+// implementations now delegate to environs.WaitDNSName.
2388+func (i sigmaInstance) WaitDNSName() (string, error) {
2389+ return common.WaitDNSName(i)
2390+}
2391+
2392+// OpenPorts opens the given ports on the instance, which
2393+// should have been started with the given machine id.
2394+func (i sigmaInstance) OpenPorts(machineID string, ports []instance.Port) error {
2395+ logger.Tracef("sigmaInstance.OpenPorts: not implemented")
2396+ return nil
2397+}
2398+
2399+// ClosePorts closes the given ports on the instance, which
2400+// should have been started with the given machine id.
2401+func (i sigmaInstance) ClosePorts(machineID string, ports []instance.Port) error {
2402+ logger.Tracef("sigmaInstance.ClosePorts: not implemented")
2403+ return nil
2404+}
2405+
2406+// Ports returns the set of ports open on the instance, which
2407+// should have been started with the given machine id.
2408+// The ports are returned as sorted by SortPorts.
2409+func (i sigmaInstance) Ports(machineID string) ([]instance.Port, error) {
2410+ logger.Tracef("sigmaInstance.Ports: not implemented")
2411+ return []instance.Port{}, nil
2412+}
2413+
2414+func (i sigmaInstance) findIPv4() string {
2415+ if i.server == nil {
2416+ return ""
2417+ }
2418+ addrs := i.server.IPv4()
2419+ if len(addrs) == 0 {
2420+ return ""
2421+ }
2422+ return addrs[0]
2423+}
2424+
2425+func (i sigmaInstance) hardware() *instance.HardwareCharacteristics {
2426+ if i.server == nil {
2427+ return nil
2428+ }
2429+ memory := i.server.Mem() / gosigma.Megabyte
2430+ cores := uint64(i.server.SMP())
2431+ cpu := i.server.CPU()
2432+ hw := instance.HardwareCharacteristics{
2433+ Mem: &memory,
2434+ CpuCores: &cores,
2435+ CpuPower: &cpu,
2436+ }
2437+ return &hw
2438+}
2439
2440=== added file 'provider/cloudsigma/instance_test.go'
2441--- provider/cloudsigma/instance_test.go 1970-01-01 00:00:00 +0000
2442+++ provider/cloudsigma/instance_test.go 2014-06-09 11:10:33 +0000
2443@@ -0,0 +1,328 @@
2444+// Copyright 2014 Canonical Ltd.
2445+// Licensed under the AGPLv3, see LICENCE file for details.
2446+
2447+package cloudsigma
2448+
2449+import (
2450+ "strings"
2451+
2452+ "github.com/Altoros/gosigma"
2453+ "github.com/Altoros/gosigma/data"
2454+ "github.com/Altoros/gosigma/mock"
2455+ gc "launchpad.net/gocheck"
2456+ "launchpad.net/juju-core/instance"
2457+ "launchpad.net/juju-core/testing"
2458+)
2459+
2460+type instanceSuite struct {
2461+ testing.BaseSuite
2462+ inst *sigmaInstance
2463+ instWithoutIP *sigmaInstance
2464+}
2465+
2466+var _ = gc.Suite(&instanceSuite{})
2467+
2468+func (s *instanceSuite) SetUpSuite(c *gc.C) {
2469+ s.BaseSuite.SetUpSuite(c)
2470+ mock.Start()
2471+}
2472+
2473+func (s *instanceSuite) TearDownSuite(c *gc.C) {
2474+ mock.Stop()
2475+ s.BaseSuite.TearDownSuite(c)
2476+}
2477+
2478+func (s *instanceSuite) SetUpTest(c *gc.C) {
2479+ s.BaseSuite.SetUpTest(c)
2480+
2481+ cli, err := gosigma.NewClient(mock.Endpoint(""), mock.TestUser, mock.TestPassword, nil)
2482+ c.Assert(err, gc.IsNil)
2483+
2484+ mock.ResetServers()
2485+
2486+ ds, err := data.ReadServer(strings.NewReader(jsonInstanceData))
2487+ c.Assert(err, gc.IsNil)
2488+ mock.AddServer(ds)
2489+
2490+ mock.AddServer(&data.Server{
2491+ Resource: data.Resource{URI: "uri", UUID: "uuid-no-ip"},
2492+ })
2493+
2494+ server, err := cli.Server("f4ec5097-121e-44a7-a207-75bc02163260")
2495+ c.Assert(err, gc.IsNil)
2496+ c.Assert(server, gc.NotNil)
2497+ s.inst = &sigmaInstance{server}
2498+
2499+ server, err = cli.Server("uuid-no-ip")
2500+ c.Assert(err, gc.IsNil)
2501+ c.Assert(server, gc.NotNil)
2502+ s.instWithoutIP = &sigmaInstance{server}
2503+}
2504+
2505+func (s *instanceSuite) TearDownTest(c *gc.C) {
2506+ mock.ResetServers()
2507+ s.BaseSuite.TearDownTest(c)
2508+}
2509+
2510+func (s *instanceSuite) TestInstanceEmpty(c *gc.C) {
2511+ e := sigmaInstance{}
2512+ c.Check(e.Id(), gc.Equals, instance.Id(""))
2513+ c.Check(e.Status(), gc.Equals, "")
2514+ c.Check(e.Refresh(), gc.ErrorMatches, "invalid instance")
2515+
2516+ addrs, err := e.Addresses()
2517+ c.Check(addrs, gc.IsNil)
2518+ c.Check(err, gc.Equals, instance.ErrNoDNSName)
2519+
2520+ name, err := e.DNSName()
2521+ c.Check(name, gc.Equals, "")
2522+ c.Check(err, gc.ErrorMatches, "invalid instance")
2523+
2524+ wname, err := e.WaitDNSName()
2525+ c.Check(wname, gc.Equals, "")
2526+ c.Check(err, gc.ErrorMatches, "invalid instance")
2527+
2528+ c.Check(e.hardware(), gc.IsNil)
2529+}
2530+
2531+func (s *instanceSuite) TestInstanceId(c *gc.C) {
2532+ c.Check(s.inst.Id(), gc.Equals, instance.Id("f4ec5097-121e-44a7-a207-75bc02163260"))
2533+}
2534+
2535+func (s *instanceSuite) TestInstanceStatus(c *gc.C) {
2536+ c.Check(s.inst.Status(), gc.Equals, "running")
2537+}
2538+
2539+func (s *instanceSuite) TestInstanceRefresh(c *gc.C) {
2540+ c.Check(s.inst.Status(), gc.Equals, "running")
2541+
2542+ mock.SetServerStatus("f4ec5097-121e-44a7-a207-75bc02163260", "stopped")
2543+
2544+ err := s.inst.Refresh()
2545+ c.Check(err, gc.IsNil)
2546+
2547+ c.Check(s.inst.Status(), gc.Equals, "stopped")
2548+}
2549+
2550+func (s *instanceSuite) TestInstanceAddresses(c *gc.C) {
2551+ addrs, err := s.inst.Addresses()
2552+ c.Check(addrs, gc.HasLen, 1)
2553+ c.Check(err, gc.IsNil)
2554+ a := addrs[0]
2555+ c.Check(a.Value, gc.Equals, "178.22.70.33")
2556+ c.Check(a.Type, gc.Equals, instance.Ipv4Address)
2557+ c.Check(a.NetworkScope, gc.Equals, instance.NetworkPublic)
2558+ c.Check(a.NetworkName, gc.Equals, "")
2559+
2560+ addrs, err = s.instWithoutIP.Addresses()
2561+ c.Check(addrs, gc.IsNil)
2562+ c.Check(err, gc.Equals, instance.ErrNoDNSName)
2563+}
2564+
2565+func (s *instanceSuite) TestInstanceDNSName(c *gc.C) {
2566+ name, err := s.inst.DNSName()
2567+ c.Check(name, gc.Equals, "178.22.70.33")
2568+ c.Check(err, gc.IsNil)
2569+
2570+ name, err = s.instWithoutIP.DNSName()
2571+ c.Check(name, gc.Equals, "")
2572+ c.Check(err, gc.Equals, instance.ErrNoDNSName)
2573+
2574+ wname, err := s.inst.WaitDNSName()
2575+ c.Check(wname, gc.Equals, "178.22.70.33")
2576+ c.Check(err, gc.IsNil)
2577+
2578+ go func() {
2579+ mock.AddServer(&data.Server{
2580+ Resource: data.Resource{URI: "uri", UUID: "uuid-no-ip"},
2581+ NICs: []data.NIC{
2582+ data.NIC{
2583+ IPv4: &data.IPv4{
2584+ Conf: "static",
2585+ IP: data.MakeIPResource("31.171.246.37"),
2586+ },
2587+ Runtime: &data.RuntimeNetwork{
2588+ InterfaceType: "public",
2589+ IPv4: data.MakeIPResource("31.171.246.37"),
2590+ },
2591+ },
2592+ },
2593+ })
2594+ }()
2595+
2596+ wname, err = s.instWithoutIP.WaitDNSName()
2597+ c.Check(wname, gc.Equals, "31.171.246.37")
2598+ c.Check(err, gc.IsNil)
2599+}
2600+
2601+func (s *instanceSuite) TestInstancePorts(c *gc.C) {
2602+ c.Check(s.inst.OpenPorts("", nil), gc.IsNil)
2603+ c.Check(s.inst.ClosePorts("", nil), gc.IsNil)
2604+
2605+ ports, err := s.inst.Ports("")
2606+ c.Check(ports, gc.NotNil)
2607+ if ports != nil {
2608+ c.Check(ports, gc.HasLen, 0)
2609+ }
2610+ c.Check(err, gc.IsNil)
2611+}
2612+
2613+func (s *instanceSuite) TestInstanceHardware(c *gc.C) {
2614+ hw := s.inst.hardware()
2615+ c.Assert(hw, gc.NotNil)
2616+
2617+ c.Check(hw.Arch, gc.IsNil)
2618+
2619+ c.Check(hw.Mem, gc.NotNil)
2620+ if hw.Mem != nil {
2621+ c.Check(*hw.Mem, gc.Equals, uint64(2048))
2622+ }
2623+
2624+ c.Check(hw.RootDisk, gc.IsNil)
2625+
2626+ c.Check(hw.CpuCores, gc.NotNil)
2627+ if hw.CpuCores != nil {
2628+ c.Check(*hw.CpuCores, gc.Equals, uint64(1))
2629+ }
2630+
2631+ c.Check(hw.CpuPower, gc.NotNil)
2632+ if hw.CpuPower != nil {
2633+ c.Check(*hw.CpuPower, gc.Equals, uint64(2000))
2634+ }
2635+
2636+ c.Check(hw.Tags, gc.IsNil)
2637+}
2638+
2639+const jsonInstanceData = `{
2640+ "context": true,
2641+ "cpu": 2000,
2642+ "cpu_model": null,
2643+ "cpus_instead_of_cores": false,
2644+ "drives": [
2645+ {
2646+ "boot_order": 1,
2647+ "dev_channel": "0:0",
2648+ "device": "virtio",
2649+ "drive": {
2650+ "resource_uri": "/api/2.0/drives/f968dc48-25a0-4d46-8f16-e12e073e1fe6/",
2651+ "uuid": "f968dc48-25a0-4d46-8f16-e12e073e1fe6"
2652+ },
2653+ "runtime": {
2654+ "io": {
2655+ "bytes_read": 82980352,
2656+ "bytes_written": 189440,
2657+ "count_flush": 0,
2658+ "count_read": 3952,
2659+ "count_written": 19,
2660+ "total_time_ns_flush": 0,
2661+ "total_time_ns_read": 4435322816,
2662+ "total_time_ns_write": 123240430
2663+ }
2664+ }
2665+ }
2666+ ],
2667+ "enable_numa": false,
2668+ "hv_relaxed": false,
2669+ "hv_tsc": false,
2670+ "jobs": [],
2671+ "mem": 2147483648,
2672+ "meta": {
2673+ "description": "test-description",
2674+ "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDiwTGBsmFKBYHcKaVy5IgsYBR4XVYLS6KP/NKClE7gONlIGURE3+/45BX8TfHJHM5WTN8NBqJejKDHqwfyueR1f2VGoPkJxODGt/X/ZDNftLZLYwPd2DfDBs27ahOadZCk4Cl5l7mU0aoE74UnIcQoNPl6w7axkIFTIXr8+0HMk8DFB0iviBSJK118p1RGwhsoA1Hudn1CsgqARGPmNn6mxwvmQfQY7hZxZoOH9WMcvkNZ7rAFrwS/BuvEpEXkoC95K/JDPvmQVVJk7we+WeHfTYSmApkDFcSaypyjL2HOV8pvE+VntcIIhZccHiOubyjsBAx5aoTI+ueCsoz5AL1 maxim.perenesenko@altoros.com"
2675+ },
2676+ "name": "LiveTest-srv-17-54-51-999999999",
2677+ "nics": [
2678+ {
2679+ "boot_order": null,
2680+ "firewall_policy": null,
2681+ "ip_v4_conf": {
2682+ "conf": "dhcp",
2683+ "ip": null
2684+ },
2685+ "ip_v6_conf": null,
2686+ "mac": "22:ab:bf:26:e1:be",
2687+ "model": "virtio",
2688+ "runtime": {
2689+ "interface_type": "public",
2690+ "io": {
2691+ "bytes_recv": 0,
2692+ "bytes_sent": 17540,
2693+ "packets_recv": 0,
2694+ "packets_sent": 256
2695+ },
2696+ "ip_v4": {
2697+ "resource_uri": "/api/2.0/ips/178.22.70.33/",
2698+ "uuid": "178.22.70.33"
2699+ },
2700+ "ip_v6": null
2701+ },
2702+ "vlan": null
2703+ },
2704+ {
2705+ "boot_order": null,
2706+ "firewall_policy": null,
2707+ "ip_v4_conf": null,
2708+ "ip_v6_conf": null,
2709+ "mac": "22:9e:e7:d7:86:94",
2710+ "model": "virtio",
2711+ "runtime": {
2712+ "interface_type": "private",
2713+ "io": {
2714+ "bytes_recv": 0,
2715+ "bytes_sent": 1046,
2716+ "packets_recv": 0,
2717+ "packets_sent": 13
2718+ },
2719+ "ip_v4": null,
2720+ "ip_v6": null
2721+ },
2722+ "vlan": {
2723+ "resource_uri": "/api/2.0/vlans/5bc05e7e-6555-4f40-add8-3b8e91447702/",
2724+ "uuid": "5bc05e7e-6555-4f40-add8-3b8e91447702"
2725+ }
2726+ }
2727+ ],
2728+ "owner": {
2729+ "resource_uri": "/api/2.0/user/c25eb0ed-161f-44f4-ac1d-d584ce3a5312/",
2730+ "uuid": "c25eb0ed-161f-44f4-ac1d-d584ce3a5312"
2731+ },
2732+ "requirements": [],
2733+ "resource_uri": "/api/2.0/servers/f4ec5097-121e-44a7-a207-75bc02163260/",
2734+ "runtime": {
2735+ "active_since": "2014-04-24T14:56:58+00:00",
2736+ "nics": [
2737+ {
2738+ "interface_type": "public",
2739+ "io": {
2740+ "bytes_recv": 0,
2741+ "bytes_sent": 17540,
2742+ "packets_recv": 0,
2743+ "packets_sent": 256
2744+ },
2745+ "ip_v4": {
2746+ "resource_uri": "/api/2.0/ips/178.22.70.33/",
2747+ "uuid": "178.22.70.33"
2748+ },
2749+ "ip_v6": null,
2750+ "mac": "22:ab:bf:26:e1:be"
2751+ },
2752+ {
2753+ "interface_type": "private",
2754+ "io": {
2755+ "bytes_recv": 0,
2756+ "bytes_sent": 1046,
2757+ "packets_recv": 0,
2758+ "packets_sent": 13
2759+ },
2760+ "ip_v4": null,
2761+ "ip_v6": null,
2762+ "mac": "22:9e:e7:d7:86:94"
2763+ }
2764+ ]
2765+ },
2766+ "smp": 1,
2767+ "status": "running",
2768+ "tags": [],
2769+ "uuid": "f4ec5097-121e-44a7-a207-75bc02163260",
2770+ "vnc_password": "test-vnc-password"
2771+}`
2772
2773=== added file 'provider/cloudsigma/provider.go'
2774--- provider/cloudsigma/provider.go 1970-01-01 00:00:00 +0000
2775+++ provider/cloudsigma/provider.go 2014-06-09 11:10:33 +0000
2776@@ -0,0 +1,140 @@
2777+// Copyright 2014 Canonical Ltd.
2778+// Licensed under the AGPLv3, see LICENCE file for details.
2779+
2780+// Juju provider for CloudSigma
2781+
2782+package cloudsigma
2783+
2784+import (
2785+ "fmt"
2786+
2787+ "github.com/juju/loggo"
2788+
2789+ "launchpad.net/juju-core/environs"
2790+ "launchpad.net/juju-core/environs/config"
2791+)
2792+
2793+var logger = loggo.GetLogger("juju.provider.cloudsigma")
2794+
2795+type loggerWriter struct {
2796+ level loggo.Level
2797+}
2798+
2799+func (lw loggerWriter) Write(p []byte) (n int, err error) {
2800+ logger.Logf(lw.level, string(p))
2801+ return len(p), nil
2802+}
2803+
2804+type environProvider struct{}
2805+
2806+var providerInstance = environProvider{}
2807+
2808+// check the provider implements environs.EnvironProvider interface
2809+var _ environs.EnvironProvider = (*environProvider)(nil)
2810+
2811+func init() {
2812+ // This will only happen in binaries that actually import this provider
2813+ // somewhere. To enable a provider, import it in the "providers/all"
2814+ // package; please do *not* import individual providers anywhere else,
2815+ // except in direct tests for that provider.
2816+ environs.RegisterProvider("cloudsigma", providerInstance)
2817+}
2818+
2819+// Boilerplate returns a default configuration for the environment in yaml format.
2820+// The text should be a key followed by some number of attributes:
2821+// `environName:
2822+// type: environTypeName
2823+// attr1: val1
2824+// `
2825+// The text is used as a template (see the template package) with one extra template
2826+// function available, rand, which expands to a random hexadecimal string when invoked.
2827+func (environProvider) BoilerplateConfig() string {
2828+ return boilerplateConfig
2829+}
2830+
2831+// Open opens the environment and returns it.
2832+// The configuration must have come from a previously
2833+// prepared environment.
2834+func (environProvider) Open(cfg *config.Config) (environs.Environ, error) {
2835+ logger.Infof("opening environment %q", cfg.Name())
2836+
2837+ cfg, err := prepareConfig(cfg)
2838+ if err != nil {
2839+ return nil, err
2840+ }
2841+
2842+ env := &environ{name: cfg.Name()}
2843+ if err := env.SetConfig(cfg); err != nil {
2844+ return nil, err
2845+ }
2846+
2847+ return env, nil
2848+}
2849+
2850+// Prepare prepares an environment for use. Any additional
2851+// configuration attributes in the returned environment should
2852+// be saved to be used later. If the environment is already
2853+// prepared, this call is equivalent to Open.
2854+func (environProvider) Prepare(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) {
2855+ logger.Infof("preparing environment %q", cfg.Name())
2856+ return providerInstance.Open(cfg)
2857+}
2858+
2859+// Validate ensures that config is a valid configuration for this
2860+// provider, applying changes to it if necessary, and returns the
2861+// validated configuration.
2862+// If old is not nil, it holds the previous environment configuration
2863+// for consideration when validating changes.
2864+func (environProvider) Validate(cfg, old *config.Config) (*config.Config, error) {
2865+ logger.Infof("validating environment %q", cfg.Name())
2866+
2867+ // You should almost certainly not change this method; if you need to change
2868+ // how configs are validated, you should edit validateConfig itself, to ensure
2869+ // that your checks are always applied.
2870+ newEcfg, err := validateConfig(cfg, nil)
2871+ if err != nil {
2872+ return nil, fmt.Errorf("invalid config: %v", err)
2873+ }
2874+ if old != nil {
2875+ oldEcfg, err := validateConfig(old, nil)
2876+ if err != nil {
2877+ return nil, fmt.Errorf("invalid base config: %v", err)
2878+ }
2879+ if newEcfg, err = validateConfig(cfg, oldEcfg); err != nil {
2880+ return nil, fmt.Errorf("invalid config change: %v", err)
2881+ }
2882+ }
2883+
2884+ return newEcfg.Config, nil
2885+}
2886+
2887+// SecretAttrs filters the supplied configuration returning only values
2888+// which are considered sensitive. All of the values of these secret
2889+// attributes need to be strings.
2890+func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) {
2891+ logger.Infof("filtering secret attributes for environment %q", cfg.Name())
2892+
2893+ // If you keep configSecretFields up to date, this method should Just Work.
2894+ ecfg, err := validateConfig(cfg, nil)
2895+ if err != nil {
2896+ return nil, err
2897+ }
2898+ secretAttrs := map[string]string{}
2899+ for _, field := range configSecretFields {
2900+ if value, ok := ecfg.attrs[field]; ok {
2901+ if stringValue, ok := value.(string); ok {
2902+ secretAttrs[field] = stringValue
2903+ } else {
2904+ // All your secret attributes must be strings at the moment. Sorry.
2905+ // It's an expedient and hopefully temporary measure that helps us
2906+ // plug a security hole in the API.
2907+ return nil, fmt.Errorf(
2908+ "secret %q field must have a string value; got %v",
2909+ field, value,
2910+ )
2911+ }
2912+ }
2913+ }
2914+
2915+ return secretAttrs, nil
2916+}
2917
2918=== added file 'provider/cloudsigma/provider_test.go'
2919--- provider/cloudsigma/provider_test.go 1970-01-01 00:00:00 +0000
2920+++ provider/cloudsigma/provider_test.go 2014-06-09 11:10:33 +0000
2921@@ -0,0 +1,94 @@
2922+// Copyright 2014 Canonical Ltd.
2923+// Licensed under the AGPLv3, see LICENCE file for details.
2924+
2925+package cloudsigma
2926+
2927+import (
2928+ "flag"
2929+ "io"
2930+ "testing"
2931+
2932+ gc "launchpad.net/gocheck"
2933+ tt "launchpad.net/juju-core/testing"
2934+ "launchpad.net/juju-core/utils"
2935+)
2936+
2937+var live = flag.Bool("live", false, "run tests on live CloudSigma account")
2938+
2939+func TestCloudSigma(t *testing.T) {
2940+ /*
2941+ if *live {
2942+ registerLiveTests()
2943+ }
2944+ */
2945+ gc.TestingT(t)
2946+}
2947+
2948+type providerSuite struct {
2949+ tt.BaseSuite
2950+}
2951+
2952+var _ = gc.Suite(&providerSuite{})
2953+
2954+func (s *providerSuite) TestProviderBoilerplateConfig(c *gc.C) {
2955+ cfg := providerInstance.BoilerplateConfig()
2956+ c.Assert(cfg, gc.Not(gc.Equals), "")
2957+}
2958+
2959+type failReader struct {
2960+ err error
2961+}
2962+
2963+func (f *failReader) Read(p []byte) (n int, err error) {
2964+ return 0, f.err
2965+}
2966+
2967+type fakeStorage struct {
2968+ call string
2969+ name string
2970+ prefix string
2971+ err error
2972+ reader io.Reader
2973+ length int64
2974+}
2975+
2976+func (f *fakeStorage) Get(name string) (io.ReadCloser, error) {
2977+ f.call = "Get"
2978+ f.name = name
2979+ return nil, nil
2980+}
2981+func (f *fakeStorage) List(prefix string) ([]string, error) {
2982+ f.call = "List"
2983+ f.prefix = prefix
2984+ return []string{prefix}, nil
2985+}
2986+func (f *fakeStorage) URL(name string) (string, error) {
2987+ f.call = "URL"
2988+ f.name = name
2989+ return "", nil
2990+}
2991+func (f *fakeStorage) DefaultConsistencyStrategy() utils.AttemptStrategy {
2992+ f.call = "DefaultConsistencyStrategy"
2993+ return utils.AttemptStrategy{}
2994+}
2995+func (f *fakeStorage) ShouldRetry(err error) bool {
2996+ f.call = "ShouldRetry"
2997+ f.err = err
2998+ return false
2999+}
3000+func (f *fakeStorage) Put(name string, r io.Reader, length int64) error {
3001+ f.call = "Put"
3002+ f.name = name
3003+ f.reader = r
3004+ f.length = length
3005+ return nil
3006+}
3007+func (f *fakeStorage) Remove(name string) error {
3008+ f.call = "Remove"
3009+ f.name = name
3010+ return nil
3011+}
3012+func (f *fakeStorage) RemoveAll() error {
3013+ f.call = "RemoveAll"
3014+ return nil
3015+}
3016
3017=== added file 'provider/cloudsigma/storage.go'
3018--- provider/cloudsigma/storage.go 1970-01-01 00:00:00 +0000
3019+++ provider/cloudsigma/storage.go 2014-06-09 11:10:33 +0000
3020@@ -0,0 +1,252 @@
3021+// Copyright 2014 Canonical Ltd.
3022+// Licensed under the AGPLv3, see LICENCE file for details.
3023+
3024+package cloudsigma
3025+
3026+import (
3027+ "bytes"
3028+ "fmt"
3029+ "github.com/juju/loggo"
3030+ "io"
3031+ "io/ioutil"
3032+ "os"
3033+ "path"
3034+ "strings"
3035+
3036+ "launchpad.net/juju-core/agent"
3037+ "launchpad.net/juju-core/environs/filestorage"
3038+ "launchpad.net/juju-core/environs/httpstorage"
3039+ "launchpad.net/juju-core/environs/sshstorage"
3040+ "launchpad.net/juju-core/environs/storage"
3041+ "launchpad.net/juju-core/juju/osenv"
3042+ "launchpad.net/juju-core/utils"
3043+)
3044+
3045+const (
3046+ // storageSubdir is the subdirectory of
3047+ // dataDir in which storage will be located.
3048+ storageSubdir = "storage"
3049+
3050+ // storageTmpSubdir is the subdirectory of
3051+ // dataDir in which temporary storage will
3052+ // be located.
3053+ storageTmpSubdir = "storage-tmp"
3054+)
3055+
3056+type environStorage struct {
3057+ storage storage.Storage
3058+ uuid string
3059+ tmp bool
3060+}
3061+
3062+var _ storage.Storage = (*environStorage)(nil)
3063+
3064+var newStorage = func(ecfg *environConfig, client *environClient) (*environStorage, error) {
3065+ var stg storage.Storage
3066+ var err error
3067+ var tmp bool
3068+
3069+ uuid, ip, ok := client.stateServerAddress()
3070+ if ok {
3071+ logger.Debugf("active state server: %q", ip)
3072+ // create https storage client
3073+ stg, err = newRemoteStorage(ecfg, ip)
3074+ } else {
3075+ // create tmp storage
3076+ tmp = true
3077+ path := path.Join(osenv.JujuHome(), "tmp")
3078+
3079+ logger.Debugf("prepare %q for tmp storage", path)
3080+ logger.Debugf(" removing all...")
3081+ if err := os.RemoveAll(path); err != nil {
3082+ return nil, fmt.Errorf("create tmp storage (RemoveAll): %v", err)
3083+ }
3084+
3085+ logger.Debugf(" creating path...")
3086+ if err := os.MkdirAll(path, 0755); err != nil {
3087+ return nil, fmt.Errorf("create tmp storage (MkdirAll): %v", err)
3088+ }
3089+ stg, err = filestorage.NewFileStorageWriter(path)
3090+ logger.Debugf("using local tmp storage at: %q", path)
3091+ }
3092+
3093+ if err != nil {
3094+ return nil, err
3095+ }
3096+
3097+ result := &environStorage{
3098+ storage: stg,
3099+ uuid: uuid,
3100+ tmp: tmp,
3101+ }
3102+
3103+ client.storage = result
3104+
3105+ return result, nil
3106+}
3107+
3108+func newRemoteStorage(ecfg *environConfig, ip string) (storage.Storage, error) {
3109+ caCertPEM, ok := ecfg.CACert()
3110+ if !ok {
3111+ // should not be possible to validate base config
3112+ return nil, fmt.Errorf("ca-cert not set")
3113+ }
3114+
3115+ addr := fmt.Sprintf("%s:%d", ip, ecfg.storagePort())
3116+ logger.Debugf("using https storage at: %q", addr)
3117+
3118+ authkey := ecfg.storageAuthKey()
3119+ stg, err := httpstorage.ClientTLS(addr, caCertPEM, authkey)
3120+ if err != nil {
3121+ return nil, fmt.Errorf("initializing HTTPS storage failed: %v", err)
3122+ }
3123+
3124+ return stg, nil
3125+}
3126+
3127+func (s *environStorage) onStateInstanceStop(uuid string) {
3128+ if s.uuid != uuid {
3129+ return
3130+ }
3131+ s.storage = nil
3132+ s.uuid = ""
3133+ s.tmp = false
3134+}
3135+
3136+func (s *environStorage) List(prefix string) ([]string, error) {
3137+ if s.storage == nil {
3138+ return nil, fmt.Errorf("storage is not initialized")
3139+ }
3140+ list, err := s.storage.List(prefix)
3141+ logger.Tracef("environStorage.List, prefix = %s, len = %d, err = %v", prefix, len(list), err)
3142+ if err == nil && logger.LogLevel() <= loggo.TRACE {
3143+ for _, name := range list {
3144+ logger.Tracef("...%q", name)
3145+ }
3146+ }
3147+ return list, err
3148+}
3149+
3150+func (s *environStorage) URL(name string) (string, error) {
3151+ if s.storage == nil {
3152+ return "", fmt.Errorf("storage is not initialized")
3153+ }
3154+ if s.tmp {
3155+ return "%s/" + name, nil
3156+ }
3157+ url, err := s.storage.URL(name)
3158+ logger.Tracef("environStorage.URL, name = %q, url = %q, err = %v", name, url, err)
3159+ return url, err
3160+}
3161+
3162+func (s *environStorage) Get(name string) (io.ReadCloser, error) {
3163+ if s.storage == nil {
3164+ return nil, fmt.Errorf("storage is not initialized")
3165+ }
3166+ r, err := s.storage.Get(name)
3167+ logger.Tracef("environStorage.Get, name = %q, err = %v", name, err)
3168+ return r, err
3169+}
3170+
3171+func (s *environStorage) Put(name string, r io.Reader, length int64) error {
3172+ if s.storage == nil {
3173+ return fmt.Errorf("storage is not initialized")
3174+ }
3175+ err := s.storage.Put(name, r, length)
3176+ logger.Tracef("environStorage.Put, name = %q, len = %d, err = %v", name, length, err)
3177+ return err
3178+}
3179+
3180+func (s *environStorage) Remove(name string) error {
3181+ if s.storage == nil {
3182+ return fmt.Errorf("storage is not initialized")
3183+ }
3184+ err := s.storage.Remove(name)
3185+ logger.Tracef("environStorage.Remove, name = %q, err = %v", name, err)
3186+ return err
3187+}
3188+
3189+func (s *environStorage) RemoveAll() error {
3190+ if s.storage == nil {
3191+ // this method is called after state server destroy at destroy-environment
3192+ return nil
3193+ }
3194+ err := s.storage.RemoveAll()
3195+ logger.Tracef("environStorage.RemoveAll, err = %v", err)
3196+ return err
3197+}
3198+
3199+func (s *environStorage) DefaultConsistencyStrategy() utils.AttemptStrategy {
3200+ if s.storage == nil {
3201+ return utils.AttemptStrategy{}
3202+ }
3203+ return s.storage.DefaultConsistencyStrategy()
3204+}
3205+
3206+func (s *environStorage) ShouldRetry(err error) bool {
3207+ if s.storage == nil {
3208+ return false
3209+ }
3210+ return s.storage.ShouldRetry(err)
3211+}
3212+
3213+var newSSHStorage = func(sshurl, dir, tmpdir string) (storage.Storage, error) {
3214+ return sshstorage.NewSSHStorage(sshstorage.NewSSHStorageParams{
3215+ Host: sshurl,
3216+ StorageDir: dir,
3217+ TmpDir: tmpdir,
3218+ })
3219+}
3220+
3221+func (s *environStorage) MoveToSSH(user, host string) error {
3222+ if s.storage == nil {
3223+ return fmt.Errorf("storage is not initialized")
3224+ }
3225+
3226+ sshurl := user + "@" + host
3227+ if !s.tmp {
3228+ return fmt.Errorf("failed to move non-temporary storage to %q", sshurl)
3229+ }
3230+
3231+ storageDir := path.Join(agent.DefaultDataDir, storageSubdir)
3232+ storageTmpdir := path.Join(agent.DefaultDataDir, storageTmpSubdir)
3233+
3234+ logger.Debugf("using ssh storage at host %q dir %q", sshurl, storageDir)
3235+ stor, err := newSSHStorage(sshurl, storageDir, storageTmpdir)
3236+ if err != nil {
3237+ return fmt.Errorf("initializing SSH storage failed: %v", err)
3238+ }
3239+
3240+ list, err := s.List("")
3241+ if err != nil {
3242+ return fmt.Errorf("listing tmp storage failed: %v", err)
3243+ }
3244+
3245+ logger.Tracef("list to move:\n%s", strings.Join(list, "\n"))
3246+
3247+ for _, path := range list {
3248+ r, err := s.Get(path)
3249+ if err != nil {
3250+ return fmt.Errorf("getting %q from tmp storage failed: %v", path, err)
3251+ }
3252+ defer r.Close()
3253+
3254+ bb, err := ioutil.ReadAll(r)
3255+ if err != nil {
3256+ return fmt.Errorf("error MoveToSSH: reading %q from ssh storage failed: %v", path, err)
3257+ }
3258+
3259+ rb := bytes.NewReader(bb)
3260+ length := len(bb)
3261+ if err := stor.Put(path, rb, int64(length)); err != nil {
3262+ return fmt.Errorf("error MoveToSSH: putting %q to ssh storage failed: %v", path, err)
3263+ }
3264+ }
3265+
3266+ s.storage.RemoveAll()
3267+
3268+ s.storage = stor
3269+ s.tmp = false
3270+
3271+ return nil
3272+}
3273
3274=== added file 'provider/cloudsigma/storage_test.go'
3275--- provider/cloudsigma/storage_test.go 1970-01-01 00:00:00 +0000
3276+++ provider/cloudsigma/storage_test.go 2014-06-09 11:10:33 +0000
3277@@ -0,0 +1,373 @@
3278+// Copyright 2014 Canonical Ltd.
3279+// Licensed under the AGPLv3, see LICENCE file for details.
3280+
3281+package cloudsigma
3282+
3283+import (
3284+ "fmt"
3285+ "github.com/juju/loggo"
3286+ "io"
3287+ "io/ioutil"
3288+ "strings"
3289+
3290+ gc "launchpad.net/gocheck"
3291+ "launchpad.net/juju-core/environs/config"
3292+ "launchpad.net/juju-core/environs/storage"
3293+ "launchpad.net/juju-core/environs/testing"
3294+ tt "launchpad.net/juju-core/testing"
3295+ "launchpad.net/juju-core/utils"
3296+)
3297+
3298+type StorageSuite struct {
3299+ tt.BaseSuite
3300+}
3301+
3302+var _ = gc.Suite(&StorageSuite{})
3303+
3304+func (s *StorageSuite) TestStorageEmpty(c *gc.C) {
3305+ stg := &environStorage{}
3306+
3307+ lst, err := stg.List("")
3308+ c.Check(lst, gc.IsNil)
3309+ c.Check(err, gc.NotNil)
3310+
3311+ url, err := stg.URL("")
3312+ c.Check(url, gc.Equals, "")
3313+ c.Check(err, gc.NotNil)
3314+
3315+ r, err := stg.Get("")
3316+ c.Check(r, gc.IsNil)
3317+ c.Check(err, gc.NotNil)
3318+
3319+ err = stg.Put("", nil, 0)
3320+ c.Check(err, gc.NotNil)
3321+
3322+ err = stg.Remove("")
3323+ c.Check(err, gc.NotNil)
3324+
3325+ err = stg.RemoveAll()
3326+ c.Check(err, gc.IsNil)
3327+
3328+ strategy := stg.DefaultConsistencyStrategy()
3329+ c.Check(strategy, gc.Equals, utils.AttemptStrategy{})
3330+
3331+ shr := stg.ShouldRetry(nil)
3332+ c.Check(shr, gc.Equals, false)
3333+
3334+ err = stg.MoveToSSH("", "")
3335+ c.Check(err, gc.NotNil)
3336+}
3337+
3338+func (s *StorageSuite) TestStorageInstanceStop(c *gc.C) {
3339+ stg := &environStorage{
3340+ storage: &fakeStorage{},
3341+ uuid: "uuid",
3342+ tmp: true,
3343+ }
3344+ stg.onStateInstanceStop("")
3345+ c.Check(stg.uuid, gc.Equals, "uuid")
3346+ stg.onStateInstanceStop("uuid")
3347+ c.Check(stg.storage, gc.IsNil)
3348+ c.Check(stg.uuid, gc.Equals, "")
3349+ c.Check(stg.tmp, gc.Equals, false)
3350+}
3351+
3352+func (s *StorageSuite) TestStorageURL(c *gc.C) {
3353+ fs := &fakeStorage{}
3354+ stg := &environStorage{
3355+ storage: fs,
3356+ uuid: "uuid",
3357+ tmp: true,
3358+ }
3359+
3360+ url, err := stg.URL("")
3361+ c.Check(err, gc.IsNil)
3362+ c.Check(url, gc.Equals, "%s/")
3363+
3364+ url, err = stg.URL("path/name")
3365+ c.Check(err, gc.IsNil)
3366+ c.Check(url, gc.Equals, "%s/path/name")
3367+
3368+ stg.tmp = false
3369+
3370+ url, err = stg.URL("path/name")
3371+ c.Check(err, gc.IsNil)
3372+ c.Check(url, gc.Equals, "")
3373+ c.Check(fs.call, gc.Equals, "URL")
3374+ c.Check(fs.name, gc.Equals, "path/name")
3375+}
3376+
3377+func (s *StorageSuite) TestStorageProxy(c *gc.C) {
3378+ fs := &fakeStorage{}
3379+ stg := &environStorage{
3380+ storage: fs,
3381+ uuid: "uuid",
3382+ tmp: true,
3383+ }
3384+
3385+ test := func(s storage.Storage) {
3386+
3387+ ll := logger.LogLevel()
3388+ logger.SetLogLevel(loggo.TRACE)
3389+ s.List("list")
3390+ c.Check(fs.call, gc.Equals, "List")
3391+ c.Check(fs.prefix, gc.Equals, "list")
3392+ logger.SetLogLevel(ll)
3393+
3394+ s.Get("get")
3395+ c.Check(fs.call, gc.Equals, "Get")
3396+ c.Check(fs.name, gc.Equals, "get")
3397+
3398+ r := strings.NewReader("")
3399+ s.Put("put", r, 1024)
3400+ c.Check(fs.call, gc.Equals, "Put")
3401+ c.Check(fs.name, gc.Equals, "put")
3402+ c.Check(fs.reader, gc.Equals, r)
3403+ c.Check(fs.length, gc.Equals, int64(1024))
3404+
3405+ s.Remove("remove")
3406+ c.Check(fs.call, gc.Equals, "Remove")
3407+ c.Check(fs.name, gc.Equals, "remove")
3408+
3409+ s.RemoveAll()
3410+ c.Check(fs.call, gc.Equals, "RemoveAll")
3411+
3412+ s.DefaultConsistencyStrategy()
3413+ c.Check(fs.call, gc.Equals, "DefaultConsistencyStrategy")
3414+
3415+ err := fmt.Errorf("test")
3416+ s.ShouldRetry(err)
3417+ c.Check(fs.call, gc.Equals, "ShouldRetry")
3418+ c.Check(fs.err, gc.Equals, err)
3419+ }
3420+
3421+ test(stg)
3422+
3423+ stg.tmp = false
3424+ test(stg)
3425+}
3426+
3427+func (s *StorageSuite) TestStorageMoveNonTemp(c *gc.C) {
3428+ fs := &fakeStorage{}
3429+ stg := &environStorage{
3430+ storage: fs,
3431+ uuid: "uuid",
3432+ tmp: false,
3433+ }
3434+
3435+ err := stg.MoveToSSH("", "")
3436+ c.Check(err, gc.NotNil)
3437+}
3438+
3439+func (s *StorageSuite) TestStorageMoveFailSsh(c *gc.C) {
3440+ fs := &fakeStorage{}
3441+ stg := &environStorage{
3442+ storage: fs,
3443+ uuid: "uuid",
3444+ tmp: true,
3445+ }
3446+
3447+ s.PatchValue(&newSSHStorage,
3448+ func(sshurl, dir, tmpdir string) (storage.Storage, error) {
3449+ return nil, fmt.Errorf("test")
3450+ })
3451+
3452+ err := stg.MoveToSSH("", "")
3453+ c.Check(err, gc.NotNil)
3454+ c.Check(err, gc.ErrorMatches, "initializing SSH storage failed: test")
3455+}
3456+
3457+func (s *StorageSuite) TestStorageMoveFailList(c *gc.C) {
3458+ fs := &fakeStorage{}
3459+ stg := &environStorage{
3460+ storage: fs,
3461+ uuid: "uuid",
3462+ tmp: true,
3463+ }
3464+
3465+ s.PatchValue(&newSSHStorage,
3466+ func(sshurl, dir, tmpdir string) (storage.Storage, error) {
3467+ stg.storage = nil
3468+ return nil, nil
3469+ })
3470+
3471+ err := stg.MoveToSSH("", "")
3472+ c.Check(err, gc.NotNil)
3473+ c.Check(err, gc.ErrorMatches, "listing tmp storage failed: storage is not initialized")
3474+}
3475+
3476+func newTestStorage(s *StorageSuite, c *gc.C) storage.Storage {
3477+ closer, stor, _ := testing.CreateLocalTestStorage(c)
3478+ s.AddCleanup(func(*gc.C) { closer.Close() })
3479+ return stor
3480+}
3481+
3482+type moveStorageTest struct {
3483+ stg storage.Storage
3484+ handler func(_, _, _ string) (storage.Storage, error)
3485+ data map[string]string
3486+ emsg string
3487+}
3488+
3489+func (s *StorageSuite) testMoveStorage(c *gc.C, d *moveStorageTest) {
3490+
3491+ s.PatchValue(&newSSHStorage, d.handler)
3492+
3493+ for k, v := range d.data {
3494+ err := d.stg.Put(k, strings.NewReader(v), int64(len(v)))
3495+ c.Assert(err, gc.IsNil)
3496+ }
3497+
3498+ src := &environStorage{
3499+ storage: d.stg,
3500+ uuid: "uuid",
3501+ tmp: true,
3502+ }
3503+
3504+ err := src.MoveToSSH("user", "host")
3505+ if d.emsg == "" {
3506+ c.Check(err, gc.IsNil)
3507+ } else {
3508+ c.Check(err, gc.ErrorMatches, d.emsg)
3509+ return
3510+ }
3511+
3512+ kk, err := src.storage.List("")
3513+ c.Assert(err, gc.IsNil)
3514+
3515+ var result = make(map[string]string, len(kk))
3516+ for _, k := range kk {
3517+ r, err := src.storage.Get(k)
3518+ c.Assert(err, gc.IsNil)
3519+ defer r.Close()
3520+ bb, err := ioutil.ReadAll(r)
3521+ c.Assert(err, gc.IsNil)
3522+ result[k] = string(bb)
3523+ }
3524+
3525+ for k, v := range d.data {
3526+ if vv, ok := result[k]; !ok {
3527+ c.Errorf("key %s not found", k)
3528+ } else {
3529+ c.Check(vv, gc.Equals, v)
3530+ }
3531+ }
3532+}
3533+
3534+func (s *StorageSuite) TestStorageMoveEmpty(c *gc.C) {
3535+ data := &moveStorageTest{
3536+ stg: newTestStorage(s, c),
3537+ handler: func(sshurl, dir, tmpdir string) (storage.Storage, error) {
3538+ c.Check(sshurl, gc.Equals, "user@host")
3539+ return newTestStorage(s, c), nil
3540+ }}
3541+ s.testMoveStorage(c, data)
3542+}
3543+
3544+var moveData = map[string]string{
3545+ "test0": "0987654321",
3546+ "test/test1": "1234567890",
3547+}
3548+
3549+func (s *StorageSuite) TestStorageMoveData(c *gc.C) {
3550+ data := &moveStorageTest{
3551+ stg: newTestStorage(s, c),
3552+ handler: func(sshurl, dir, tmpdir string) (storage.Storage, error) {
3553+ c.Check(sshurl, gc.Equals, "user@host")
3554+ return newTestStorage(s, c), nil
3555+ },
3556+ data: moveData,
3557+ }
3558+ s.testMoveStorage(c, data)
3559+}
3560+
3561+type storageProxyGetFailed struct {
3562+ storage.Storage
3563+}
3564+
3565+func (s *storageProxyGetFailed) Get(name string) (io.ReadCloser, error) {
3566+ return nil, fmt.Errorf("test")
3567+}
3568+
3569+func (s *StorageSuite) TestStorageMoveGetFailed(c *gc.C) {
3570+ data := &moveStorageTest{
3571+ stg: &storageProxyGetFailed{newTestStorage(s, c)},
3572+ handler: func(sshurl, dir, tmpdir string) (storage.Storage, error) {
3573+ c.Check(sshurl, gc.Equals, "user@host")
3574+ return newTestStorage(s, c), nil
3575+ },
3576+ data: moveData,
3577+ emsg: "getting .* from tmp storage failed: test",
3578+ }
3579+ s.testMoveStorage(c, data)
3580+}
3581+
3582+type storageProxyGetReadFailed struct {
3583+ storage.Storage
3584+}
3585+
3586+func (s *storageProxyGetReadFailed) Get(name string) (io.ReadCloser, error) {
3587+ err := fmt.Errorf("test")
3588+ r := &failReader{err}
3589+ return ioutil.NopCloser(r), nil
3590+}
3591+
3592+func (s *StorageSuite) TestStorageMoveGetReadFailed(c *gc.C) {
3593+ data := &moveStorageTest{
3594+ stg: &storageProxyGetReadFailed{newTestStorage(s, c)},
3595+ handler: func(sshurl, dir, tmpdir string) (storage.Storage, error) {
3596+ c.Check(sshurl, gc.Equals, "user@host")
3597+ return newTestStorage(s, c), nil
3598+ },
3599+ data: moveData,
3600+ emsg: ".*MoveToSSH: reading .* from ssh storage failed: test",
3601+ }
3602+ s.testMoveStorage(c, data)
3603+}
3604+
3605+type storageProxyPutFailed struct {
3606+ storage.Storage
3607+}
3608+
3609+func (s *storageProxyPutFailed) Put(name string, r io.Reader, length int64) error {
3610+ return fmt.Errorf("test")
3611+}
3612+
3613+func (s *StorageSuite) TestStorageMovePutFailed(c *gc.C) {
3614+ data := &moveStorageTest{
3615+ stg: newTestStorage(s, c),
3616+ handler: func(sshurl, dir, tmpdir string) (storage.Storage, error) {
3617+ c.Check(sshurl, gc.Equals, "user@host")
3618+ return &storageProxyPutFailed{newTestStorage(s, c)}, nil
3619+ },
3620+ data: moveData,
3621+ emsg: ".*MoveToSSH: putting .* to ssh storage failed: test",
3622+ }
3623+ s.testMoveStorage(c, data)
3624+}
3625+
3626+func (s *StorageSuite) TestStorageNewRemoteStorage(c *gc.C) {
3627+ ecfg := &environConfig{
3628+ Config: newConfig(c, tt.Attrs{
3629+ "name": "client-test",
3630+ }),
3631+ attrs: map[string]interface{}{
3632+ "storage-port": 1234,
3633+ },
3634+ }
3635+ stg, err := newRemoteStorage(ecfg, "0.1.2.3")
3636+ c.Check(stg, gc.NotNil)
3637+ c.Check(err, gc.IsNil)
3638+
3639+ attrs := tt.FakeConfig().Delete("ca-cert", "ca-private-key")
3640+ cfg, err := config.New(config.NoDefaults, attrs)
3641+ c.Assert(err, gc.IsNil)
3642+ ecfg = &environConfig{
3643+ Config: cfg,
3644+ attrs: map[string]interface{}{
3645+ "storage-port": 1234,
3646+ }}
3647+ stg, err = newRemoteStorage(ecfg, "0.1.2.3")
3648+ c.Check(stg, gc.IsNil)
3649+ c.Check(err, gc.ErrorMatches, "ca-cert not set")
3650+}
3651
3652=== added file 'provider/cloudsigma/storageconfig.go'
3653--- provider/cloudsigma/storageconfig.go 1970-01-01 00:00:00 +0000
3654+++ provider/cloudsigma/storageconfig.go 2014-06-09 11:10:33 +0000
3655@@ -0,0 +1,59 @@
3656+// Copyright 2014 Canonical Ltd.
3657+// Licensed under the AGPLv3, see LICENCE file for details.
3658+
3659+package cloudsigma
3660+
3661+import (
3662+ "fmt"
3663+
3664+ "launchpad.net/juju-core/worker/localstorage"
3665+)
3666+
3667+// storageConfig is an struct implementing LocalTLSStorageConfig interface
3668+// to support serving storage over TLS.
3669+type storageConfig struct {
3670+ ecfg *environConfig
3671+ storageDir string
3672+ storageAddr string
3673+ storagePort int
3674+}
3675+
3676+var _ localstorage.LocalTLSStorageConfig = (*storageConfig)(nil)
3677+
3678+// StorageDir is a storage local directory
3679+func (c storageConfig) StorageDir() string {
3680+ return c.storageDir
3681+}
3682+
3683+// StorageAddr is a storage IP address and port
3684+func (c storageConfig) StorageAddr() string {
3685+ return fmt.Sprintf("%s:%d", c.storageAddr, c.storagePort)
3686+}
3687+
3688+// StorageCACert is the CA certificate in PEM format.
3689+func (c storageConfig) StorageCACert() string {
3690+ if cert, ok := c.ecfg.CACert(); ok {
3691+ return cert
3692+ }
3693+ return ""
3694+}
3695+
3696+// StorageCAKey is the CA private key in PEM format.
3697+func (c storageConfig) StorageCAKey() string {
3698+ if key, ok := c.ecfg.CAPrivateKey(); ok {
3699+ return key
3700+ }
3701+ return ""
3702+}
3703+
3704+// StorageHostnames is the set of hostnames that will
3705+// be assigned to the storage server's certificate.
3706+func (c storageConfig) StorageHostnames() []string {
3707+ return []string{c.storageAddr}
3708+}
3709+
3710+// StorageAuthKey is the key that clients must present
3711+// to perform modifying operations.
3712+func (c storageConfig) StorageAuthKey() string {
3713+ return c.ecfg.storageAuthKey()
3714+}
3715
3716=== added file 'provider/cloudsigma/storageconfig_test.go'
3717--- provider/cloudsigma/storageconfig_test.go 1970-01-01 00:00:00 +0000
3718+++ provider/cloudsigma/storageconfig_test.go 2014-06-09 11:10:33 +0000
3719@@ -0,0 +1,58 @@
3720+// Copyright 2014 Canonical Ltd.
3721+// Licensed under the AGPLv3, see LICENCE file for details.
3722+
3723+package cloudsigma
3724+
3725+import (
3726+ gc "launchpad.net/gocheck"
3727+ "launchpad.net/juju-core/environs/config"
3728+ "launchpad.net/juju-core/testing"
3729+)
3730+
3731+func newStorageConfig(c *gc.C, attrs testing.Attrs) *storageConfig {
3732+ attrs = testing.FakeConfig().Merge(attrs)
3733+ cfg, err := config.New(config.NoDefaults, attrs)
3734+ c.Assert(err, gc.IsNil)
3735+ ecfg, err := validateConfig(cfg, nil)
3736+ c.Assert(err, gc.IsNil)
3737+ return &storageConfig{ecfg: ecfg}
3738+}
3739+
3740+type StorageConfigSuite struct {
3741+ testing.BaseSuite
3742+}
3743+
3744+var _ = gc.Suite(&StorageConfigSuite{})
3745+
3746+func (s *StorageConfigSuite) TestStorageConfig(c *gc.C) {
3747+ cfg := newStorageConfig(c, validAttrs())
3748+ cfg.storageDir = "dir"
3749+ cfg.storageAddr = "addr"
3750+ cfg.storagePort = 8080
3751+ c.Check(cfg.StorageDir(), gc.Equals, "dir")
3752+ c.Check(cfg.StorageAddr(), gc.Equals, "addr:8080")
3753+ c.Check(cfg.StorageCACert(), gc.Equals, testing.CACert)
3754+ c.Check(cfg.StorageCAKey(), gc.Equals, testing.CAKey)
3755+ c.Check(cfg.StorageAuthKey(), gc.Equals, "ABCDEFGH")
3756+
3757+ hostnames := cfg.StorageHostnames()
3758+ c.Assert(hostnames, gc.HasLen, 1)
3759+ c.Check(hostnames[0], gc.Equals, "addr")
3760+}
3761+
3762+func (s *StorageConfigSuite) TestStorageConfigEmpty(c *gc.C) {
3763+ cfg := storageConfig{
3764+ ecfg: &environConfig{
3765+ Config: &config.Config{},
3766+ },
3767+ }
3768+ c.Check(cfg.StorageDir(), gc.Equals, "")
3769+ c.Check(cfg.StorageAddr(), gc.Equals, ":0")
3770+ c.Check(cfg.StorageCACert(), gc.Equals, "")
3771+ c.Check(cfg.StorageCAKey(), gc.Equals, "")
3772+ c.Check(cfg.StorageAuthKey(), gc.Equals, "")
3773+
3774+ hostnames := cfg.StorageHostnames()
3775+ c.Assert(hostnames, gc.HasLen, 1)
3776+ c.Check(hostnames[0], gc.Equals, "")
3777+}

Subscribers

People subscribed via source and target branches

to status/vote changes: