Merge lp:~usc-isi/nova/instance_type_extra_specs into lp:~hudson-openstack/nova/trunk
- instance_type_extra_specs
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Brian Waldon | ||||
Approved revision: | 1153 | ||||
Merged at revision: | 1219 | ||||
Proposed branch: | lp:~usc-isi/nova/instance_type_extra_specs | ||||
Merge into: | lp:~hudson-openstack/nova/trunk | ||||
Diff against target: |
986 lines (+775/-9) 12 files modified
nova/api/openstack/contrib/flavorextraspecs.py (+126/-0) nova/db/api.py (+21/-0) nova/db/sqlalchemy/api.py (+114/-4) nova/db/sqlalchemy/migrate_repo/versions/028_add_instance_type_extra_specs.py (+67/-0) nova/db/sqlalchemy/models.py (+16/-1) nova/exception.py (+5/-0) nova/scheduler/host_filter.py (+25/-1) nova/tests/api/openstack/extensions/test_flavors_extra_specs.py (+198/-0) nova/tests/scheduler/test_host_filter.py (+35/-1) nova/tests/test_host_filter.py (+2/-1) nova/tests/test_instance_types_extra_specs.py (+165/-0) tools/pip-requires (+1/-1) |
||||
To merge this branch: | bzr merge lp:~usc-isi/nova/instance_type_extra_specs | ||||
Related bugs: |
|
||||
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brian Waldon (community) | Approve | ||
Vish Ishaya (community) | Approve | ||
Nova Core security contacts | Pending | ||
Review via email: mp+62728@code.launchpad.net |
Commit message
Description of the change
Adds support for "extra specs", additional capability requirements associated with instance types.
The instance_type dictionary now has a new extra_specs field.
Adds a new table to the database: InstanceTypeExt
The InstanceTypeFilter has been modified to check for extra specs.
This will ultimately be needed for supporting heterogeneous instances: we'll annotate the instance types with info about whether GPUs are present, so users will be able to request something like a "cg1.4xlarge".
Includes api support as an extension for querying and modifying this info.
Mark Washenberger (markwash) wrote : | # |
Lorin Hochstein (lorinh) wrote : | # |
Migration number has been bumped.
This code shouldn't affect the api (at least, not the user api). From a user's point of view, they are just requesting an instance type by name. For example, if you wanted a compute node with GPUs, the user would request something like a "cg1.4xlarge" instead of an "m1.large".
(Somebody needs to add the gpu-related metadata to cg1.4xlarge instance type, but this will be site-specific. Is there currently an admin api for adding/modifying instance types? I assumed that this was currently done by just modifying the database directly. We'd ultimately like to modify the dashboard to allow an admin to edit this type of content).
To use this data:
- The compute service needs to report its capabilities (this code is already there)
- The scheduler needs to check if an instance type's metadata matches a compute service's capabilities. (the distributed scheduler code has some generic facilities for filtering, we would just filter on the instance type metadata if it is present).
Mark Washenberger (markwash) wrote : | # |
What you're saying is that a user finds out about the capabilities associated with each flavor/
Lorin Hochstein (lorinh) wrote : | # |
Mark:
That's right. We expect that the local installation would have user documentation that describes which instance types were associated with which capabilities.
We don't expect the users to be able to query for this information directly via the api.
Brian Waldon (bcwaldon) wrote : | # |
The code looks rather solid, however I do have a few higher-level questions:
1) The concept of 'metadata' doesn't fit in very well here. The data you are providing is integral to the provisioning process. Our usage of metadata in other places represents user-defined key/value pairs that our services will never care about. cpu_arch would probably fit better as a core instance type attribute, while we could look at the others as something closer to "optional attributes". An instance could use one of the standard instance types with a don't-care attitude when provisioning towards something like xpu_arch, while you could provide more specific instance types with those optional attributes being explicitly defined.
2) I'm not completely sold on the idea that a deployer should provide another method for communication of instance types. I would love to integrate this information into the existing flavors resource in the openstack api.
3) Am I correct in assuming another merge prop will be coming to fill in the scheduler integration? Otherwise, there is no way to respect whatever values are actually indicated in the metadata.
Josh Kearney (jk0) wrote : | # |
Setting to WIP until above feedback is worked in.
Lorin Hochstein (lorinh) wrote : | # |
On Jun 1, 2011, at 3:54 PM, Brian Waldon wrote:
> Review: Needs Information
> The code looks rather solid, however I do have a few higher-level questions:
>
> 1) The concept of 'metadata' doesn't fit in very well here. The data you are providing is integral to the provisioning process. Our usage of metadata in other places represents user-defined key/value pairs that our services will never care about. cpu_arch would probably fit better as a core instance type attribute, while we could look at the others as something closer to "optional attributes". An instance could use one of the standard instance types with a don't-care attitude when provisioning towards something like xpu_arch, while you could provide more specific instance types with those optional attributes being explicitly defined.
>
How about the name "InstanceTypeEx
> 2) I'm not completely sold on the idea that a deployer should provide another method for communication of instance types. I would love to integrate this information into the existing flavors resource in the openstack api.
>
I must admit that I'm only familiar with the ec2 API. I'll take a look at the openstack api to try and figure out how to support querying this type of data.
> 3) Am I correct in assuming another merge prop will be coming to fill in the scheduler integration? Otherwise, there is no way to respect whatever values are actually indicated in the metadata.
>
Yes, this will come in a future merge prop, although I may end up adding it in to the nova/scheduler/
Brian Waldon (bcwaldon) wrote : | # |
> > 1) The concept of 'metadata' doesn't fit in very well here. The data you are
> providing is integral to the provisioning process. Our usage of metadata in
> other places represents user-defined key/value pairs that our services will
> never care about. cpu_arch would probably fit better as a core instance type
> attribute, while we could look at the others as something closer to "optional
> attributes". An instance could use one of the standard instance types with a
> don't-care attitude when provisioning towards something like xpu_arch, while
> you could provide more specific instance types with those optional attributes
> being explicitly defined.
> >
> How about the name "InstanceTypeEx
> consistent with the distributed zone scheduler code, which uses the term
> "specs" to refer to requested attributes that need to match the capabilities
> of a compute node.
That name definitely feels better.
> > 2) I'm not completely sold on the idea that a deployer should provide
> another method for communication of instance types. I would love to integrate
> this information into the existing flavors resource in the openstack api.
> >
> I must admit that I'm only familiar with the ec2 API. I'll take a look at the
> openstack api to try and figure out how to support querying this type of data.
Something similar to the /servers/<id>/meta resource may be something to look at.
Brian Waldon (bcwaldon) wrote : | # |
Can you implement the api-layer code as an extension? I don't think this is going to make it into the v1.1 spec.
Lorin Hochstein (lorinh) wrote : | # |
> Can you implement the api-layer code as an extension? I don't think this is
> going to make it into the v1.1 spec.
Will do.
Vish Ishaya (vishvananda) wrote : | # |
This seems good. Just a question really. Why is the:
inst_dict[
necessary. It seems like the joined load should handle populating the subobjects. If there is some reason to convert to nested dictionaries, is utils.to_
Lorin Hochstein (lorinh) wrote : | # |
> This seems good. Just a question really. Why is the:
> inst_dict[
>
> necessary. It seems like the joined load should handle populating the
> subobjects. If there is some reason to convert to nested dictionaries, is
> utils.to_
> written?
Just ignorance, this is my first time plumbing the depths of sqlalchemy and I wasn't aware there was a utils.to_primitive function. I'll fix it in the code.
Lorin Hochstein (lorinh) wrote : | # |
> This seems good. Just a question really. Why is the:
> inst_dict[
>
> necessary. It seems like the joined load should handle populating the
> subobjects. If there is some reason to convert to nested dictionaries, is
> utils.to_
> written?
Having gone over the code again, I now recall that a helper method is needed is to convert from:
'extra_specs': [{'key': 'k1', 'value': 'v1', ...}, {'key': 'k2', 'value': 'v2', ...}, ...]
to
'extra_specs': {'k1':'v1', 'k2':'v2', ...}
I've renamed the helper method and modified the docstring to clarify this in the code.
Vish Ishaya (vishvananda) wrote : | # |
Ok thanks. I understand now. LGTM.
Brian Waldon (bcwaldon) wrote : | # |
Thanks for making an extension, Lorin. Here are the last few items:
110, 125: Can you please add a 'os-' prefix to your extension alias? See the volumes extension as an example.
529: This file should probably be moved into nova/tests/
983: I think we might want version 0.6.1, not latest. Thoughts?
Lorin Hochstein (lorinh) wrote : | # |
> Thanks for making an extension, Lorin. Here are the last few items:
>
> 110, 125: Can you please add a 'os-' prefix to your extension alias? See the
> volumes extension as an example.
>
> 529: This file should probably be moved into nova/tests/
> The other test files are fine where they are.
>
> 983: I think we might want version 0.6.1, not latest. Thoughts?
Sure, I think pegging it to 0.6.1 is reasonable.
Preview Diff
1 | === added file 'nova/api/openstack/contrib/flavorextraspecs.py' |
2 | --- nova/api/openstack/contrib/flavorextraspecs.py 1970-01-01 00:00:00 +0000 |
3 | +++ nova/api/openstack/contrib/flavorextraspecs.py 2011-06-27 00:06:02 +0000 |
4 | @@ -0,0 +1,126 @@ |
5 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
6 | + |
7 | +# Copyright 2011 University of Southern California |
8 | +# All Rights Reserved. |
9 | +# |
10 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
11 | +# not use this file except in compliance with the License. You may obtain |
12 | +# a copy of the License at |
13 | +# |
14 | +# http://www.apache.org/licenses/LICENSE-2.0 |
15 | +# |
16 | +# Unless required by applicable law or agreed to in writing, software |
17 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
18 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
19 | +# License for the specific language governing permissions and limitations |
20 | +# under the License. |
21 | + |
22 | +""" The instance type extra specs extension""" |
23 | + |
24 | +from webob import exc |
25 | + |
26 | +from nova import db |
27 | +from nova import quota |
28 | +from nova.api.openstack import extensions |
29 | +from nova.api.openstack import faults |
30 | +from nova.api.openstack import wsgi |
31 | + |
32 | + |
33 | +class FlavorExtraSpecsController(object): |
34 | + """ The flavor extra specs API controller for the Openstack API """ |
35 | + |
36 | + def _get_extra_specs(self, context, flavor_id): |
37 | + extra_specs = db.api.instance_type_extra_specs_get(context, flavor_id) |
38 | + specs_dict = {} |
39 | + for key, value in extra_specs.iteritems(): |
40 | + specs_dict[key] = value |
41 | + return dict(extra_specs=specs_dict) |
42 | + |
43 | + def _check_body(self, body): |
44 | + if body == None or body == "": |
45 | + expl = _('No Request Body') |
46 | + raise exc.HTTPBadRequest(explanation=expl) |
47 | + |
48 | + def index(self, req, flavor_id): |
49 | + """ Returns the list of extra specs for a givenflavor """ |
50 | + context = req.environ['nova.context'] |
51 | + return self._get_extra_specs(context, flavor_id) |
52 | + |
53 | + def create(self, req, flavor_id, body): |
54 | + self._check_body(body) |
55 | + context = req.environ['nova.context'] |
56 | + specs = body.get('extra_specs') |
57 | + try: |
58 | + db.api.instance_type_extra_specs_update_or_create(context, |
59 | + flavor_id, |
60 | + specs) |
61 | + except quota.QuotaError as error: |
62 | + self._handle_quota_error(error) |
63 | + return body |
64 | + |
65 | + def update(self, req, flavor_id, id, body): |
66 | + self._check_body(body) |
67 | + context = req.environ['nova.context'] |
68 | + if not id in body: |
69 | + expl = _('Request body and URI mismatch') |
70 | + raise exc.HTTPBadRequest(explanation=expl) |
71 | + if len(body) > 1: |
72 | + expl = _('Request body contains too many items') |
73 | + raise exc.HTTPBadRequest(explanation=expl) |
74 | + try: |
75 | + db.api.instance_type_extra_specs_update_or_create(context, |
76 | + flavor_id, |
77 | + body) |
78 | + except quota.QuotaError as error: |
79 | + self._handle_quota_error(error) |
80 | + |
81 | + return body |
82 | + |
83 | + def show(self, req, flavor_id, id): |
84 | + """ Return a single extra spec item """ |
85 | + context = req.environ['nova.context'] |
86 | + specs = self._get_extra_specs(context, flavor_id) |
87 | + if id in specs['extra_specs']: |
88 | + return {id: specs['extra_specs'][id]} |
89 | + else: |
90 | + return faults.Fault(exc.HTTPNotFound()) |
91 | + |
92 | + def delete(self, req, flavor_id, id): |
93 | + """ Deletes an existing extra spec """ |
94 | + context = req.environ['nova.context'] |
95 | + db.api.instance_type_extra_specs_delete(context, flavor_id, id) |
96 | + |
97 | + def _handle_quota_error(self, error): |
98 | + """Reraise quota errors as api-specific http exceptions.""" |
99 | + if error.code == "MetadataLimitExceeded": |
100 | + raise exc.HTTPBadRequest(explanation=error.message) |
101 | + raise error |
102 | + |
103 | + |
104 | +class Flavorextraspecs(extensions.ExtensionDescriptor): |
105 | + |
106 | + def get_name(self): |
107 | + return "FlavorExtraSpecs" |
108 | + |
109 | + def get_alias(self): |
110 | + return "os-flavor-extra-specs" |
111 | + |
112 | + def get_description(self): |
113 | + return "Instance type (flavor) extra specs" |
114 | + |
115 | + def get_namespace(self): |
116 | + return \ |
117 | + "http://docs.openstack.org/ext/flavor_extra_specs/api/v1.1" |
118 | + |
119 | + def get_updated(self): |
120 | + return "2011-06-23T00:00:00+00:00" |
121 | + |
122 | + def get_resources(self): |
123 | + resources = [] |
124 | + res = extensions.ResourceExtension( |
125 | + 'os-extra_specs', |
126 | + FlavorExtraSpecsController(), |
127 | + parent=dict(member_name='flavor', collection_name='flavors')) |
128 | + |
129 | + resources.append(res) |
130 | + return resources |
131 | |
132 | === modified file 'nova/db/api.py' |
133 | --- nova/db/api.py 2011-06-25 19:38:07 +0000 |
134 | +++ nova/db/api.py 2011-06-27 00:06:02 +0000 |
135 | @@ -1339,3 +1339,24 @@ |
136 | def agent_build_update(context, agent_build_id, values): |
137 | """Update agent build entry.""" |
138 | IMPL.agent_build_update(context, agent_build_id, values) |
139 | + |
140 | + |
141 | +#################### |
142 | + |
143 | + |
144 | +def instance_type_extra_specs_get(context, instance_type_id): |
145 | + """Get all extra specs for an instance type.""" |
146 | + return IMPL.instance_type_extra_specs_get(context, instance_type_id) |
147 | + |
148 | + |
149 | +def instance_type_extra_specs_delete(context, instance_type_id, key): |
150 | + """Delete the given extra specs item.""" |
151 | + IMPL.instance_type_extra_specs_delete(context, instance_type_id, key) |
152 | + |
153 | + |
154 | +def instance_type_extra_specs_update_or_create(context, instance_type_id, |
155 | + extra_specs): |
156 | + """Create or update instance type extra specs. This adds or modifies the |
157 | + key/value pairs specified in the extra specs dict argument""" |
158 | + IMPL.instance_type_extra_specs_update_or_create(context, instance_type_id, |
159 | + extra_specs) |
160 | |
161 | === modified file 'nova/db/sqlalchemy/api.py' |
162 | --- nova/db/sqlalchemy/api.py 2011-06-25 19:38:07 +0000 |
163 | +++ nova/db/sqlalchemy/api.py 2011-06-27 00:06:02 +0000 |
164 | @@ -2597,7 +2597,22 @@ |
165 | |
166 | @require_admin_context |
167 | def instance_type_create(_context, values): |
168 | + """Create a new instance type. In order to pass in extra specs, |
169 | + the values dict should contain a 'extra_specs' key/value pair: |
170 | + |
171 | + {'extra_specs' : {'k1': 'v1', 'k2': 'v2', ...}} |
172 | + |
173 | + """ |
174 | try: |
175 | + specs = values.get('extra_specs') |
176 | + specs_refs = [] |
177 | + if specs: |
178 | + for k, v in specs.iteritems(): |
179 | + specs_ref = models.InstanceTypeExtraSpecs() |
180 | + specs_ref['key'] = k |
181 | + specs_ref['value'] = v |
182 | + specs_refs.append(specs_ref) |
183 | + values['extra_specs'] = specs_refs |
184 | instance_type_ref = models.InstanceTypes() |
185 | instance_type_ref.update(values) |
186 | instance_type_ref.save() |
187 | @@ -2606,6 +2621,25 @@ |
188 | return instance_type_ref |
189 | |
190 | |
191 | +def _dict_with_extra_specs(inst_type_query): |
192 | + """Takes an instance type query returned by sqlalchemy |
193 | + and returns it as a dictionary, converting the extra_specs |
194 | + entry from a list of dicts: |
195 | + |
196 | + 'extra_specs' : [{'key': 'k1', 'value': 'v1', ...}, ...] |
197 | + |
198 | + to a single dict: |
199 | + |
200 | + 'extra_specs' : {'k1': 'v1'} |
201 | + |
202 | + """ |
203 | + inst_type_dict = dict(inst_type_query) |
204 | + extra_specs = dict([(x['key'], x['value']) for x in \ |
205 | + inst_type_query['extra_specs']]) |
206 | + inst_type_dict['extra_specs'] = extra_specs |
207 | + return inst_type_dict |
208 | + |
209 | + |
210 | @require_context |
211 | def instance_type_get_all(context, inactive=False): |
212 | """ |
213 | @@ -2614,17 +2648,19 @@ |
214 | session = get_session() |
215 | if inactive: |
216 | inst_types = session.query(models.InstanceTypes).\ |
217 | + options(joinedload('extra_specs')).\ |
218 | order_by("name").\ |
219 | all() |
220 | else: |
221 | inst_types = session.query(models.InstanceTypes).\ |
222 | + options(joinedload('extra_specs')).\ |
223 | filter_by(deleted=False).\ |
224 | order_by("name").\ |
225 | all() |
226 | if inst_types: |
227 | inst_dict = {} |
228 | for i in inst_types: |
229 | - inst_dict[i['name']] = dict(i) |
230 | + inst_dict[i['name']] = _dict_with_extra_specs(i) |
231 | return inst_dict |
232 | else: |
233 | raise exception.NoInstanceTypesFound() |
234 | @@ -2635,12 +2671,14 @@ |
235 | """Returns a dict describing specific instance_type""" |
236 | session = get_session() |
237 | inst_type = session.query(models.InstanceTypes).\ |
238 | + options(joinedload('extra_specs')).\ |
239 | filter_by(id=id).\ |
240 | first() |
241 | + |
242 | if not inst_type: |
243 | raise exception.InstanceTypeNotFound(instance_type=id) |
244 | else: |
245 | - return dict(inst_type) |
246 | + return _dict_with_extra_specs(inst_type) |
247 | |
248 | |
249 | @require_context |
250 | @@ -2648,12 +2686,13 @@ |
251 | """Returns a dict describing specific instance_type""" |
252 | session = get_session() |
253 | inst_type = session.query(models.InstanceTypes).\ |
254 | + options(joinedload('extra_specs')).\ |
255 | filter_by(name=name).\ |
256 | first() |
257 | if not inst_type: |
258 | raise exception.InstanceTypeNotFoundByName(instance_type_name=name) |
259 | else: |
260 | - return dict(inst_type) |
261 | + return _dict_with_extra_specs(inst_type) |
262 | |
263 | |
264 | @require_context |
265 | @@ -2661,12 +2700,13 @@ |
266 | """Returns a dict describing specific flavor_id""" |
267 | session = get_session() |
268 | inst_type = session.query(models.InstanceTypes).\ |
269 | + options(joinedload('extra_specs')).\ |
270 | filter_by(flavorid=int(id)).\ |
271 | first() |
272 | if not inst_type: |
273 | raise exception.FlavorNotFound(flavor_id=id) |
274 | else: |
275 | - return dict(inst_type) |
276 | + return _dict_with_extra_specs(inst_type) |
277 | |
278 | |
279 | @require_admin_context |
280 | @@ -2834,6 +2874,9 @@ |
281 | return metadata |
282 | |
283 | |
284 | +#################### |
285 | + |
286 | + |
287 | @require_admin_context |
288 | def agent_build_create(context, values): |
289 | agent_build_ref = models.AgentBuild() |
290 | @@ -2883,3 +2926,70 @@ |
291 | first() |
292 | agent_build_ref.update(values) |
293 | agent_build_ref.save(session=session) |
294 | + |
295 | + |
296 | +#################### |
297 | + |
298 | + |
299 | +@require_context |
300 | +def instance_type_extra_specs_get(context, instance_type_id): |
301 | + session = get_session() |
302 | + |
303 | + spec_results = session.query(models.InstanceTypeExtraSpecs).\ |
304 | + filter_by(instance_type_id=instance_type_id).\ |
305 | + filter_by(deleted=False).\ |
306 | + all() |
307 | + |
308 | + spec_dict = {} |
309 | + for i in spec_results: |
310 | + spec_dict[i['key']] = i['value'] |
311 | + return spec_dict |
312 | + |
313 | + |
314 | +@require_context |
315 | +def instance_type_extra_specs_delete(context, instance_type_id, key): |
316 | + session = get_session() |
317 | + session.query(models.InstanceTypeExtraSpecs).\ |
318 | + filter_by(instance_type_id=instance_type_id).\ |
319 | + filter_by(key=key).\ |
320 | + filter_by(deleted=False).\ |
321 | + update({'deleted': True, |
322 | + 'deleted_at': utils.utcnow(), |
323 | + 'updated_at': literal_column('updated_at')}) |
324 | + |
325 | + |
326 | +@require_context |
327 | +def instance_type_extra_specs_get_item(context, instance_type_id, key): |
328 | + session = get_session() |
329 | + |
330 | + sppec_result = session.query(models.InstanceTypeExtraSpecs).\ |
331 | + filter_by(instance_type_id=instance_type_id).\ |
332 | + filter_by(key=key).\ |
333 | + filter_by(deleted=False).\ |
334 | + first() |
335 | + |
336 | + if not spec_result: |
337 | + raise exception.\ |
338 | + InstanceTypeExtraSpecsNotFound(extra_specs_key=key, |
339 | + instance_type_id=instance_type_id) |
340 | + return spec_result |
341 | + |
342 | + |
343 | +@require_context |
344 | +def instance_type_extra_specs_update_or_create(context, instance_type_id, |
345 | + specs): |
346 | + session = get_session() |
347 | + spec_ref = None |
348 | + for key, value in specs.iteritems(): |
349 | + try: |
350 | + spec_ref = instance_type_extra_specs_get_item(context, |
351 | + instance_type_id, |
352 | + key, |
353 | + session) |
354 | + except: |
355 | + spec_ref = models.InstanceTypeExtraSpecs() |
356 | + spec_ref.update({"key": key, "value": value, |
357 | + "instance_type_id": instance_type_id, |
358 | + "deleted": 0}) |
359 | + spec_ref.save(session=session) |
360 | + return specs |
361 | |
362 | === added file 'nova/db/sqlalchemy/migrate_repo/versions/028_add_instance_type_extra_specs.py' |
363 | --- nova/db/sqlalchemy/migrate_repo/versions/028_add_instance_type_extra_specs.py 1970-01-01 00:00:00 +0000 |
364 | +++ nova/db/sqlalchemy/migrate_repo/versions/028_add_instance_type_extra_specs.py 2011-06-27 00:06:02 +0000 |
365 | @@ -0,0 +1,67 @@ |
366 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
367 | + |
368 | +# Copyright 2011 University of Southern California |
369 | +# |
370 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
371 | +# not use this file except in compliance with the License. You may obtain |
372 | +# a copy of the License at |
373 | +# |
374 | +# http://www.apache.org/licenses/LICENSE-2.0 |
375 | +# |
376 | +# Unless required by applicable law or agreed to in writing, software |
377 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
378 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
379 | +# License for the specific language governing permissions and limitations |
380 | +# under the License. |
381 | + |
382 | +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer |
383 | +from sqlalchemy import MetaData, String, Table |
384 | +from nova import log as logging |
385 | + |
386 | +meta = MetaData() |
387 | + |
388 | +# Just for the ForeignKey and column creation to succeed, these are not the |
389 | +# actual definitions of instances or services. |
390 | +instance_types = Table('instance_types', meta, |
391 | + Column('id', Integer(), primary_key=True, nullable=False), |
392 | + ) |
393 | + |
394 | +# |
395 | +# New Tables |
396 | +# |
397 | + |
398 | +instance_type_extra_specs_table = Table('instance_type_extra_specs', meta, |
399 | + Column('created_at', DateTime(timezone=False)), |
400 | + Column('updated_at', DateTime(timezone=False)), |
401 | + Column('deleted_at', DateTime(timezone=False)), |
402 | + Column('deleted', Boolean(create_constraint=True, name=None)), |
403 | + Column('id', Integer(), primary_key=True, nullable=False), |
404 | + Column('instance_type_id', |
405 | + Integer(), |
406 | + ForeignKey('instance_types.id'), |
407 | + nullable=False), |
408 | + Column('key', |
409 | + String(length=255, convert_unicode=False, assert_unicode=None, |
410 | + unicode_error=None, _warn_on_bytestring=False)), |
411 | + Column('value', |
412 | + String(length=255, convert_unicode=False, assert_unicode=None, |
413 | + unicode_error=None, _warn_on_bytestring=False))) |
414 | + |
415 | + |
416 | +def upgrade(migrate_engine): |
417 | + # Upgrade operations go here. Don't create your own engine; |
418 | + # bind migrate_engine to your metadata |
419 | + meta.bind = migrate_engine |
420 | + for table in (instance_type_extra_specs_table, ): |
421 | + try: |
422 | + table.create() |
423 | + except Exception: |
424 | + logging.info(repr(table)) |
425 | + logging.exception('Exception while creating table') |
426 | + raise |
427 | + |
428 | + |
429 | +def downgrade(migrate_engine): |
430 | + # Operations to reverse the above upgrade go here. |
431 | + for table in (instance_type_extra_specs_table, ): |
432 | + table.drop() |
433 | |
434 | === modified file 'nova/db/sqlalchemy/models.py' |
435 | --- nova/db/sqlalchemy/models.py 2011-06-24 12:01:51 +0000 |
436 | +++ nova/db/sqlalchemy/models.py 2011-06-27 00:06:02 +0000 |
437 | @@ -716,6 +716,21 @@ |
438 | 'InstanceMetadata.deleted == False)') |
439 | |
440 | |
441 | +class InstanceTypeExtraSpecs(BASE, NovaBase): |
442 | + """Represents additional specs as key/value pairs for an instance_type""" |
443 | + __tablename__ = 'instance_type_extra_specs' |
444 | + id = Column(Integer, primary_key=True) |
445 | + key = Column(String(255)) |
446 | + value = Column(String(255)) |
447 | + instance_type_id = Column(Integer, ForeignKey('instance_types.id'), |
448 | + nullable=False) |
449 | + instance_type = relationship(InstanceTypes, backref="extra_specs", |
450 | + foreign_keys=instance_type_id, |
451 | + primaryjoin='and_(' |
452 | + 'InstanceTypeExtraSpecs.instance_type_id == InstanceTypes.id,' |
453 | + 'InstanceTypeExtraSpecs.deleted == False)') |
454 | + |
455 | + |
456 | class Zone(BASE, NovaBase): |
457 | """Represents a child zone of this zone.""" |
458 | __tablename__ = 'zones' |
459 | @@ -750,7 +765,7 @@ |
460 | Network, SecurityGroup, SecurityGroupIngressRule, |
461 | SecurityGroupInstanceAssociation, AuthToken, User, |
462 | Project, Certificate, ConsolePool, Console, Zone, |
463 | - AgentBuild, InstanceMetadata, Migration) |
464 | + AgentBuild, InstanceMetadata, InstanceTypeExtraSpecs, Migration) |
465 | engine = create_engine(FLAGS.sql_connection, echo=False) |
466 | for model in models: |
467 | model.metadata.create_all(engine) |
468 | |
469 | === modified file 'nova/exception.py' |
470 | --- nova/exception.py 2011-06-24 12:01:51 +0000 |
471 | +++ nova/exception.py 2011-06-27 00:06:02 +0000 |
472 | @@ -504,6 +504,11 @@ |
473 | "key %(metadata_key)s.") |
474 | |
475 | |
476 | +class InstanceTypeExtraSpecsNotFound(NotFound): |
477 | + message = _("Instance Type %(instance_type_id)s has no extra specs with " |
478 | + "key %(extra_specs_key)s.") |
479 | + |
480 | + |
481 | class LDAPObjectNotFound(NotFound): |
482 | message = _("LDAP object could not be found") |
483 | |
484 | |
485 | === modified file 'nova/scheduler/host_filter.py' |
486 | --- nova/scheduler/host_filter.py 2011-06-03 20:32:42 +0000 |
487 | +++ nova/scheduler/host_filter.py 2011-06-27 00:06:02 +0000 |
488 | @@ -93,6 +93,26 @@ |
489 | """Use instance_type to filter hosts.""" |
490 | return (self._full_name(), instance_type) |
491 | |
492 | + def _satisfies_extra_specs(self, capabilities, instance_type): |
493 | + """Check that the capabilities provided by the compute service |
494 | + satisfy the extra specs associated with the instance type""" |
495 | + |
496 | + if 'extra_specs' not in instance_type: |
497 | + return True |
498 | + |
499 | + # Note(lorinh): For now, we are just checking exact matching on the |
500 | + # values. Later on, we want to handle numerical |
501 | + # values so we can represent things like number of GPU cards |
502 | + |
503 | + try: |
504 | + for key, value in instance_type['extra_specs'].iteritems(): |
505 | + if capabilities[key] != value: |
506 | + return False |
507 | + except KeyError: |
508 | + return False |
509 | + |
510 | + return True |
511 | + |
512 | def filter_hosts(self, zone_manager, query): |
513 | """Return a list of hosts that can create instance_type.""" |
514 | instance_type = query |
515 | @@ -103,7 +123,11 @@ |
516 | disk_bytes = capabilities['disk_available'] |
517 | spec_ram = instance_type['memory_mb'] |
518 | spec_disk = instance_type['local_gb'] |
519 | - if host_ram_mb >= spec_ram and disk_bytes >= spec_disk: |
520 | + extra_specs = instance_type['extra_specs'] |
521 | + |
522 | + if host_ram_mb >= spec_ram and \ |
523 | + disk_bytes >= spec_disk and \ |
524 | + self._satisfies_extra_specs(capabilities, instance_type): |
525 | selected_hosts.append((host, capabilities)) |
526 | return selected_hosts |
527 | |
528 | |
529 | === added file 'nova/tests/api/openstack/extensions/test_flavors_extra_specs.py' |
530 | --- nova/tests/api/openstack/extensions/test_flavors_extra_specs.py 1970-01-01 00:00:00 +0000 |
531 | +++ nova/tests/api/openstack/extensions/test_flavors_extra_specs.py 2011-06-27 00:06:02 +0000 |
532 | @@ -0,0 +1,198 @@ |
533 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
534 | + |
535 | +# Copyright 2011 University of Southern California |
536 | +# All Rights Reserved. |
537 | +# |
538 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
539 | +# not use this file except in compliance with the License. You may obtain |
540 | +# a copy of the License at |
541 | +# |
542 | +# http://www.apache.org/licenses/LICENSE-2.0 |
543 | +# |
544 | +# Unless required by applicable law or agreed to in writing, software |
545 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
546 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
547 | +# License for the specific language governing permissions and limitations |
548 | +# under the License. |
549 | + |
550 | +import json |
551 | +import stubout |
552 | +import unittest |
553 | +import webob |
554 | +import os.path |
555 | + |
556 | + |
557 | +from nova import flags |
558 | +from nova.api import openstack |
559 | +from nova.api.openstack import auth |
560 | +from nova.api.openstack import extensions |
561 | +from nova.tests.api.openstack import fakes |
562 | +import nova.wsgi |
563 | + |
564 | +FLAGS = flags.FLAGS |
565 | + |
566 | + |
567 | +def return_create_flavor_extra_specs(context, flavor_id, extra_specs): |
568 | + return stub_flavor_extra_specs() |
569 | + |
570 | + |
571 | +def return_flavor_extra_specs(context, flavor_id): |
572 | + return stub_flavor_extra_specs() |
573 | + |
574 | + |
575 | +def return_flavor_extra_specs(context, flavor_id): |
576 | + return stub_flavor_extra_specs() |
577 | + |
578 | + |
579 | +def return_empty_flavor_extra_specs(context, flavor_id): |
580 | + return {} |
581 | + |
582 | + |
583 | +def delete_flavor_extra_specs(context, flavor_id, key): |
584 | + pass |
585 | + |
586 | + |
587 | +def stub_flavor_extra_specs(): |
588 | + specs = { |
589 | + "key1": "value1", |
590 | + "key2": "value2", |
591 | + "key3": "value3", |
592 | + "key4": "value4", |
593 | + "key5": "value5"} |
594 | + return specs |
595 | + |
596 | + |
597 | +class FlavorsExtraSpecsTest(unittest.TestCase): |
598 | + |
599 | + def setUp(self): |
600 | + super(FlavorsExtraSpecsTest, self).setUp() |
601 | + FLAGS.osapi_extensions_path = os.path.join(os.path.dirname(__file__), |
602 | + "extensions") |
603 | + self.stubs = stubout.StubOutForTesting() |
604 | + fakes.FakeAuthManager.auth_data = {} |
605 | + fakes.FakeAuthDatabase.data = {} |
606 | + fakes.stub_out_auth(self.stubs) |
607 | + fakes.stub_out_key_pair_funcs(self.stubs) |
608 | + self.mware = auth.AuthMiddleware( |
609 | + extensions.ExtensionMiddleware( |
610 | + openstack.APIRouterV11())) |
611 | + |
612 | + def tearDown(self): |
613 | + self.stubs.UnsetAll() |
614 | + super(FlavorsExtraSpecsTest, self).tearDown() |
615 | + |
616 | + def test_index(self): |
617 | + self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get', |
618 | + return_flavor_extra_specs) |
619 | + request = webob.Request.blank('/flavors/1/os-extra_specs') |
620 | + res = request.get_response(self.mware) |
621 | + self.assertEqual(200, res.status_int) |
622 | + res_dict = json.loads(res.body) |
623 | + self.assertEqual('application/json', res.headers['Content-Type']) |
624 | + self.assertEqual('value1', res_dict['extra_specs']['key1']) |
625 | + |
626 | + def test_index_no_data(self): |
627 | + self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get', |
628 | + return_empty_flavor_extra_specs) |
629 | + req = webob.Request.blank('/flavors/1/os-extra_specs') |
630 | + res = req.get_response(self.mware) |
631 | + res_dict = json.loads(res.body) |
632 | + self.assertEqual(200, res.status_int) |
633 | + self.assertEqual('application/json', res.headers['Content-Type']) |
634 | + self.assertEqual(0, len(res_dict['extra_specs'])) |
635 | + |
636 | + def test_show(self): |
637 | + self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get', |
638 | + return_flavor_extra_specs) |
639 | + req = webob.Request.blank('/flavors/1/os-extra_specs/key5') |
640 | + res = req.get_response(self.mware) |
641 | + self.assertEqual(200, res.status_int) |
642 | + res_dict = json.loads(res.body) |
643 | + self.assertEqual('application/json', res.headers['Content-Type']) |
644 | + self.assertEqual('value5', res_dict['key5']) |
645 | + |
646 | + def test_show_spec_not_found(self): |
647 | + self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get', |
648 | + return_empty_flavor_extra_specs) |
649 | + req = webob.Request.blank('/flavors/1/os-extra_specs/key6') |
650 | + res = req.get_response(self.mware) |
651 | + res_dict = json.loads(res.body) |
652 | + self.assertEqual(404, res.status_int) |
653 | + |
654 | + def test_delete(self): |
655 | + self.stubs.Set(nova.db.api, 'instance_type_extra_specs_delete', |
656 | + delete_flavor_extra_specs) |
657 | + req = webob.Request.blank('/flavors/1/os-extra_specs/key5') |
658 | + req.method = 'DELETE' |
659 | + res = req.get_response(self.mware) |
660 | + self.assertEqual(200, res.status_int) |
661 | + |
662 | + def test_create(self): |
663 | + self.stubs.Set(nova.db.api, |
664 | + 'instance_type_extra_specs_update_or_create', |
665 | + return_create_flavor_extra_specs) |
666 | + req = webob.Request.blank('/flavors/1/os-extra_specs') |
667 | + req.method = 'POST' |
668 | + req.body = '{"extra_specs": {"key1": "value1"}}' |
669 | + req.headers["content-type"] = "application/json" |
670 | + res = req.get_response(self.mware) |
671 | + res_dict = json.loads(res.body) |
672 | + self.assertEqual(200, res.status_int) |
673 | + self.assertEqual('application/json', res.headers['Content-Type']) |
674 | + self.assertEqual('value1', res_dict['extra_specs']['key1']) |
675 | + |
676 | + def test_create_empty_body(self): |
677 | + self.stubs.Set(nova.db.api, |
678 | + 'instance_type_extra_specs_update_or_create', |
679 | + return_create_flavor_extra_specs) |
680 | + req = webob.Request.blank('/flavors/1/os-extra_specs') |
681 | + req.method = 'POST' |
682 | + req.headers["content-type"] = "application/json" |
683 | + res = req.get_response(self.mware) |
684 | + self.assertEqual(400, res.status_int) |
685 | + |
686 | + def test_update_item(self): |
687 | + self.stubs.Set(nova.db.api, |
688 | + 'instance_type_extra_specs_update_or_create', |
689 | + return_create_flavor_extra_specs) |
690 | + req = webob.Request.blank('/flavors/1/os-extra_specs/key1') |
691 | + req.method = 'PUT' |
692 | + req.body = '{"key1": "value1"}' |
693 | + req.headers["content-type"] = "application/json" |
694 | + res = req.get_response(self.mware) |
695 | + self.assertEqual(200, res.status_int) |
696 | + self.assertEqual('application/json', res.headers['Content-Type']) |
697 | + res_dict = json.loads(res.body) |
698 | + self.assertEqual('value1', res_dict['key1']) |
699 | + |
700 | + def test_update_item_empty_body(self): |
701 | + self.stubs.Set(nova.db.api, |
702 | + 'instance_type_extra_specs_update_or_create', |
703 | + return_create_flavor_extra_specs) |
704 | + req = webob.Request.blank('/flavors/1/os-extra_specs/key1') |
705 | + req.method = 'PUT' |
706 | + req.headers["content-type"] = "application/json" |
707 | + res = req.get_response(self.mware) |
708 | + self.assertEqual(400, res.status_int) |
709 | + |
710 | + def test_update_item_too_many_keys(self): |
711 | + self.stubs.Set(nova.db.api, |
712 | + 'instance_type_extra_specs_update_or_create', |
713 | + return_create_flavor_extra_specs) |
714 | + req = webob.Request.blank('/flavors/1/os-extra_specs/key1') |
715 | + req.method = 'PUT' |
716 | + req.body = '{"key1": "value1", "key2": "value2"}' |
717 | + req.headers["content-type"] = "application/json" |
718 | + res = req.get_response(self.mware) |
719 | + self.assertEqual(400, res.status_int) |
720 | + |
721 | + def test_update_item_body_uri_mismatch(self): |
722 | + self.stubs.Set(nova.db.api, |
723 | + 'instance_type_extra_specs_update_or_create', |
724 | + return_create_flavor_extra_specs) |
725 | + req = webob.Request.blank('/flavors/1/os-extra_specs/bad') |
726 | + req.method = 'PUT' |
727 | + req.body = '{"key1": "value1"}' |
728 | + req.headers["content-type"] = "application/json" |
729 | + res = req.get_response(self.mware) |
730 | + self.assertEqual(400, res.status_int) |
731 | |
732 | === modified file 'nova/tests/scheduler/test_host_filter.py' |
733 | --- nova/tests/scheduler/test_host_filter.py 2011-06-14 01:14:26 +0000 |
734 | +++ nova/tests/scheduler/test_host_filter.py 2011-06-27 00:06:02 +0000 |
735 | @@ -67,7 +67,18 @@ |
736 | flavorid=1, |
737 | swap=500, |
738 | rxtx_quota=30000, |
739 | - rxtx_cap=200) |
740 | + rxtx_cap=200, |
741 | + extra_specs={}) |
742 | + self.gpu_instance_type = dict(name='tiny.gpu', |
743 | + memory_mb=50, |
744 | + vcpus=10, |
745 | + local_gb=500, |
746 | + flavorid=2, |
747 | + swap=500, |
748 | + rxtx_quota=30000, |
749 | + rxtx_cap=200, |
750 | + extra_specs={'xpu_arch': 'fermi', |
751 | + 'xpu_info': 'Tesla 2050'}) |
752 | |
753 | self.zone_manager = FakeZoneManager() |
754 | states = {} |
755 | @@ -75,6 +86,18 @@ |
756 | states['host%02d' % (x + 1)] = {'compute': self._host_caps(x)} |
757 | self.zone_manager.service_states = states |
758 | |
759 | + # Add some extra capabilities to some hosts |
760 | + host07 = self.zone_manager.service_states['host07']['compute'] |
761 | + host07['xpu_arch'] = 'fermi' |
762 | + host07['xpu_info'] = 'Tesla 2050' |
763 | + |
764 | + host08 = self.zone_manager.service_states['host08']['compute'] |
765 | + host08['xpu_arch'] = 'radeon' |
766 | + |
767 | + host09 = self.zone_manager.service_states['host09']['compute'] |
768 | + host09['xpu_arch'] = 'fermi' |
769 | + host09['xpu_info'] = 'Tesla 2150' |
770 | + |
771 | def tearDown(self): |
772 | FLAGS.default_host_filter = self.old_flag |
773 | |
774 | @@ -116,6 +139,17 @@ |
775 | self.assertEquals('host05', just_hosts[0]) |
776 | self.assertEquals('host10', just_hosts[5]) |
777 | |
778 | + def test_instance_type_filter_extra_specs(self): |
779 | + hf = host_filter.InstanceTypeFilter() |
780 | + # filter all hosts that can support 50 ram and 500 disk |
781 | + name, cooked = hf.instance_type_to_filter(self.gpu_instance_type) |
782 | + self.assertEquals('nova.scheduler.host_filter.InstanceTypeFilter', |
783 | + name) |
784 | + hosts = hf.filter_hosts(self.zone_manager, cooked) |
785 | + self.assertEquals(1, len(hosts)) |
786 | + just_hosts = [host for host, caps in hosts] |
787 | + self.assertEquals('host07', just_hosts[0]) |
788 | + |
789 | def test_json_filter(self): |
790 | hf = host_filter.JsonFilter() |
791 | # filter all hosts that can support 50 ram and 500 disk |
792 | |
793 | === modified file 'nova/tests/test_host_filter.py' |
794 | --- nova/tests/test_host_filter.py 2011-06-02 19:08:19 +0000 |
795 | +++ nova/tests/test_host_filter.py 2011-06-27 00:06:02 +0000 |
796 | @@ -67,7 +67,8 @@ |
797 | flavorid=1, |
798 | swap=500, |
799 | rxtx_quota=30000, |
800 | - rxtx_cap=200) |
801 | + rxtx_cap=200, |
802 | + extra_specs={}) |
803 | |
804 | self.zone_manager = FakeZoneManager() |
805 | states = {} |
806 | |
807 | === added file 'nova/tests/test_instance_types_extra_specs.py' |
808 | --- nova/tests/test_instance_types_extra_specs.py 1970-01-01 00:00:00 +0000 |
809 | +++ nova/tests/test_instance_types_extra_specs.py 2011-06-27 00:06:02 +0000 |
810 | @@ -0,0 +1,165 @@ |
811 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
812 | + |
813 | +# Copyright 2011 University of Southern California |
814 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
815 | +# not use this file except in compliance with the License. You may obtain |
816 | +# a copy of the License at |
817 | +# |
818 | +# http://www.apache.org/licenses/LICENSE-2.0 |
819 | +# |
820 | +# Unless required by applicable law or agreed to in writing, software |
821 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
822 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
823 | +# License for the specific language governing permissions and limitations |
824 | +# under the License. |
825 | +""" |
826 | +Unit Tests for instance types extra specs code |
827 | +""" |
828 | + |
829 | +from nova import context |
830 | +from nova import db |
831 | +from nova import test |
832 | +from nova.db.sqlalchemy.session import get_session |
833 | +from nova.db.sqlalchemy import models |
834 | + |
835 | + |
836 | +class InstanceTypeExtraSpecsTestCase(test.TestCase): |
837 | + |
838 | + def setUp(self): |
839 | + super(InstanceTypeExtraSpecsTestCase, self).setUp() |
840 | + self.context = context.get_admin_context() |
841 | + values = dict(name="cg1.4xlarge", |
842 | + memory_mb=22000, |
843 | + vcpus=8, |
844 | + local_gb=1690, |
845 | + flavorid=105) |
846 | + specs = dict(cpu_arch="x86_64", |
847 | + cpu_model="Nehalem", |
848 | + xpu_arch="fermi", |
849 | + xpus=2, |
850 | + xpu_model="Tesla 2050") |
851 | + values['extra_specs'] = specs |
852 | + ref = db.api.instance_type_create(self.context, |
853 | + values) |
854 | + self.instance_type_id = ref.id |
855 | + |
856 | + def tearDown(self): |
857 | + # Remove the instance type from the database |
858 | + db.api.instance_type_purge(context.get_admin_context(), "cg1.4xlarge") |
859 | + super(InstanceTypeExtraSpecsTestCase, self).tearDown() |
860 | + |
861 | + def test_instance_type_specs_get(self): |
862 | + expected_specs = dict(cpu_arch="x86_64", |
863 | + cpu_model="Nehalem", |
864 | + xpu_arch="fermi", |
865 | + xpus="2", |
866 | + xpu_model="Tesla 2050") |
867 | + actual_specs = db.api.instance_type_extra_specs_get( |
868 | + context.get_admin_context(), |
869 | + self.instance_type_id) |
870 | + self.assertEquals(expected_specs, actual_specs) |
871 | + |
872 | + def test_instance_type_extra_specs_delete(self): |
873 | + expected_specs = dict(cpu_arch="x86_64", |
874 | + cpu_model="Nehalem", |
875 | + xpu_arch="fermi", |
876 | + xpus="2") |
877 | + db.api.instance_type_extra_specs_delete(context.get_admin_context(), |
878 | + self.instance_type_id, |
879 | + "xpu_model") |
880 | + actual_specs = db.api.instance_type_extra_specs_get( |
881 | + context.get_admin_context(), |
882 | + self.instance_type_id) |
883 | + self.assertEquals(expected_specs, actual_specs) |
884 | + |
885 | + def test_instance_type_extra_specs_update(self): |
886 | + expected_specs = dict(cpu_arch="x86_64", |
887 | + cpu_model="Sandy Bridge", |
888 | + xpu_arch="fermi", |
889 | + xpus="2", |
890 | + xpu_model="Tesla 2050") |
891 | + db.api.instance_type_extra_specs_update_or_create( |
892 | + context.get_admin_context(), |
893 | + self.instance_type_id, |
894 | + dict(cpu_model="Sandy Bridge")) |
895 | + actual_specs = db.api.instance_type_extra_specs_get( |
896 | + context.get_admin_context(), |
897 | + self.instance_type_id) |
898 | + self.assertEquals(expected_specs, actual_specs) |
899 | + |
900 | + def test_instance_type_extra_specs_create(self): |
901 | + expected_specs = dict(cpu_arch="x86_64", |
902 | + cpu_model="Nehalem", |
903 | + xpu_arch="fermi", |
904 | + xpus="2", |
905 | + xpu_model="Tesla 2050", |
906 | + net_arch="ethernet", |
907 | + net_mbps="10000") |
908 | + db.api.instance_type_extra_specs_update_or_create( |
909 | + context.get_admin_context(), |
910 | + self.instance_type_id, |
911 | + dict(net_arch="ethernet", |
912 | + net_mbps=10000)) |
913 | + actual_specs = db.api.instance_type_extra_specs_get( |
914 | + context.get_admin_context(), |
915 | + self.instance_type_id) |
916 | + self.assertEquals(expected_specs, actual_specs) |
917 | + |
918 | + def test_instance_type_get_by_id_with_extra_specs(self): |
919 | + instance_type = db.api.instance_type_get_by_id( |
920 | + context.get_admin_context(), |
921 | + self.instance_type_id) |
922 | + self.assertEquals(instance_type['extra_specs'], |
923 | + dict(cpu_arch="x86_64", |
924 | + cpu_model="Nehalem", |
925 | + xpu_arch="fermi", |
926 | + xpus="2", |
927 | + xpu_model="Tesla 2050")) |
928 | + instance_type = db.api.instance_type_get_by_id( |
929 | + context.get_admin_context(), |
930 | + 5) |
931 | + self.assertEquals(instance_type['extra_specs'], {}) |
932 | + |
933 | + def test_instance_type_get_by_name_with_extra_specs(self): |
934 | + instance_type = db.api.instance_type_get_by_name( |
935 | + context.get_admin_context(), |
936 | + "cg1.4xlarge") |
937 | + self.assertEquals(instance_type['extra_specs'], |
938 | + dict(cpu_arch="x86_64", |
939 | + cpu_model="Nehalem", |
940 | + xpu_arch="fermi", |
941 | + xpus="2", |
942 | + xpu_model="Tesla 2050")) |
943 | + |
944 | + instance_type = db.api.instance_type_get_by_name( |
945 | + context.get_admin_context(), |
946 | + "m1.small") |
947 | + self.assertEquals(instance_type['extra_specs'], {}) |
948 | + |
949 | + def test_instance_type_get_by_id_with_extra_specs(self): |
950 | + instance_type = db.api.instance_type_get_by_flavor_id( |
951 | + context.get_admin_context(), |
952 | + 105) |
953 | + self.assertEquals(instance_type['extra_specs'], |
954 | + dict(cpu_arch="x86_64", |
955 | + cpu_model="Nehalem", |
956 | + xpu_arch="fermi", |
957 | + xpus="2", |
958 | + xpu_model="Tesla 2050")) |
959 | + |
960 | + instance_type = db.api.instance_type_get_by_flavor_id( |
961 | + context.get_admin_context(), |
962 | + 2) |
963 | + self.assertEquals(instance_type['extra_specs'], {}) |
964 | + |
965 | + def test_instance_type_get_all(self): |
966 | + specs = dict(cpu_arch="x86_64", |
967 | + cpu_model="Nehalem", |
968 | + xpu_arch="fermi", |
969 | + xpus='2', |
970 | + xpu_model="Tesla 2050") |
971 | + |
972 | + types = db.api.instance_type_get_all(context.get_admin_context()) |
973 | + |
974 | + self.assertEquals(types['cg1.4xlarge']['extra_specs'], specs) |
975 | + self.assertEquals(types['m1.small']['extra_specs'], {}) |
976 | |
977 | === modified file 'tools/pip-requires' |
978 | --- tools/pip-requires 2011-06-25 19:38:07 +0000 |
979 | +++ tools/pip-requires 2011-06-27 00:06:02 +0000 |
980 | @@ -1,5 +1,5 @@ |
981 | SQLAlchemy==0.6.3 |
982 | -pep8==0.5.0 |
983 | +pep8==0.6.1 |
984 | pylint==0.19 |
985 | Cheetah==2.4.4 |
986 | M2Crypto==0.20.2 |
I'm unsure how this would show up in the api--and I would like to see that as part of this change
However, at minimum you need to bump your migration number.