Merge lp:~axwalk/goose/nova-availability-zones into lp:goose

Proposed by Andrew Wilkins on 2014-06-06
Status: Merged
Approved by: Martin Packman on 2014-06-10
Approved revision: 127
Merged at revision: 125
Proposed branch: lp:~axwalk/goose/nova-availability-zones
Merge into: lp:goose
Diff against target: 634 lines (+301/-70)
10 files modified
errors/errors.go (+22/-6)
errors/errors_test.go (+14/-0)
nova/live_test.go (+37/-20)
nova/local_test.go (+45/-0)
nova/nova.go (+45/-6)
testservices/errors.go (+4/-0)
testservices/novaservice/service.go (+62/-20)
testservices/novaservice/service_http.go (+47/-18)
testservices/novaservice/service_http_test.go (+21/-0)
testservices/service.go (+4/-0)
To merge this branch: bzr merge lp:~axwalk/goose/nova-availability-zones
Reviewer Review Type Date Requested Status
Juju Engineering 2014-06-06 Pending
Review via email: mp+222275@code.launchpad.net

Commit message

Add support for Availability Zones

- Added ListAvailabilityZones to Nova client
- Added AvailabilityZone field to RunServerOpts
- Added AvailabilityZone field to ServerDetail
- Updated nova test-service to support all of the above

Availability zones are an OpenStack extension,
so I've made it so that ListAvailabilityZones will
ignore any 404 to the os-availability-zone URL.

https://codereview.appspot.com/103900045/

R=gz

Description of the change

Add support for Availability Zones

- Added ListAvailabilityZones to Nova client
- Added AvailabilityZone field to RunServerOpts
- Added AvailabilityZone field to ServerDetail
- Updated nova test-service to support all of the above

Availability zones are an OpenStack extension,
so I've made it so that ListAvailabilityZones will
ignore any 404 to the os-availability-zone URL.

https://codereview.appspot.com/103900045/

To post a comment you must log in.
Andrew Wilkins (axwalk) wrote :

Reviewers: mp+222275_code.launchpad.net,

Message:
Please take a look.

Description:
Add support for Availability Zones

- Added ListAvailabilityZones to Nova client
- Added AvailabilityZone field to RunServerOpts
- Added AvailabilityZone field to ServerDetail
- Updated nova test-service to support all of the above

Availability zones are an OpenStack extension,
so I've made it so that ListAvailabilityZones will
ignore any 404 to the os-availability-zone URL.

https://code.launchpad.net/~axwalk/goose/nova-availability-zones/+merge/222275

(do not edit description out of merge proposal)

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

Affected files (+260, -64 lines):
   A [revision details]
   M nova/live_test.go
   M nova/local_test.go
   M nova/nova.go
   M testservices/novaservice/service.go
   M testservices/novaservice/service_http.go
   M testservices/novaservice/service_http_test.go

Dave Cheney (dave-cheney) wrote :

https://codereview.appspot.com/103900045/diff/1/nova/local_test.go
File nova/local_test.go (right):

https://codereview.appspot.com/103900045/diff/1/nova/local_test.go#newcode251
nova/local_test.go:251: c.Assert(err, IsNil)
Are these zones guaranteed to be returned in order? Should this be
jc.SameContents

https://codereview.appspot.com/103900045/diff/1/testservices/novaservice/service.go
File testservices/novaservice/service.go (right):

https://codereview.appspot.com/103900045/diff/1/testservices/novaservice/service.go#newcode826
testservices/novaservice/service.go:826: sort.Sort(azByName(zones))
Please document the order of az's is stored.

https://codereview.appspot.com/103900045/diff/1/testservices/novaservice/service_http_test.go
File testservices/novaservice/service_http_test.go (right):

https://codereview.appspot.com/103900045/diff/1/testservices/novaservice/service_http_test.go#newcode1196
testservices/novaservice/service_http_test.go:1196:
c.Assert(expected.Zones, DeepEquals, zones)
jc.SameContents

https://codereview.appspot.com/103900045/

126. By Andrew Wilkins on 2014-06-06

Add comment on ordering of zones

Andrew Wilkins (axwalk) wrote :

Please take a look.

https://codereview.appspot.com/103900045/diff/1/nova/local_test.go
File nova/local_test.go (right):

https://codereview.appspot.com/103900045/diff/1/nova/local_test.go#newcode251
nova/local_test.go:251: c.Assert(err, IsNil)
On 2014/06/06 04:33:17, dfc wrote:
> Are these zones guaranteed to be returned in order? Should this be
> jc.SameContents

Yes, they are sorted by the server.

https://codereview.appspot.com/103900045/diff/1/testservices/novaservice/service.go
File testservices/novaservice/service.go (right):

https://codereview.appspot.com/103900045/diff/1/testservices/novaservice/service.go#newcode826
testservices/novaservice/service.go:826: sort.Sort(azByName(zones))
On 2014/06/06 04:33:17, dfc wrote:
> Please document the order of az's is stored.

Added to the doc comment of this function. There's no user-facing place
to add a comment.

https://codereview.appspot.com/103900045/diff/1/testservices/novaservice/service_http_test.go
File testservices/novaservice/service_http_test.go (right):

https://codereview.appspot.com/103900045/diff/1/testservices/novaservice/service_http_test.go#newcode1196
testservices/novaservice/service_http_test.go:1196:
c.Assert(expected.Zones, DeepEquals, zones)
On 2014/06/06 04:33:17, dfc wrote:
> jc.SameContents

They are sorted by the server. No need to introduce a dependency on
juju/testing/checkers here.

https://codereview.appspot.com/103900045/

Martin Packman (gz) wrote :

Do any of our openstack clouds actually support this extension? Neither
canonistack nor HP seem to. I'm not totally up to dat on current plans,
but this seemed to be a dead end a year ago, with different mechanisms
talked about as fulfilling the role that weren't availabilty zone based.

https://codereview.appspot.com/103900045/

Andrew Wilkins (axwalk) wrote :

On 2014/06/07 14:54:10, gz wrote:
> Do any of our openstack clouds actually support this extension?
Neither
> canonistack nor HP seem to. I'm not totally up to dat on current
plans, but this
> seemed to be a dead end a year ago, with different mechanisms talked
about as
> fulfilling the role that weren't availabilty zone based.

HP's US West has 4 availability zones. I've tested this live.

https://codereview.appspot.com/103900045/

Martin Packman (gz) wrote :

Ah, interesting. I wasn't on their 12.12 deployment (which is being
retired Monday), so must have been added for the 13.5 one.

https://codereview.appspot.com/103900045/

Martin Packman (gz) wrote :

LGTM.

https://codereview.appspot.com/103900045/diff/20001/nova/local_test.go
File nova/local_test.go (right):

https://codereview.appspot.com/103900045/diff/20001/nova/local_test.go#newcode231
nova/local_test.go:231: s.openstack.Nova.SetAvailabilityZones()
This is pretty magical test server behaviour, but you have a nice big
comment at least.

https://codereview.appspot.com/103900045/diff/20001/nova/nova.go
File nova/nova.go (right):

https://codereview.appspot.com/103900045/diff/20001/nova/nova.go#newcode695
nova/nova.go:695: return nil, nil
I'd prefer we return a specific error that can be checked, but that's
not really a job for this proposal, we need better goose-wide handling
of extensions and errors.

https://codereview.appspot.com/103900045/diff/20001/testservices/novaservice/service_http.go
File testservices/novaservice/service_http.go (right):

https://codereview.appspot.com/103900045/diff/20001/testservices/novaservice/service_http.go#newcode228
testservices/novaservice/service_http.go:228:
errAvailabilityZoneIsNotAvailable = &errorResponse{
Rather than doing this the old way, you should use the new
testservices/errors.go method of creating the error.

https://codereview.appspot.com/103900045/

127. By Andrew Wilkins on 2014-06-09

Address review comments

- Return an error if AZ extension is not supported
- Use new testservices/errors

Andrew Wilkins (axwalk) wrote :

Please take a look.

https://codereview.appspot.com/103900045/diff/20001/nova/nova.go
File nova/nova.go (right):

https://codereview.appspot.com/103900045/diff/20001/nova/nova.go#newcode695
nova/nova.go:695: return nil, nil
On 2014/06/07 17:13:03, gz wrote:
> I'd prefer we return a specific error that can be checked, but that's
not really
> a job for this proposal, we need better goose-wide handling of
extensions and
> errors.

I think you're right, and I'd prefer to get the API right to begin with.
The error type/method of checking may change later, but at least we
should start out with propagating the error.

I've created a error code in goose/errors for "not implemented".

https://codereview.appspot.com/103900045/diff/20001/testservices/novaservice/service_http.go
File testservices/novaservice/service_http.go (right):

https://codereview.appspot.com/103900045/diff/20001/testservices/novaservice/service_http.go#newcode228
testservices/novaservice/service_http.go:228:
errAvailabilityZoneIsNotAvailable = &errorResponse{
On 2014/06/07 17:13:03, gz wrote:
> Rather than doing this the old way, you should use the new
> testservices/errors.go method of creating the error.

Done.

https://codereview.appspot.com/103900045/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'errors/errors.go'
2--- errors/errors.go 2013-04-11 06:31:23 +0000
3+++ errors/errors.go 2014-06-09 01:36:50 +0000
4@@ -15,6 +15,7 @@
5 DuplicateValueError = Code("DuplicateValue")
6 TimeoutError = Code("Timeout")
7 UnauthorisedError = Code("Unauthorised")
8+ NotImplementedError = Code("NotImplemented")
9 )
10
11 // Error instances store an optional error cause.
12@@ -95,7 +96,14 @@
13 return false
14 }
15
16-// New creates a new Error instance with the specified cause.
17+func IsNotImplemented(err error) bool {
18+ if e, ok := err.(*gooseError); ok {
19+ return e.causedBy(NotImplementedError)
20+ }
21+ return false
22+}
23+
24+// makeErrorf creates a new Error instance with the specified cause.
25 func makeErrorf(code Code, cause error, format string, args ...interface{}) Error {
26 return &gooseError{
27 errcode: code,
28@@ -104,12 +112,12 @@
29 }
30 }
31
32-// New creates a new Unspecified Error instance with the specified cause.
33+// Newf creates a new Unspecified Error instance with the specified cause.
34 func Newf(cause error, format string, args ...interface{}) Error {
35 return makeErrorf(UnspecifiedError, cause, format, args...)
36 }
37
38-// New creates a new NotFound Error instance with the specified cause.
39+// NewNotFoundf creates a new NotFound Error instance with the specified cause.
40 func NewNotFoundf(cause error, context interface{}, format string, args ...interface{}) Error {
41 if format == "" {
42 format = fmt.Sprintf("Not found: %s", context)
43@@ -117,7 +125,7 @@
44 return makeErrorf(NotFoundError, cause, format, args...)
45 }
46
47-// New creates a new DuplicateValue Error instance with the specified cause.
48+// NewDuplicateValuef creates a new DuplicateValue Error instance with the specified cause.
49 func NewDuplicateValuef(cause error, context interface{}, format string, args ...interface{}) Error {
50 if format == "" {
51 format = fmt.Sprintf("Duplicate: %s", context)
52@@ -125,7 +133,7 @@
53 return makeErrorf(DuplicateValueError, cause, format, args...)
54 }
55
56-// New creates a new Timeout Error instance with the specified cause.
57+// NewTimeoutf creates a new Timeout Error instance with the specified cause.
58 func NewTimeoutf(cause error, context interface{}, format string, args ...interface{}) Error {
59 if format == "" {
60 format = fmt.Sprintf("Timeout: %s", context)
61@@ -133,10 +141,18 @@
62 return makeErrorf(TimeoutError, cause, format, args...)
63 }
64
65-// New creates a new Unauthorised Error instance with the specified cause.
66+// NewUnauthorisedf creates a new Unauthorised Error instance with the specified cause.
67 func NewUnauthorisedf(cause error, context interface{}, format string, args ...interface{}) Error {
68 if format == "" {
69 format = fmt.Sprintf("Unauthorised: %s", context)
70 }
71 return makeErrorf(UnauthorisedError, cause, format, args...)
72 }
73+
74+// NewNotImplementedf creates a new NotImplemented Error instance with the specified cause.
75+func NewNotImplementedf(cause error, context interface{}, format string, args ...interface{}) Error {
76+ if format == "" {
77+ format = fmt.Sprintf("Not implemented: %s", context)
78+ }
79+ return makeErrorf(NotImplementedError, cause, format, args...)
80+}
81
82=== modified file 'errors/errors_test.go'
83--- errors/errors_test.go 2013-03-28 09:06:20 +0000
84+++ errors/errors_test.go 2014-06-09 01:36:50 +0000
85@@ -55,6 +55,20 @@
86 c.Assert(err.Error(), Equals, "It was unauthorised: context")
87 }
88
89+func (s *ErrorsSuite) TestCreateSimpleNotImplementedfError(c *C) {
90+ context := "context"
91+ err := errors.NewNotImplementedf(nil, context, "")
92+ c.Assert(errors.IsNotImplemented(err), Equals, true)
93+ c.Assert(err.Error(), Equals, "Not implemented: context")
94+}
95+
96+func (s *ErrorsSuite) TestCreateNotImplementedfError(c *C) {
97+ context := "context"
98+ err := errors.NewNotImplementedf(nil, context, "It was not implemented: %s", context)
99+ c.Assert(errors.IsNotImplemented(err), Equals, true)
100+ c.Assert(err.Error(), Equals, "It was not implemented: context")
101+}
102+
103 func (s *ErrorsSuite) TestErrorCause(c *C) {
104 rootCause := errors.NewNotFoundf(nil, "some value", "")
105 // Construct a new error, based on a not found root cause.
106
107=== modified file 'nova/live_test.go'
108--- nova/live_test.go 2014-01-16 14:53:18 +0000
109+++ nova/live_test.go 2014-06-09 01:36:50 +0000
110@@ -28,17 +28,18 @@
111 }
112
113 type LiveTests struct {
114- cred *identity.Credentials
115- client client.AuthenticatingClient
116- nova *nova.Client
117- testServer *nova.Entity
118- userId string
119- tenantId string
120- testImageId string
121- testFlavor string
122- testFlavorId string
123- vendor string
124- useNumericIds bool
125+ cred *identity.Credentials
126+ client client.AuthenticatingClient
127+ nova *nova.Client
128+ testServer *nova.Entity
129+ userId string
130+ tenantId string
131+ testImageId string
132+ testFlavor string
133+ testFlavorId string
134+ testAvailabilityZone string
135+ vendor string
136+ useNumericIds bool
137 }
138
139 func (s *LiveTests) SetUpSuite(c *C) {
140@@ -47,7 +48,7 @@
141 var err error
142 s.testFlavorId, err = s.findFlavorId(s.testFlavor)
143 c.Assert(err, IsNil)
144- s.testServer, err = s.createInstance(c, testImageName)
145+ s.testServer, err = s.createInstance(testImageName)
146 c.Assert(err, IsNil)
147 s.waitTestServerToStart(c)
148 // These will not be filled in until a client has authorised which will happen creating the instance above.
149@@ -88,12 +89,13 @@
150 // noop, called by local test suite.
151 }
152
153-func (s *LiveTests) createInstance(c *C, name string) (instance *nova.Entity, err error) {
154+func (s *LiveTests) createInstance(name string) (instance *nova.Entity, err error) {
155 opts := nova.RunServerOpts{
156- Name: name,
157- FlavorId: s.testFlavorId,
158- ImageId: s.testImageId,
159- UserData: nil,
160+ Name: name,
161+ FlavorId: s.testFlavorId,
162+ ImageId: s.testImageId,
163+ AvailabilityZone: s.testAvailabilityZone,
164+ UserData: nil,
165 }
166 instance, err = s.nova.RunServer(opts)
167 if err != nil {
168@@ -108,6 +110,9 @@
169 c.Check(sr.Name, Equals, testImageName)
170 c.Check(sr.Flavor.Id, Equals, s.testFlavorId)
171 c.Check(sr.Image.Id, Equals, s.testImageId)
172+ if s.testAvailabilityZone != "" {
173+ c.Check(sr.AvailabilityZone, Equals, s.testAvailabilityZone)
174+ }
175 }
176
177 func (s *LiveTests) TestListFlavors(c *C) {
178@@ -163,7 +168,7 @@
179 }
180
181 func (s *LiveTests) TestListServersWithFilter(c *C) {
182- inst, err := s.createInstance(c, "filtered_server")
183+ inst, err := s.createInstance("filtered_server")
184 c.Assert(err, IsNil)
185 defer s.nova.DeleteServer(inst.Id)
186 filter := nova.NewFilter()
187@@ -224,7 +229,7 @@
188 }
189
190 func (s *LiveTests) TestListServersDetailWithFilter(c *C) {
191- inst, err := s.createInstance(c, "filtered_server")
192+ inst, err := s.createInstance("filtered_server")
193 c.Assert(err, IsNil)
194 defer s.nova.DeleteServer(inst.Id)
195 filter := nova.NewFilter()
196@@ -488,7 +493,7 @@
197 "123barbaz",
198 }
199 for _, name := range serverNames {
200- inst, err := s.createInstance(c, name)
201+ inst, err := s.createInstance(name)
202 c.Assert(err, IsNil)
203 defer s.nova.DeleteServer(inst.Id)
204 }
205@@ -518,3 +523,15 @@
206 c.Assert(network.Cidr, Matches, `\d{1,3}(\.+\d{1,3}){3}\/\d+`)
207 }
208 }
209+
210+func (s *LiveTests) runServerAvailabilityZone(zone string) (*nova.Entity, error) {
211+ old := s.testAvailabilityZone
212+ defer func() { s.testAvailabilityZone = old }()
213+ s.testAvailabilityZone = zone
214+ return s.createInstance(testImageName)
215+}
216+
217+func (s *LiveTests) TestRunServerUnknownAvailabilityZone(c *C) {
218+ _, err := s.runServerAvailabilityZone("something_that_will_never_exist")
219+ c.Assert(err, ErrorMatches, "(.|\n)*The requested availability zone is not available(.|\n)*")
220+}
221
222=== modified file 'nova/local_test.go'
223--- nova/local_test.go 2013-11-25 05:52:30 +0000
224+++ nova/local_test.go 2014-06-09 01:36:50 +0000
225@@ -223,3 +223,48 @@
226 _, err := novaClient.ListServers(nil)
227 c.Assert(errors.IsUnauthorised(err), Equals, true)
228 }
229+
230+func (s *localLiveSuite) TestListAvailabilityZonesUnimplemented(c *C) {
231+ // When the test service has no availability zones registered,
232+ // the /os-availability-zone API will return 404. We swallow
233+ // that error.
234+ s.openstack.Nova.SetAvailabilityZones()
235+ listedZones, err := s.nova.ListAvailabilityZones()
236+ c.Assert(err, ErrorMatches, "the server does not support availability zones(.|\n)*")
237+ c.Assert(listedZones, HasLen, 0)
238+}
239+
240+func (s *localLiveSuite) setAvailabilityZones() []nova.AvailabilityZone {
241+ zones := []nova.AvailabilityZone{
242+ nova.AvailabilityZone{Name: "az1"},
243+ nova.AvailabilityZone{
244+ Name: "az2", State: nova.AvailabilityZoneState{Available: true},
245+ },
246+ }
247+ s.openstack.Nova.SetAvailabilityZones(zones...)
248+ return zones
249+}
250+
251+func (s *localLiveSuite) TestListAvailabilityZones(c *C) {
252+ zones := s.setAvailabilityZones()
253+ listedZones, err := s.nova.ListAvailabilityZones()
254+ c.Assert(err, IsNil)
255+ c.Assert(listedZones, DeepEquals, zones)
256+}
257+
258+func (s *localLiveSuite) TestRunServerAvailabilityZone(c *C) {
259+ s.setAvailabilityZones()
260+ inst, err := s.runServerAvailabilityZone("az2")
261+ c.Assert(err, IsNil)
262+ defer s.nova.DeleteServer(inst.Id)
263+ server, err := s.nova.GetServer(inst.Id)
264+ c.Assert(err, IsNil)
265+ c.Assert(server.AvailabilityZone, Equals, "az2")
266+}
267+
268+func (s *localLiveSuite) TestRunServerAvailabilityZoneNotAvailable(c *C) {
269+ s.setAvailabilityZones()
270+ // az1 is known, but not currently available.
271+ _, err := s.runServerAvailabilityZone("az1")
272+ c.Assert(err, ErrorMatches, "(.|\n)*The requested availability zone is not available(.|\n)*")
273+}
274
275=== modified file 'nova/nova.go'
276--- nova/nova.go 2014-01-24 16:33:31 +0000
277+++ nova/nova.go 2014-06-09 01:36:50 +0000
278@@ -22,6 +22,7 @@
279 apiSecurityGroups = "os-security-groups"
280 apiSecurityGroupRules = "os-security-group-rules"
281 apiFloatingIPs = "os-floating-ips"
282+ apiAvailabilityZone = "os-availability-zone"
283 )
284
285 // Server status values.
286@@ -245,6 +246,8 @@
287 Updated string
288
289 UserId string `json:"user_id"`
290+
291+ AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"`
292 }
293
294 // ListServersDetail lists all details for available servers.
295@@ -307,12 +310,13 @@
296
297 // RunServerOpts defines required and optional arguments for RunServer().
298 type RunServerOpts struct {
299- Name string `json:"name"` // Required
300- FlavorId string `json:"flavorRef"` // Required
301- ImageId string `json:"imageRef"` // Required
302- UserData []byte `json:"user_data"` // Optional
303- SecurityGroupNames []SecurityGroupName `json:"security_groups"` // Optional
304- Networks []ServerNetworks `json:"networks"` // Optional
305+ Name string `json:"name"` // Required
306+ FlavorId string `json:"flavorRef"` // Required
307+ ImageId string `json:"imageRef"` // Required
308+ UserData []byte `json:"user_data"` // Optional
309+ SecurityGroupNames []SecurityGroupName `json:"security_groups"` // Optional
310+ Networks []ServerNetworks `json:"networks"` // Optional
311+ AvailabilityZone string `json:"availability_zone,omitempty"` // Optional
312 }
313
314 // RunServer creates a new server, based on the given RunServerOpts.
315@@ -666,3 +670,38 @@
316 }
317 return err
318 }
319+
320+// AvailabilityZone identifies an availability zone, and describes its state.
321+type AvailabilityZone struct {
322+ Name string `json:"zoneName"`
323+ State AvailabilityZoneState `json:"zoneState"`
324+}
325+
326+// AvailabilityZoneState describes an availability zone's state.
327+type AvailabilityZoneState struct {
328+ Available bool
329+}
330+
331+// ListAvailabilityZones lists all availability zones.
332+//
333+// Availability zones are an OpenStack extension; if the server does not
334+// support them, then an error satisfying errors.IsNotImplemented will be
335+// returned.
336+func (c *Client) ListAvailabilityZones() ([]AvailabilityZone, error) {
337+ var resp struct {
338+ AvailabilityZoneInfo []AvailabilityZone
339+ }
340+ requestData := goosehttp.RequestData{RespValue: &resp}
341+ err := c.client.SendRequest(client.GET, "compute", apiAvailabilityZone, &requestData)
342+ if errors.IsNotFound(err) {
343+ // Availability zones are an extension, so don't
344+ // return an error if the API does not exist.
345+ return nil, errors.NewNotImplementedf(
346+ err, nil, "the server does not support availability zones",
347+ )
348+ }
349+ if err != nil {
350+ return nil, errors.Newf(err, "failed to get list of availability zones")
351+ }
352+ return resp.AvailabilityZoneInfo, nil
353+}
354
355=== modified file 'testservices/errors.go'
356--- testservices/errors.go 2014-05-02 01:12:04 +0000
357+++ testservices/errors.go 2014-06-09 01:36:50 +0000
358@@ -68,6 +68,10 @@
359 return serverErrorf(413, "Retry limit exceeded")
360 }
361
362+func NewAvailabilityZoneIsNotAvailableError() *ServerError {
363+ return serverErrorf(400, "The requested availability zone is not available")
364+}
365+
366 func NewAddFlavorError(id string) *ServerError {
367 return serverErrorf(409, "A flavor with id %q already exists", id)
368 }
369
370=== modified file 'testservices/novaservice/service.go'
371--- testservices/novaservice/service.go 2014-05-02 01:12:04 +0000
372+++ testservices/novaservice/service.go 2014-06-09 01:36:50 +0000
373@@ -9,6 +9,7 @@
374 "launchpad.net/goose/testservices/identityservice"
375 "net/url"
376 "regexp"
377+ "sort"
378 "strings"
379 )
380
381@@ -19,18 +20,19 @@
382 // contains the service double's internal state.
383 type Nova struct {
384 testservices.ServiceInstance
385- flavors map[string]nova.FlavorDetail
386- servers map[string]nova.ServerDetail
387- groups map[string]nova.SecurityGroup
388- rules map[string]nova.SecurityGroupRule
389- floatingIPs map[string]nova.FloatingIP
390- networks map[string]nova.Network
391- serverGroups map[string][]string
392- serverIPs map[string][]string
393- nextServerId int
394- nextGroupId int
395- nextRuleId int
396- nextIPId int
397+ flavors map[string]nova.FlavorDetail
398+ servers map[string]nova.ServerDetail
399+ groups map[string]nova.SecurityGroup
400+ rules map[string]nova.SecurityGroupRule
401+ floatingIPs map[string]nova.FloatingIP
402+ networks map[string]nova.Network
403+ serverGroups map[string][]string
404+ serverIPs map[string][]string
405+ availabilityZones map[string]nova.AvailabilityZone
406+ nextServerId int
407+ nextGroupId int
408+ nextRuleId int
409+ nextIPId int
410 }
411
412 func errorJSONEncode(err error) (int, string) {
413@@ -86,14 +88,15 @@
414 {Id: "999", Name: "default", Description: "default group"},
415 }
416 novaService := &Nova{
417- flavors: make(map[string]nova.FlavorDetail),
418- servers: make(map[string]nova.ServerDetail),
419- groups: make(map[string]nova.SecurityGroup),
420- rules: make(map[string]nova.SecurityGroupRule),
421- floatingIPs: make(map[string]nova.FloatingIP),
422- networks: make(map[string]nova.Network),
423- serverGroups: make(map[string][]string),
424- serverIPs: make(map[string][]string),
425+ flavors: make(map[string]nova.FlavorDetail),
426+ servers: make(map[string]nova.ServerDetail),
427+ groups: make(map[string]nova.SecurityGroup),
428+ rules: make(map[string]nova.SecurityGroupRule),
429+ floatingIPs: make(map[string]nova.FloatingIP),
430+ networks: make(map[string]nova.Network),
431+ serverGroups: make(map[string][]string),
432+ serverIPs: make(map[string][]string),
433+ availabilityZones: make(map[string]nova.AvailabilityZone),
434 ServiceInstance: testservices.ServiceInstance{
435 IdentityService: identityService,
436 Scheme: URL.Scheme,
437@@ -130,6 +133,21 @@
438 return novaService
439 }
440
441+// SetAvailabilityZones sets the availability zones for setting
442+// availability zones.
443+//
444+// Note: this is implemented as a public method rather than as
445+// an HTTP API for two reasons: availability zones are created
446+// indirectly via "host aggregates", which are a cloud-provider
447+// concept that we have not implemented, and because we want to
448+// be able to synthesize zone state changes.
449+func (n *Nova) SetAvailabilityZones(zones ...nova.AvailabilityZone) {
450+ n.availabilityZones = make(map[string]nova.AvailabilityZone)
451+ for _, z := range zones {
452+ n.availabilityZones[z.Name] = z
453+ }
454+}
455+
456 // buildFlavorLinks populates the Links field of the passed
457 // FlavorDetail as needed by OpenStack HTTP API. Call this
458 // before addFlavor().
459@@ -799,3 +817,27 @@
460 }
461 return networks
462 }
463+
464+// allAvailabilityZones returns a list of all existing availability zones,
465+// sorted by name.
466+func (n *Nova) allAvailabilityZones() (zones []nova.AvailabilityZone) {
467+ for _, zone := range n.availabilityZones {
468+ zones = append(zones, zone)
469+ }
470+ sort.Sort(azByName(zones))
471+ return zones
472+}
473+
474+type azByName []nova.AvailabilityZone
475+
476+func (a azByName) Len() int {
477+ return len(a)
478+}
479+
480+func (a azByName) Less(i, j int) bool {
481+ return a[i].Name < a[j].Name
482+}
483+
484+func (a azByName) Swap(i, j int) {
485+ a[i], a[j] = a[j], a[i]
486+}
487
488=== modified file 'testservices/novaservice/service_http.go'
489--- testservices/novaservice/service_http.go 2014-05-16 06:14:14 +0000
490+++ testservices/novaservice/service_http.go 2014-06-09 01:36:50 +0000
491@@ -536,12 +536,13 @@
492 func (n *Nova) handleRunServer(body []byte, w http.ResponseWriter, r *http.Request) error {
493 var req struct {
494 Server struct {
495- FlavorRef string
496- ImageRef string
497- Name string
498- Metadata map[string]string
499- SecurityGroups []map[string]string `json:"security_groups"`
500- Networks []map[string]string
501+ FlavorRef string
502+ ImageRef string
503+ Name string
504+ Metadata map[string]string
505+ SecurityGroups []map[string]string `json:"security_groups"`
506+ Networks []map[string]string
507+ AvailabilityZone string `json:"availability_zone"`
508 }
509 }
510 if err := json.Unmarshal(body, &req); err != nil {
511@@ -556,6 +557,11 @@
512 if req.Server.FlavorRef == "" {
513 return errBadRequestSrvFlavor
514 }
515+ if az := req.Server.AvailabilityZone; az != "" {
516+ if !n.availabilityZones[az].State.Available {
517+ return testservices.AvailabilityZoneIsNotAvailable
518+ }
519+ }
520 n.nextServerId++
521 id := strconv.Itoa(n.nextServerId)
522 uuid, err := newUUID()
523@@ -590,18 +596,19 @@
524 timestr := time.Now().Format(time.RFC3339)
525 userInfo, _ := userInfo(n.IdentityService, r)
526 server := nova.ServerDetail{
527- Id: id,
528- UUID: uuid,
529- Name: req.Server.Name,
530- TenantId: n.TenantId,
531- UserId: userInfo.Id,
532- HostId: "1",
533- Image: image,
534- Flavor: flavorEnt,
535- Status: nova.StatusActive,
536- Created: timestr,
537- Updated: timestr,
538- Addresses: make(map[string][]nova.IPAddress),
539+ Id: id,
540+ UUID: uuid,
541+ Name: req.Server.Name,
542+ TenantId: n.TenantId,
543+ UserId: userInfo.Id,
544+ HostId: "1",
545+ Image: image,
546+ Flavor: flavorEnt,
547+ Status: nova.StatusActive,
548+ Created: timestr,
549+ Updated: timestr,
550+ Addresses: make(map[string][]nova.IPAddress),
551+ AvailabilityZone: req.Server.AvailabilityZone,
552 }
553 nextServer := len(n.allServers(nil)) + 1
554 n.buildServerLinks(&server)
555@@ -1019,6 +1026,27 @@
556 return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
557 }
558
559+// handleAvailabilityZones handles the os-availability-zone HTTP API.
560+func (n *Nova) handleAvailabilityZones(w http.ResponseWriter, r *http.Request) error {
561+ switch r.Method {
562+ case "GET":
563+ if ipId := path.Base(r.URL.Path); ipId != "os-availability-zone" {
564+ return errNotFoundJSON
565+ }
566+ zones := n.allAvailabilityZones()
567+ if len(zones) == 0 {
568+ // If there are no availability zones defined, act as
569+ // if we don't support the availability zones extension.
570+ return errNotFoundJSON
571+ }
572+ resp := struct {
573+ Zones []nova.AvailabilityZone `json:"availabilityZoneInfo"`
574+ }{zones}
575+ return sendJSON(http.StatusOK, resp, w, r)
576+ }
577+ return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
578+}
579+
580 // setupHTTP attaches all the needed handlers to provide the HTTP API.
581 func (n *Nova) SetupHTTP(mux *http.ServeMux) {
582 handlers := map[string]http.Handler{
583@@ -1033,6 +1061,7 @@
584 "/$v/$t/os-security-group-rules": n.handler((*Nova).handleSecurityGroupRules),
585 "/$v/$t/os-floating-ips": n.handler((*Nova).handleFloatingIPs),
586 "/$v/$t/os-networks": n.handler((*Nova).handleNetworks),
587+ "/$v/$t/os-availability-zone": n.handler((*Nova).handleAvailabilityZones),
588 }
589 for path, h := range handlers {
590 path = strings.Replace(path, "$v", n.VersionPath, 1)
591
592=== modified file 'testservices/novaservice/service_http_test.go'
593--- testservices/novaservice/service_http_test.go 2014-05-02 01:12:04 +0000
594+++ testservices/novaservice/service_http_test.go 2014-06-09 01:36:50 +0000
595@@ -1175,6 +1175,27 @@
596 c.Assert(s.service.hasServerFloatingIP(server.Id, fip.IP), Equals, false)
597 }
598
599+func (s *NovaHTTPSuite) TestListAvailabilityZones(c *C) {
600+ resp, err := s.jsonRequest("GET", "/os-availability-zone", nil, nil)
601+ c.Assert(err, IsNil)
602+ assertBody(c, resp, errNotFoundJSON)
603+
604+ zones := []nova.AvailabilityZone{
605+ nova.AvailabilityZone{Name: "az1"},
606+ nova.AvailabilityZone{
607+ Name: "az2", State: nova.AvailabilityZoneState{Available: true},
608+ },
609+ }
610+ s.service.SetAvailabilityZones(zones...)
611+ resp, err = s.jsonRequest("GET", "/os-availability-zone", nil, nil)
612+ c.Assert(err, IsNil)
613+ var expected struct {
614+ Zones []nova.AvailabilityZone `json:"availabilityZoneInfo"`
615+ }
616+ assertJSON(c, resp, &expected)
617+ c.Assert(expected.Zones, DeepEquals, zones)
618+}
619+
620 func (s *NovaHTTPSSuite) SetUpSuite(c *C) {
621 s.HTTPSuite.SetUpSuite(c)
622 identityDouble := identityservice.NewUserPass()
623
624=== modified file 'testservices/service.go'
625--- testservices/service.go 2014-04-30 19:30:20 +0000
626+++ testservices/service.go 2014-06-09 01:36:50 +0000
627@@ -32,3 +32,7 @@
628
629 // IPLimitExceeded corresponds to "HTTP 413 Maximum number of floating ips exceeded"
630 var IPLimitExceeded = NewIPLimitExceededError()
631+
632+// AvailabilityZoneIsNotAvailable corresponds to
633+// "HTTP 400 The requested availability zone is not available"
634+var AvailabilityZoneIsNotAvailable = NewAvailabilityZoneIsNotAvailableError()

Subscribers

People subscribed via source and target branches