Merge lp:~dimitern/juju-core/033-openstack-pick-instance-type-based-on-constraints into lp:~juju/juju-core/trunk

Proposed by Dimiter Naydenov
Status: Work in progress
Proposed branch: lp:~dimitern/juju-core/033-openstack-pick-instance-type-based-on-constraints
Merge into: lp:~juju/juju-core/trunk
Diff against target: 252 lines (+127/-46)
4 files modified
environs/openstack/export_test.go (+8/-6)
environs/openstack/image.go (+105/-31)
environs/openstack/live_test.go (+8/-4)
environs/openstack/provider.go (+6/-5)
To merge this branch: bzr merge lp:~dimitern/juju-core/033-openstack-pick-instance-type-based-on-constraints
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+158889@code.launchpad.net

Description of the change

openstack: pick flavor based on constraints

WIP, for discussion
Not tested properly yet.

https://codereview.appspot.com/8753044/

To post a comment you must log in.
Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Reviewers: mp+158889_code.launchpad.net,

Message:
Comments on implementation as agreed online.

https://codereview.appspot.com/8753044/diff/1/environs/openstack/image.go
File environs/openstack/image.go (right):

https://codereview.appspot.com/8753044/diff/1/environs/openstack/image.go#newcode110
environs/openstack/image.go:110: if flavor.RAM >= minMemoryForMongoDB {
use minMemory IFF there's no explicit memory constraint.

Also: remove the MongoDB context from the name.

Description:
openstack: pick flavor based on constraints

WIP, for discussion
Not tested properly yet.

https://code.launchpad.net/~dimitern/juju-core/033-openstack-pick-instance-type-based-on-constraints/+merge/158889

(do not edit description out of merge proposal)

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

Affected files:
   A [revision details]
   M environs/openstack/export_test.go
   M environs/openstack/image.go
   M environs/openstack/live_test.go
   M environs/openstack/provider.go

Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Another comment

https://codereview.appspot.com/8753044/diff/1/environs/openstack/image.go
File environs/openstack/image.go (right):

https://codereview.appspot.com/8753044/diff/1/environs/openstack/image.go#newcode75
environs/openstack/image.go:75: allFlavors, err :=
novaClient.ListFlavorsDetail()
extract the logic below in a separate func, taking flavors list and
default flavor name, so it can be tested separately.

https://codereview.appspot.com/8753044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'environs/openstack/export_test.go'
2--- environs/openstack/export_test.go 2013-04-14 22:12:49 +0000
3+++ environs/openstack/export_test.go 2013-04-15 11:03:26 +0000
4@@ -4,6 +4,7 @@
5 "fmt"
6 "launchpad.net/goose/nova"
7 "launchpad.net/goose/swift"
8+ "launchpad.net/juju-core/constraints"
9 "launchpad.net/juju-core/environs"
10 "launchpad.net/juju-core/environs/jujutest"
11 "launchpad.net/juju-core/state"
12@@ -99,13 +100,14 @@
13 return instanceAddress(addresses)
14 }
15
16-func FindInstanceSpec(e environs.Environ, series, arch, flavor string) (imageId, flavorId string, err error) {
17+func FindInstanceSpec(e environs.Environ, series, arch, flavor string, cons constraints.Value) (imageId, flavorId string, err error) {
18 env := e.(*environ)
19- spec, err := findInstanceSpec(env, &instanceConstraint{
20- series: series,
21- arch: arch,
22- region: env.ecfg().region(),
23- flavor: flavor,
24+ spec, err := findInstanceSpec(env, &instanceFilter{
25+ series: series,
26+ arch: arch,
27+ region: env.ecfg().region(),
28+ defaultFlavor: flavor,
29+ constraints: cons,
30 })
31 if err == nil {
32 imageId = spec.imageId
33
34=== modified file 'environs/openstack/image.go'
35--- environs/openstack/image.go 2013-01-30 00:18:03 +0000
36+++ environs/openstack/image.go 2013-04-15 11:03:26 +0000
37@@ -2,50 +2,124 @@
38
39 import (
40 "fmt"
41+ "launchpad.net/goose/nova"
42+ "launchpad.net/juju-core/constraints"
43 "launchpad.net/juju-core/environs"
44+ "sort"
45 )
46
47-// instanceConstraint constrains the possible instances that may be
48-// chosen by the Openstack provider.
49-type instanceConstraint struct {
50- series string // Ubuntu release name.
51- arch string
52- region string
53- flavor string
54+// minMemoryForMongoDB is the assumed minimum amount of memory we require in order to run MongoDB (1GB)
55+const minMemoryForMongoDB = 1 << 30
56+
57+// instanceFilter defines a filter to match a suitable OpenStack
58+// flavor to use when starting an instance.
59+type instanceFilter struct {
60+ series string // Ubuntu release name.
61+ arch string
62+ region string
63+ defaultFlavor string
64+ constraints constraints.Value
65 }
66
67-// instanceSpec specifies a particular kind of instance.
68+// instanceSpec specifies a particular combination of flavor and image
69+// to start an instance.
70 type instanceSpec struct {
71 imageId string
72 flavorId string
73 }
74
75-func findInstanceSpec(e *environ, constraint *instanceConstraint) (*instanceSpec, error) {
76- nova := e.nova()
77- flavors, err := nova.ListFlavors()
78+// flavorsByCost defines a list of flavors sortable by cost.
79+type flavorsByCost []nova.FlavorDetail
80+
81+func (f flavorsByCost) Len() int { return len(f) }
82+func (f flavorsByCost) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
83+func (f flavorsByCost) Less(i, j int) bool { return f[i].RAM < f[j].RAM }
84+
85+// reverseByCost implements a reverse sort by cost.
86+type reverseByCost struct {
87+ sort.Interface
88+}
89+
90+func (r reverseByCost) Less(i, j int) bool { return r.Interface.Less(j, i) }
91+
92+// flavorMatches returns true if the given flavor matches the given constraints.
93+func flavorMatches(flavor *nova.FlavorDetail, cons constraints.Value) bool {
94+ if cons.Mem != nil && uint64(flavor.RAM) < *cons.Mem {
95+ return false
96+ }
97+ if cons.CpuCores != nil && uint64(flavor.VCPUs) < *cons.CpuCores {
98+ return false
99+ }
100+ // NOTE: CpuPower constraint is not supported by OpenStack.
101+ return true
102+}
103+
104+// findInstanceSpec picks a suitable combination of flavor and image
105+// based on default flavor and constraints.
106+// TODO(dimitern) also pick image using clouddata once done.
107+func findInstanceSpec(e *environ, filter *instanceFilter) (*instanceSpec, error) {
108+ novaClient := e.nova()
109+
110+ // TODO(wallyworld) - we want to search for an image based on the
111+ // series, arch, region like for ec2 providers and
112+ // http://cloud-images.ubuntu.com but there's nothing to support
113+ // that yet. So we allow the user to configure a default image Id
114+ // to use.
115+ defaultImageId := e.ecfg().defaultImageId()
116+ if defaultImageId == "" {
117+ return nil, fmt.Errorf("Unable to find image for series/arch/region %s/%s/%s and no default specified.",
118+ filter.series, filter.arch, filter.region)
119+ }
120+
121+ // 1. Get all flavors to match against the constraints.
122+ allFlavors, err := novaClient.ListFlavorsDetail()
123 if err != nil {
124 return nil, err
125 }
126- var flavorId string
127- for _, flavor := range flavors {
128- if flavor.Name == constraint.flavor {
129- flavorId = flavor.Id
130- break
131- }
132- }
133- if flavorId == "" {
134- return nil, environs.NotFoundError{fmt.Errorf("No such flavor %s", constraint.flavor)}
135- }
136- // TODO(wallyworld) - we want to search for an image based on the series, arch, region like for ec2 providers
137- // and http://cloud-images.ubuntu.com but there's nothing to support that yet.
138- // So we allow the user to configure a default image Id to use.
139- imageId := e.ecfg().defaultImageId()
140- if imageId == "" {
141- return nil, fmt.Errorf("Unable to find image for series/arch/region %s/%s/%s and no default specified.",
142- constraint.series, constraint.arch, constraint.region)
143- }
144+ matchingFlavors := make(map[string]nova.FlavorDetail)
145+ matchingByCost := make(flavorsByCost, len(allFlavors))
146+ var defaultFlavorId string
147+ // 2. Filter out flavors not matching the constraints and pick the
148+ // default flavor id based on the given name.
149+ for _, flavor := range allFlavors {
150+ if flavor.Name == filter.defaultFlavor {
151+ defaultFlavorId = flavor.Id
152+ }
153+ if flavorMatches(&flavor, filter.constraints) {
154+ matchingFlavors[flavor.Id] = flavor
155+ matchingByCost = append(matchingByCost, flavor)
156+ }
157+ }
158+ if len(matchingFlavors) == 0 {
159+ return nil, fmt.Errorf("no flavors found matching specified constraints: %v", filter.constraints)
160+ }
161+ // 3. If default flavor is among the matching ones, pick that.
162+ if _, ok := matchingFlavors[defaultFlavorId]; ok {
163+ return &instanceSpec{
164+ imageId: defaultImageId,
165+ flavorId: defaultFlavorId,
166+ }, nil
167+ } else if defaultFlavorId == "" {
168+ return nil, environs.NotFoundError{fmt.Errorf("no such flavor %s", filter.defaultFlavor)}
169+ }
170+ // 4. Sort by cost (memory) and find the cheapest matching both
171+ // user's explicit constraints and our own heuristic: minimum
172+ // amount of memory required to run MongoDB.
173+ sort.Sort(matchingByCost)
174+ for _, flavor := range matchingByCost {
175+ if flavor.RAM >= minMemoryForMongoDB {
176+ return &instanceSpec{
177+ imageId: defaultImageId,
178+ flavorId: flavor.Id,
179+ }, nil
180+ }
181+ }
182+ // 5. Failing that, sort in reverse order and return the most
183+ // expensive one, which will hopefully work, albeit not the best
184+ // match.
185+ sort.Sort(reverseByCost{matchingByCost})
186 return &instanceSpec{
187- imageId: imageId,
188- flavorId: flavorId,
189+ imageId: defaultImageId,
190+ flavorId: matchingByCost[0].Id,
191 }, nil
192 }
193
194=== modified file 'environs/openstack/live_test.go'
195--- environs/openstack/live_test.go 2013-04-10 00:11:24 +0000
196+++ environs/openstack/live_test.go 2013-04-15 11:03:26 +0000
197@@ -7,6 +7,7 @@
198 . "launchpad.net/gocheck"
199 "launchpad.net/goose/client"
200 "launchpad.net/goose/identity"
201+ "launchpad.net/juju-core/constraints"
202 "launchpad.net/juju-core/environs"
203 "launchpad.net/juju-core/environs/jujutest"
204 "launchpad.net/juju-core/environs/openstack"
205@@ -135,18 +136,21 @@
206 t.LoggingSuite.TearDownTest(c)
207 }
208
209-func (t *LiveTests) TestFindImageSpec(c *C) {
210+func (t *LiveTests) TestFindInstanceSpecWithoutConstraints(c *C) {
211 instanceType := openstack.DefaultInstanceType(t.Env)
212- imageId, flavorId, err := openstack.FindInstanceSpec(t.Env, "precise", "amd64", instanceType)
213+ imageId, flavorId, err := openstack.FindInstanceSpec(t.Env, "precise", "amd64", instanceType, constraints.Value{})
214 c.Assert(err, IsNil)
215 c.Assert(imageId, Equals, t.testImageId)
216 c.Assert(flavorId, Not(Equals), "")
217 }
218
219-func (t *LiveTests) TestFindImageBadFlavor(c *C) {
220- imageId, flavorId, err := openstack.FindInstanceSpec(t.Env, "precise", "amd64", "bad.flavor")
221+func (t *LiveTests) TestFindInstanceSpecWithBadFlavor(c *C) {
222+ imageId, flavorId, err := openstack.FindInstanceSpec(t.Env, "precise", "amd64", "bad.flavor", constraints.Value{})
223 _, ok := err.(environs.NotFoundError)
224 c.Assert(ok, Equals, true)
225 c.Assert(imageId, Equals, "")
226 c.Assert(flavorId, Equals, "")
227 }
228+
229+func (t *LiveTests) TestFindInstanceSpecWithConstraints(c *C) {
230+}
231
232=== modified file 'environs/openstack/provider.go'
233--- environs/openstack/provider.go 2013-04-14 22:12:49 +0000
234+++ environs/openstack/provider.go 2013-04-15 11:03:26 +0000
235@@ -768,11 +768,12 @@
236 // TODO(fwereade): this is somewhat crazy.
237 return nil, fmt.Errorf("cannot find image for %q", scfg.tools.Series)
238 }
239- spec, err := findInstanceSpec(e, &instanceConstraint{
240- series: scfg.tools.Series,
241- arch: scfg.tools.Arch,
242- region: e.ecfg().region(),
243- flavor: e.ecfg().defaultInstanceType(),
244+ spec, err := findInstanceSpec(e, &instanceFilter{
245+ series: scfg.tools.Series,
246+ arch: scfg.tools.Arch,
247+ region: e.ecfg().region(),
248+ defaultFlavor: e.ecfg().defaultInstanceType(),
249+ constraints: scfg.constraints,
250 })
251 if err != nil {
252 return nil, fmt.Errorf("cannot find image satisfying constraints: %v", err)

Subscribers

People subscribed via source and target branches