Merge lp:~usc-isi/nova/instance_type_extra_specs into lp:~hudson-openstack/nova/trunk

Proposed by Lorin Hochstein
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
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

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: InstanceTypeExtraSpecs. This is modeled on the existing InstanceMetadata table, except that it is associated with additional capability requirements of instance types rather than generic metadata for instances.

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.

To post a comment you must log in.
Revision history for this message
Mark Washenberger (markwash) wrote :

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.

Revision history for this message
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).

Revision history for this message
Mark Washenberger (markwash) wrote :

What you're saying is that a user finds out about the capabilities associated with each flavor/instance_type is by some mechanism outside of the api. (Or in any case that they _can_ find out about the capabilities without using the api for now.) Does that sound right?

Revision history for this message
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.

Revision history for this message
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.

review: Needs Information
Revision history for this message
Josh Kearney (jk0) wrote :

Setting to WIP until above feedback is worked in.

Revision history for this message
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 "InstanceTypeExtraSpecs"? This would keep terminology 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.

> 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/HostFilterScheduler code in this merge proposal once I've made the additional changes mentioned above.

Revision history for this message
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 "InstanceTypeExtraSpecs"? This would keep terminology
> 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.

Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
Vish Ishaya (vishvananda) wrote :

This seems good. Just a question really. Why is the:
inst_dict[i['name']] = _inst_type_query_to_dict(i)

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_primitive() lacking something that requires a custom method to be written?

review: Needs Information
Revision history for this message
Lorin Hochstein (lorinh) wrote :

> This seems good. Just a question really. Why is the:
> inst_dict[i['name']] = _inst_type_query_to_dict(i)
>
> 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_primitive() lacking something that requires a custom method to be
> 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.

Revision history for this message
Lorin Hochstein (lorinh) wrote :

> This seems good. Just a question really. Why is the:
> inst_dict[i['name']] = _inst_type_query_to_dict(i)
>
> 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_primitive() lacking something that requires a custom method to be
> 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.

Revision history for this message
Vish Ishaya (vishvananda) wrote :

Ok thanks. I understand now. LGTM.

review: Approve
Revision history for this message
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/api/openstack/contrib. The other test files are fine where they are.

983: I think we might want version 0.6.1, not latest. Thoughts?

review: Needs Fixing
Revision history for this message
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/api/openstack/contrib.
> 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.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Thanks, Lorin.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'nova/api/openstack/contrib/flavorextraspecs.py'
--- nova/api/openstack/contrib/flavorextraspecs.py 1970-01-01 00:00:00 +0000
+++ nova/api/openstack/contrib/flavorextraspecs.py 2011-06-27 00:06:02 +0000
@@ -0,0 +1,126 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 University of Southern California
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18""" The instance type extra specs extension"""
19
20from webob import exc
21
22from nova import db
23from nova import quota
24from nova.api.openstack import extensions
25from nova.api.openstack import faults
26from nova.api.openstack import wsgi
27
28
29class FlavorExtraSpecsController(object):
30 """ The flavor extra specs API controller for the Openstack API """
31
32 def _get_extra_specs(self, context, flavor_id):
33 extra_specs = db.api.instance_type_extra_specs_get(context, flavor_id)
34 specs_dict = {}
35 for key, value in extra_specs.iteritems():
36 specs_dict[key] = value
37 return dict(extra_specs=specs_dict)
38
39 def _check_body(self, body):
40 if body == None or body == "":
41 expl = _('No Request Body')
42 raise exc.HTTPBadRequest(explanation=expl)
43
44 def index(self, req, flavor_id):
45 """ Returns the list of extra specs for a givenflavor """
46 context = req.environ['nova.context']
47 return self._get_extra_specs(context, flavor_id)
48
49 def create(self, req, flavor_id, body):
50 self._check_body(body)
51 context = req.environ['nova.context']
52 specs = body.get('extra_specs')
53 try:
54 db.api.instance_type_extra_specs_update_or_create(context,
55 flavor_id,
56 specs)
57 except quota.QuotaError as error:
58 self._handle_quota_error(error)
59 return body
60
61 def update(self, req, flavor_id, id, body):
62 self._check_body(body)
63 context = req.environ['nova.context']
64 if not id in body:
65 expl = _('Request body and URI mismatch')
66 raise exc.HTTPBadRequest(explanation=expl)
67 if len(body) > 1:
68 expl = _('Request body contains too many items')
69 raise exc.HTTPBadRequest(explanation=expl)
70 try:
71 db.api.instance_type_extra_specs_update_or_create(context,
72 flavor_id,
73 body)
74 except quota.QuotaError as error:
75 self._handle_quota_error(error)
76
77 return body
78
79 def show(self, req, flavor_id, id):
80 """ Return a single extra spec item """
81 context = req.environ['nova.context']
82 specs = self._get_extra_specs(context, flavor_id)
83 if id in specs['extra_specs']:
84 return {id: specs['extra_specs'][id]}
85 else:
86 return faults.Fault(exc.HTTPNotFound())
87
88 def delete(self, req, flavor_id, id):
89 """ Deletes an existing extra spec """
90 context = req.environ['nova.context']
91 db.api.instance_type_extra_specs_delete(context, flavor_id, id)
92
93 def _handle_quota_error(self, error):
94 """Reraise quota errors as api-specific http exceptions."""
95 if error.code == "MetadataLimitExceeded":
96 raise exc.HTTPBadRequest(explanation=error.message)
97 raise error
98
99
100class Flavorextraspecs(extensions.ExtensionDescriptor):
101
102 def get_name(self):
103 return "FlavorExtraSpecs"
104
105 def get_alias(self):
106 return "os-flavor-extra-specs"
107
108 def get_description(self):
109 return "Instance type (flavor) extra specs"
110
111 def get_namespace(self):
112 return \
113 "http://docs.openstack.org/ext/flavor_extra_specs/api/v1.1"
114
115 def get_updated(self):
116 return "2011-06-23T00:00:00+00:00"
117
118 def get_resources(self):
119 resources = []
120 res = extensions.ResourceExtension(
121 'os-extra_specs',
122 FlavorExtraSpecsController(),
123 parent=dict(member_name='flavor', collection_name='flavors'))
124
125 resources.append(res)
126 return resources
0127
=== modified file 'nova/db/api.py'
--- nova/db/api.py 2011-06-25 19:38:07 +0000
+++ nova/db/api.py 2011-06-27 00:06:02 +0000
@@ -1339,3 +1339,24 @@
1339def agent_build_update(context, agent_build_id, values):1339def agent_build_update(context, agent_build_id, values):
1340 """Update agent build entry."""1340 """Update agent build entry."""
1341 IMPL.agent_build_update(context, agent_build_id, values)1341 IMPL.agent_build_update(context, agent_build_id, values)
1342
1343
1344####################
1345
1346
1347def instance_type_extra_specs_get(context, instance_type_id):
1348 """Get all extra specs for an instance type."""
1349 return IMPL.instance_type_extra_specs_get(context, instance_type_id)
1350
1351
1352def instance_type_extra_specs_delete(context, instance_type_id, key):
1353 """Delete the given extra specs item."""
1354 IMPL.instance_type_extra_specs_delete(context, instance_type_id, key)
1355
1356
1357def instance_type_extra_specs_update_or_create(context, instance_type_id,
1358 extra_specs):
1359 """Create or update instance type extra specs. This adds or modifies the
1360 key/value pairs specified in the extra specs dict argument"""
1361 IMPL.instance_type_extra_specs_update_or_create(context, instance_type_id,
1362 extra_specs)
13421363
=== modified file 'nova/db/sqlalchemy/api.py'
--- nova/db/sqlalchemy/api.py 2011-06-25 19:38:07 +0000
+++ nova/db/sqlalchemy/api.py 2011-06-27 00:06:02 +0000
@@ -2597,7 +2597,22 @@
25972597
2598@require_admin_context2598@require_admin_context
2599def instance_type_create(_context, values):2599def instance_type_create(_context, values):
2600 """Create a new instance type. In order to pass in extra specs,
2601 the values dict should contain a 'extra_specs' key/value pair:
2602
2603 {'extra_specs' : {'k1': 'v1', 'k2': 'v2', ...}}
2604
2605 """
2600 try:2606 try:
2607 specs = values.get('extra_specs')
2608 specs_refs = []
2609 if specs:
2610 for k, v in specs.iteritems():
2611 specs_ref = models.InstanceTypeExtraSpecs()
2612 specs_ref['key'] = k
2613 specs_ref['value'] = v
2614 specs_refs.append(specs_ref)
2615 values['extra_specs'] = specs_refs
2601 instance_type_ref = models.InstanceTypes()2616 instance_type_ref = models.InstanceTypes()
2602 instance_type_ref.update(values)2617 instance_type_ref.update(values)
2603 instance_type_ref.save()2618 instance_type_ref.save()
@@ -2606,6 +2621,25 @@
2606 return instance_type_ref2621 return instance_type_ref
26072622
26082623
2624def _dict_with_extra_specs(inst_type_query):
2625 """Takes an instance type query returned by sqlalchemy
2626 and returns it as a dictionary, converting the extra_specs
2627 entry from a list of dicts:
2628
2629 'extra_specs' : [{'key': 'k1', 'value': 'v1', ...}, ...]
2630
2631 to a single dict:
2632
2633 'extra_specs' : {'k1': 'v1'}
2634
2635 """
2636 inst_type_dict = dict(inst_type_query)
2637 extra_specs = dict([(x['key'], x['value']) for x in \
2638 inst_type_query['extra_specs']])
2639 inst_type_dict['extra_specs'] = extra_specs
2640 return inst_type_dict
2641
2642
2609@require_context2643@require_context
2610def instance_type_get_all(context, inactive=False):2644def instance_type_get_all(context, inactive=False):
2611 """2645 """
@@ -2614,17 +2648,19 @@
2614 session = get_session()2648 session = get_session()
2615 if inactive:2649 if inactive:
2616 inst_types = session.query(models.InstanceTypes).\2650 inst_types = session.query(models.InstanceTypes).\
2651 options(joinedload('extra_specs')).\
2617 order_by("name").\2652 order_by("name").\
2618 all()2653 all()
2619 else:2654 else:
2620 inst_types = session.query(models.InstanceTypes).\2655 inst_types = session.query(models.InstanceTypes).\
2656 options(joinedload('extra_specs')).\
2621 filter_by(deleted=False).\2657 filter_by(deleted=False).\
2622 order_by("name").\2658 order_by("name").\
2623 all()2659 all()
2624 if inst_types:2660 if inst_types:
2625 inst_dict = {}2661 inst_dict = {}
2626 for i in inst_types:2662 for i in inst_types:
2627 inst_dict[i['name']] = dict(i)2663 inst_dict[i['name']] = _dict_with_extra_specs(i)
2628 return inst_dict2664 return inst_dict
2629 else:2665 else:
2630 raise exception.NoInstanceTypesFound()2666 raise exception.NoInstanceTypesFound()
@@ -2635,12 +2671,14 @@
2635 """Returns a dict describing specific instance_type"""2671 """Returns a dict describing specific instance_type"""
2636 session = get_session()2672 session = get_session()
2637 inst_type = session.query(models.InstanceTypes).\2673 inst_type = session.query(models.InstanceTypes).\
2674 options(joinedload('extra_specs')).\
2638 filter_by(id=id).\2675 filter_by(id=id).\
2639 first()2676 first()
2677
2640 if not inst_type:2678 if not inst_type:
2641 raise exception.InstanceTypeNotFound(instance_type=id)2679 raise exception.InstanceTypeNotFound(instance_type=id)
2642 else:2680 else:
2643 return dict(inst_type)2681 return _dict_with_extra_specs(inst_type)
26442682
26452683
2646@require_context2684@require_context
@@ -2648,12 +2686,13 @@
2648 """Returns a dict describing specific instance_type"""2686 """Returns a dict describing specific instance_type"""
2649 session = get_session()2687 session = get_session()
2650 inst_type = session.query(models.InstanceTypes).\2688 inst_type = session.query(models.InstanceTypes).\
2689 options(joinedload('extra_specs')).\
2651 filter_by(name=name).\2690 filter_by(name=name).\
2652 first()2691 first()
2653 if not inst_type:2692 if not inst_type:
2654 raise exception.InstanceTypeNotFoundByName(instance_type_name=name)2693 raise exception.InstanceTypeNotFoundByName(instance_type_name=name)
2655 else:2694 else:
2656 return dict(inst_type)2695 return _dict_with_extra_specs(inst_type)
26572696
26582697
2659@require_context2698@require_context
@@ -2661,12 +2700,13 @@
2661 """Returns a dict describing specific flavor_id"""2700 """Returns a dict describing specific flavor_id"""
2662 session = get_session()2701 session = get_session()
2663 inst_type = session.query(models.InstanceTypes).\2702 inst_type = session.query(models.InstanceTypes).\
2703 options(joinedload('extra_specs')).\
2664 filter_by(flavorid=int(id)).\2704 filter_by(flavorid=int(id)).\
2665 first()2705 first()
2666 if not inst_type:2706 if not inst_type:
2667 raise exception.FlavorNotFound(flavor_id=id)2707 raise exception.FlavorNotFound(flavor_id=id)
2668 else:2708 else:
2669 return dict(inst_type)2709 return _dict_with_extra_specs(inst_type)
26702710
26712711
2672@require_admin_context2712@require_admin_context
@@ -2834,6 +2874,9 @@
2834 return metadata2874 return metadata
28352875
28362876
2877####################
2878
2879
2837@require_admin_context2880@require_admin_context
2838def agent_build_create(context, values):2881def agent_build_create(context, values):
2839 agent_build_ref = models.AgentBuild()2882 agent_build_ref = models.AgentBuild()
@@ -2883,3 +2926,70 @@
2883 first()2926 first()
2884 agent_build_ref.update(values)2927 agent_build_ref.update(values)
2885 agent_build_ref.save(session=session)2928 agent_build_ref.save(session=session)
2929
2930
2931####################
2932
2933
2934@require_context
2935def instance_type_extra_specs_get(context, instance_type_id):
2936 session = get_session()
2937
2938 spec_results = session.query(models.InstanceTypeExtraSpecs).\
2939 filter_by(instance_type_id=instance_type_id).\
2940 filter_by(deleted=False).\
2941 all()
2942
2943 spec_dict = {}
2944 for i in spec_results:
2945 spec_dict[i['key']] = i['value']
2946 return spec_dict
2947
2948
2949@require_context
2950def instance_type_extra_specs_delete(context, instance_type_id, key):
2951 session = get_session()
2952 session.query(models.InstanceTypeExtraSpecs).\
2953 filter_by(instance_type_id=instance_type_id).\
2954 filter_by(key=key).\
2955 filter_by(deleted=False).\
2956 update({'deleted': True,
2957 'deleted_at': utils.utcnow(),
2958 'updated_at': literal_column('updated_at')})
2959
2960
2961@require_context
2962def instance_type_extra_specs_get_item(context, instance_type_id, key):
2963 session = get_session()
2964
2965 sppec_result = session.query(models.InstanceTypeExtraSpecs).\
2966 filter_by(instance_type_id=instance_type_id).\
2967 filter_by(key=key).\
2968 filter_by(deleted=False).\
2969 first()
2970
2971 if not spec_result:
2972 raise exception.\
2973 InstanceTypeExtraSpecsNotFound(extra_specs_key=key,
2974 instance_type_id=instance_type_id)
2975 return spec_result
2976
2977
2978@require_context
2979def instance_type_extra_specs_update_or_create(context, instance_type_id,
2980 specs):
2981 session = get_session()
2982 spec_ref = None
2983 for key, value in specs.iteritems():
2984 try:
2985 spec_ref = instance_type_extra_specs_get_item(context,
2986 instance_type_id,
2987 key,
2988 session)
2989 except:
2990 spec_ref = models.InstanceTypeExtraSpecs()
2991 spec_ref.update({"key": key, "value": value,
2992 "instance_type_id": instance_type_id,
2993 "deleted": 0})
2994 spec_ref.save(session=session)
2995 return specs
28862996
=== added file 'nova/db/sqlalchemy/migrate_repo/versions/028_add_instance_type_extra_specs.py'
--- nova/db/sqlalchemy/migrate_repo/versions/028_add_instance_type_extra_specs.py 1970-01-01 00:00:00 +0000
+++ nova/db/sqlalchemy/migrate_repo/versions/028_add_instance_type_extra_specs.py 2011-06-27 00:06:02 +0000
@@ -0,0 +1,67 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 University of Southern California
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer
18from sqlalchemy import MetaData, String, Table
19from nova import log as logging
20
21meta = MetaData()
22
23# Just for the ForeignKey and column creation to succeed, these are not the
24# actual definitions of instances or services.
25instance_types = Table('instance_types', meta,
26 Column('id', Integer(), primary_key=True, nullable=False),
27 )
28
29#
30# New Tables
31#
32
33instance_type_extra_specs_table = Table('instance_type_extra_specs', meta,
34 Column('created_at', DateTime(timezone=False)),
35 Column('updated_at', DateTime(timezone=False)),
36 Column('deleted_at', DateTime(timezone=False)),
37 Column('deleted', Boolean(create_constraint=True, name=None)),
38 Column('id', Integer(), primary_key=True, nullable=False),
39 Column('instance_type_id',
40 Integer(),
41 ForeignKey('instance_types.id'),
42 nullable=False),
43 Column('key',
44 String(length=255, convert_unicode=False, assert_unicode=None,
45 unicode_error=None, _warn_on_bytestring=False)),
46 Column('value',
47 String(length=255, convert_unicode=False, assert_unicode=None,
48 unicode_error=None, _warn_on_bytestring=False)))
49
50
51def upgrade(migrate_engine):
52 # Upgrade operations go here. Don't create your own engine;
53 # bind migrate_engine to your metadata
54 meta.bind = migrate_engine
55 for table in (instance_type_extra_specs_table, ):
56 try:
57 table.create()
58 except Exception:
59 logging.info(repr(table))
60 logging.exception('Exception while creating table')
61 raise
62
63
64def downgrade(migrate_engine):
65 # Operations to reverse the above upgrade go here.
66 for table in (instance_type_extra_specs_table, ):
67 table.drop()
068
=== modified file 'nova/db/sqlalchemy/models.py'
--- nova/db/sqlalchemy/models.py 2011-06-24 12:01:51 +0000
+++ nova/db/sqlalchemy/models.py 2011-06-27 00:06:02 +0000
@@ -716,6 +716,21 @@
716 'InstanceMetadata.deleted == False)')716 'InstanceMetadata.deleted == False)')
717717
718718
719class InstanceTypeExtraSpecs(BASE, NovaBase):
720 """Represents additional specs as key/value pairs for an instance_type"""
721 __tablename__ = 'instance_type_extra_specs'
722 id = Column(Integer, primary_key=True)
723 key = Column(String(255))
724 value = Column(String(255))
725 instance_type_id = Column(Integer, ForeignKey('instance_types.id'),
726 nullable=False)
727 instance_type = relationship(InstanceTypes, backref="extra_specs",
728 foreign_keys=instance_type_id,
729 primaryjoin='and_('
730 'InstanceTypeExtraSpecs.instance_type_id == InstanceTypes.id,'
731 'InstanceTypeExtraSpecs.deleted == False)')
732
733
719class Zone(BASE, NovaBase):734class Zone(BASE, NovaBase):
720 """Represents a child zone of this zone."""735 """Represents a child zone of this zone."""
721 __tablename__ = 'zones'736 __tablename__ = 'zones'
@@ -750,7 +765,7 @@
750 Network, SecurityGroup, SecurityGroupIngressRule,765 Network, SecurityGroup, SecurityGroupIngressRule,
751 SecurityGroupInstanceAssociation, AuthToken, User,766 SecurityGroupInstanceAssociation, AuthToken, User,
752 Project, Certificate, ConsolePool, Console, Zone,767 Project, Certificate, ConsolePool, Console, Zone,
753 AgentBuild, InstanceMetadata, Migration)768 AgentBuild, InstanceMetadata, InstanceTypeExtraSpecs, Migration)
754 engine = create_engine(FLAGS.sql_connection, echo=False)769 engine = create_engine(FLAGS.sql_connection, echo=False)
755 for model in models:770 for model in models:
756 model.metadata.create_all(engine)771 model.metadata.create_all(engine)
757772
=== modified file 'nova/exception.py'
--- nova/exception.py 2011-06-24 12:01:51 +0000
+++ nova/exception.py 2011-06-27 00:06:02 +0000
@@ -504,6 +504,11 @@
504 "key %(metadata_key)s.")504 "key %(metadata_key)s.")
505505
506506
507class InstanceTypeExtraSpecsNotFound(NotFound):
508 message = _("Instance Type %(instance_type_id)s has no extra specs with "
509 "key %(extra_specs_key)s.")
510
511
507class LDAPObjectNotFound(NotFound):512class LDAPObjectNotFound(NotFound):
508 message = _("LDAP object could not be found")513 message = _("LDAP object could not be found")
509514
510515
=== modified file 'nova/scheduler/host_filter.py'
--- nova/scheduler/host_filter.py 2011-06-03 20:32:42 +0000
+++ nova/scheduler/host_filter.py 2011-06-27 00:06:02 +0000
@@ -93,6 +93,26 @@
93 """Use instance_type to filter hosts."""93 """Use instance_type to filter hosts."""
94 return (self._full_name(), instance_type)94 return (self._full_name(), instance_type)
9595
96 def _satisfies_extra_specs(self, capabilities, instance_type):
97 """Check that the capabilities provided by the compute service
98 satisfy the extra specs associated with the instance type"""
99
100 if 'extra_specs' not in instance_type:
101 return True
102
103 # Note(lorinh): For now, we are just checking exact matching on the
104 # values. Later on, we want to handle numerical
105 # values so we can represent things like number of GPU cards
106
107 try:
108 for key, value in instance_type['extra_specs'].iteritems():
109 if capabilities[key] != value:
110 return False
111 except KeyError:
112 return False
113
114 return True
115
96 def filter_hosts(self, zone_manager, query):116 def filter_hosts(self, zone_manager, query):
97 """Return a list of hosts that can create instance_type."""117 """Return a list of hosts that can create instance_type."""
98 instance_type = query118 instance_type = query
@@ -103,7 +123,11 @@
103 disk_bytes = capabilities['disk_available']123 disk_bytes = capabilities['disk_available']
104 spec_ram = instance_type['memory_mb']124 spec_ram = instance_type['memory_mb']
105 spec_disk = instance_type['local_gb']125 spec_disk = instance_type['local_gb']
106 if host_ram_mb >= spec_ram and disk_bytes >= spec_disk:126 extra_specs = instance_type['extra_specs']
127
128 if host_ram_mb >= spec_ram and \
129 disk_bytes >= spec_disk and \
130 self._satisfies_extra_specs(capabilities, instance_type):
107 selected_hosts.append((host, capabilities))131 selected_hosts.append((host, capabilities))
108 return selected_hosts132 return selected_hosts
109133
110134
=== added file 'nova/tests/api/openstack/extensions/test_flavors_extra_specs.py'
--- nova/tests/api/openstack/extensions/test_flavors_extra_specs.py 1970-01-01 00:00:00 +0000
+++ nova/tests/api/openstack/extensions/test_flavors_extra_specs.py 2011-06-27 00:06:02 +0000
@@ -0,0 +1,198 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 University of Southern California
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18import json
19import stubout
20import unittest
21import webob
22import os.path
23
24
25from nova import flags
26from nova.api import openstack
27from nova.api.openstack import auth
28from nova.api.openstack import extensions
29from nova.tests.api.openstack import fakes
30import nova.wsgi
31
32FLAGS = flags.FLAGS
33
34
35def return_create_flavor_extra_specs(context, flavor_id, extra_specs):
36 return stub_flavor_extra_specs()
37
38
39def return_flavor_extra_specs(context, flavor_id):
40 return stub_flavor_extra_specs()
41
42
43def return_flavor_extra_specs(context, flavor_id):
44 return stub_flavor_extra_specs()
45
46
47def return_empty_flavor_extra_specs(context, flavor_id):
48 return {}
49
50
51def delete_flavor_extra_specs(context, flavor_id, key):
52 pass
53
54
55def stub_flavor_extra_specs():
56 specs = {
57 "key1": "value1",
58 "key2": "value2",
59 "key3": "value3",
60 "key4": "value4",
61 "key5": "value5"}
62 return specs
63
64
65class FlavorsExtraSpecsTest(unittest.TestCase):
66
67 def setUp(self):
68 super(FlavorsExtraSpecsTest, self).setUp()
69 FLAGS.osapi_extensions_path = os.path.join(os.path.dirname(__file__),
70 "extensions")
71 self.stubs = stubout.StubOutForTesting()
72 fakes.FakeAuthManager.auth_data = {}
73 fakes.FakeAuthDatabase.data = {}
74 fakes.stub_out_auth(self.stubs)
75 fakes.stub_out_key_pair_funcs(self.stubs)
76 self.mware = auth.AuthMiddleware(
77 extensions.ExtensionMiddleware(
78 openstack.APIRouterV11()))
79
80 def tearDown(self):
81 self.stubs.UnsetAll()
82 super(FlavorsExtraSpecsTest, self).tearDown()
83
84 def test_index(self):
85 self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get',
86 return_flavor_extra_specs)
87 request = webob.Request.blank('/flavors/1/os-extra_specs')
88 res = request.get_response(self.mware)
89 self.assertEqual(200, res.status_int)
90 res_dict = json.loads(res.body)
91 self.assertEqual('application/json', res.headers['Content-Type'])
92 self.assertEqual('value1', res_dict['extra_specs']['key1'])
93
94 def test_index_no_data(self):
95 self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get',
96 return_empty_flavor_extra_specs)
97 req = webob.Request.blank('/flavors/1/os-extra_specs')
98 res = req.get_response(self.mware)
99 res_dict = json.loads(res.body)
100 self.assertEqual(200, res.status_int)
101 self.assertEqual('application/json', res.headers['Content-Type'])
102 self.assertEqual(0, len(res_dict['extra_specs']))
103
104 def test_show(self):
105 self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get',
106 return_flavor_extra_specs)
107 req = webob.Request.blank('/flavors/1/os-extra_specs/key5')
108 res = req.get_response(self.mware)
109 self.assertEqual(200, res.status_int)
110 res_dict = json.loads(res.body)
111 self.assertEqual('application/json', res.headers['Content-Type'])
112 self.assertEqual('value5', res_dict['key5'])
113
114 def test_show_spec_not_found(self):
115 self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get',
116 return_empty_flavor_extra_specs)
117 req = webob.Request.blank('/flavors/1/os-extra_specs/key6')
118 res = req.get_response(self.mware)
119 res_dict = json.loads(res.body)
120 self.assertEqual(404, res.status_int)
121
122 def test_delete(self):
123 self.stubs.Set(nova.db.api, 'instance_type_extra_specs_delete',
124 delete_flavor_extra_specs)
125 req = webob.Request.blank('/flavors/1/os-extra_specs/key5')
126 req.method = 'DELETE'
127 res = req.get_response(self.mware)
128 self.assertEqual(200, res.status_int)
129
130 def test_create(self):
131 self.stubs.Set(nova.db.api,
132 'instance_type_extra_specs_update_or_create',
133 return_create_flavor_extra_specs)
134 req = webob.Request.blank('/flavors/1/os-extra_specs')
135 req.method = 'POST'
136 req.body = '{"extra_specs": {"key1": "value1"}}'
137 req.headers["content-type"] = "application/json"
138 res = req.get_response(self.mware)
139 res_dict = json.loads(res.body)
140 self.assertEqual(200, res.status_int)
141 self.assertEqual('application/json', res.headers['Content-Type'])
142 self.assertEqual('value1', res_dict['extra_specs']['key1'])
143
144 def test_create_empty_body(self):
145 self.stubs.Set(nova.db.api,
146 'instance_type_extra_specs_update_or_create',
147 return_create_flavor_extra_specs)
148 req = webob.Request.blank('/flavors/1/os-extra_specs')
149 req.method = 'POST'
150 req.headers["content-type"] = "application/json"
151 res = req.get_response(self.mware)
152 self.assertEqual(400, res.status_int)
153
154 def test_update_item(self):
155 self.stubs.Set(nova.db.api,
156 'instance_type_extra_specs_update_or_create',
157 return_create_flavor_extra_specs)
158 req = webob.Request.blank('/flavors/1/os-extra_specs/key1')
159 req.method = 'PUT'
160 req.body = '{"key1": "value1"}'
161 req.headers["content-type"] = "application/json"
162 res = req.get_response(self.mware)
163 self.assertEqual(200, res.status_int)
164 self.assertEqual('application/json', res.headers['Content-Type'])
165 res_dict = json.loads(res.body)
166 self.assertEqual('value1', res_dict['key1'])
167
168 def test_update_item_empty_body(self):
169 self.stubs.Set(nova.db.api,
170 'instance_type_extra_specs_update_or_create',
171 return_create_flavor_extra_specs)
172 req = webob.Request.blank('/flavors/1/os-extra_specs/key1')
173 req.method = 'PUT'
174 req.headers["content-type"] = "application/json"
175 res = req.get_response(self.mware)
176 self.assertEqual(400, res.status_int)
177
178 def test_update_item_too_many_keys(self):
179 self.stubs.Set(nova.db.api,
180 'instance_type_extra_specs_update_or_create',
181 return_create_flavor_extra_specs)
182 req = webob.Request.blank('/flavors/1/os-extra_specs/key1')
183 req.method = 'PUT'
184 req.body = '{"key1": "value1", "key2": "value2"}'
185 req.headers["content-type"] = "application/json"
186 res = req.get_response(self.mware)
187 self.assertEqual(400, res.status_int)
188
189 def test_update_item_body_uri_mismatch(self):
190 self.stubs.Set(nova.db.api,
191 'instance_type_extra_specs_update_or_create',
192 return_create_flavor_extra_specs)
193 req = webob.Request.blank('/flavors/1/os-extra_specs/bad')
194 req.method = 'PUT'
195 req.body = '{"key1": "value1"}'
196 req.headers["content-type"] = "application/json"
197 res = req.get_response(self.mware)
198 self.assertEqual(400, res.status_int)
0199
=== modified file 'nova/tests/scheduler/test_host_filter.py'
--- nova/tests/scheduler/test_host_filter.py 2011-06-14 01:14:26 +0000
+++ nova/tests/scheduler/test_host_filter.py 2011-06-27 00:06:02 +0000
@@ -67,7 +67,18 @@
67 flavorid=1,67 flavorid=1,
68 swap=500,68 swap=500,
69 rxtx_quota=30000,69 rxtx_quota=30000,
70 rxtx_cap=200)70 rxtx_cap=200,
71 extra_specs={})
72 self.gpu_instance_type = dict(name='tiny.gpu',
73 memory_mb=50,
74 vcpus=10,
75 local_gb=500,
76 flavorid=2,
77 swap=500,
78 rxtx_quota=30000,
79 rxtx_cap=200,
80 extra_specs={'xpu_arch': 'fermi',
81 'xpu_info': 'Tesla 2050'})
7182
72 self.zone_manager = FakeZoneManager()83 self.zone_manager = FakeZoneManager()
73 states = {}84 states = {}
@@ -75,6 +86,18 @@
75 states['host%02d' % (x + 1)] = {'compute': self._host_caps(x)}86 states['host%02d' % (x + 1)] = {'compute': self._host_caps(x)}
76 self.zone_manager.service_states = states87 self.zone_manager.service_states = states
7788
89 # Add some extra capabilities to some hosts
90 host07 = self.zone_manager.service_states['host07']['compute']
91 host07['xpu_arch'] = 'fermi'
92 host07['xpu_info'] = 'Tesla 2050'
93
94 host08 = self.zone_manager.service_states['host08']['compute']
95 host08['xpu_arch'] = 'radeon'
96
97 host09 = self.zone_manager.service_states['host09']['compute']
98 host09['xpu_arch'] = 'fermi'
99 host09['xpu_info'] = 'Tesla 2150'
100
78 def tearDown(self):101 def tearDown(self):
79 FLAGS.default_host_filter = self.old_flag102 FLAGS.default_host_filter = self.old_flag
80103
@@ -116,6 +139,17 @@
116 self.assertEquals('host05', just_hosts[0])139 self.assertEquals('host05', just_hosts[0])
117 self.assertEquals('host10', just_hosts[5])140 self.assertEquals('host10', just_hosts[5])
118141
142 def test_instance_type_filter_extra_specs(self):
143 hf = host_filter.InstanceTypeFilter()
144 # filter all hosts that can support 50 ram and 500 disk
145 name, cooked = hf.instance_type_to_filter(self.gpu_instance_type)
146 self.assertEquals('nova.scheduler.host_filter.InstanceTypeFilter',
147 name)
148 hosts = hf.filter_hosts(self.zone_manager, cooked)
149 self.assertEquals(1, len(hosts))
150 just_hosts = [host for host, caps in hosts]
151 self.assertEquals('host07', just_hosts[0])
152
119 def test_json_filter(self):153 def test_json_filter(self):
120 hf = host_filter.JsonFilter()154 hf = host_filter.JsonFilter()
121 # filter all hosts that can support 50 ram and 500 disk155 # filter all hosts that can support 50 ram and 500 disk
122156
=== modified file 'nova/tests/test_host_filter.py'
--- nova/tests/test_host_filter.py 2011-06-02 19:08:19 +0000
+++ nova/tests/test_host_filter.py 2011-06-27 00:06:02 +0000
@@ -67,7 +67,8 @@
67 flavorid=1,67 flavorid=1,
68 swap=500,68 swap=500,
69 rxtx_quota=30000,69 rxtx_quota=30000,
70 rxtx_cap=200)70 rxtx_cap=200,
71 extra_specs={})
7172
72 self.zone_manager = FakeZoneManager()73 self.zone_manager = FakeZoneManager()
73 states = {}74 states = {}
7475
=== added file 'nova/tests/test_instance_types_extra_specs.py'
--- nova/tests/test_instance_types_extra_specs.py 1970-01-01 00:00:00 +0000
+++ nova/tests/test_instance_types_extra_specs.py 2011-06-27 00:06:02 +0000
@@ -0,0 +1,165 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 University of Southern California
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15"""
16Unit Tests for instance types extra specs code
17"""
18
19from nova import context
20from nova import db
21from nova import test
22from nova.db.sqlalchemy.session import get_session
23from nova.db.sqlalchemy import models
24
25
26class InstanceTypeExtraSpecsTestCase(test.TestCase):
27
28 def setUp(self):
29 super(InstanceTypeExtraSpecsTestCase, self).setUp()
30 self.context = context.get_admin_context()
31 values = dict(name="cg1.4xlarge",
32 memory_mb=22000,
33 vcpus=8,
34 local_gb=1690,
35 flavorid=105)
36 specs = dict(cpu_arch="x86_64",
37 cpu_model="Nehalem",
38 xpu_arch="fermi",
39 xpus=2,
40 xpu_model="Tesla 2050")
41 values['extra_specs'] = specs
42 ref = db.api.instance_type_create(self.context,
43 values)
44 self.instance_type_id = ref.id
45
46 def tearDown(self):
47 # Remove the instance type from the database
48 db.api.instance_type_purge(context.get_admin_context(), "cg1.4xlarge")
49 super(InstanceTypeExtraSpecsTestCase, self).tearDown()
50
51 def test_instance_type_specs_get(self):
52 expected_specs = dict(cpu_arch="x86_64",
53 cpu_model="Nehalem",
54 xpu_arch="fermi",
55 xpus="2",
56 xpu_model="Tesla 2050")
57 actual_specs = db.api.instance_type_extra_specs_get(
58 context.get_admin_context(),
59 self.instance_type_id)
60 self.assertEquals(expected_specs, actual_specs)
61
62 def test_instance_type_extra_specs_delete(self):
63 expected_specs = dict(cpu_arch="x86_64",
64 cpu_model="Nehalem",
65 xpu_arch="fermi",
66 xpus="2")
67 db.api.instance_type_extra_specs_delete(context.get_admin_context(),
68 self.instance_type_id,
69 "xpu_model")
70 actual_specs = db.api.instance_type_extra_specs_get(
71 context.get_admin_context(),
72 self.instance_type_id)
73 self.assertEquals(expected_specs, actual_specs)
74
75 def test_instance_type_extra_specs_update(self):
76 expected_specs = dict(cpu_arch="x86_64",
77 cpu_model="Sandy Bridge",
78 xpu_arch="fermi",
79 xpus="2",
80 xpu_model="Tesla 2050")
81 db.api.instance_type_extra_specs_update_or_create(
82 context.get_admin_context(),
83 self.instance_type_id,
84 dict(cpu_model="Sandy Bridge"))
85 actual_specs = db.api.instance_type_extra_specs_get(
86 context.get_admin_context(),
87 self.instance_type_id)
88 self.assertEquals(expected_specs, actual_specs)
89
90 def test_instance_type_extra_specs_create(self):
91 expected_specs = dict(cpu_arch="x86_64",
92 cpu_model="Nehalem",
93 xpu_arch="fermi",
94 xpus="2",
95 xpu_model="Tesla 2050",
96 net_arch="ethernet",
97 net_mbps="10000")
98 db.api.instance_type_extra_specs_update_or_create(
99 context.get_admin_context(),
100 self.instance_type_id,
101 dict(net_arch="ethernet",
102 net_mbps=10000))
103 actual_specs = db.api.instance_type_extra_specs_get(
104 context.get_admin_context(),
105 self.instance_type_id)
106 self.assertEquals(expected_specs, actual_specs)
107
108 def test_instance_type_get_by_id_with_extra_specs(self):
109 instance_type = db.api.instance_type_get_by_id(
110 context.get_admin_context(),
111 self.instance_type_id)
112 self.assertEquals(instance_type['extra_specs'],
113 dict(cpu_arch="x86_64",
114 cpu_model="Nehalem",
115 xpu_arch="fermi",
116 xpus="2",
117 xpu_model="Tesla 2050"))
118 instance_type = db.api.instance_type_get_by_id(
119 context.get_admin_context(),
120 5)
121 self.assertEquals(instance_type['extra_specs'], {})
122
123 def test_instance_type_get_by_name_with_extra_specs(self):
124 instance_type = db.api.instance_type_get_by_name(
125 context.get_admin_context(),
126 "cg1.4xlarge")
127 self.assertEquals(instance_type['extra_specs'],
128 dict(cpu_arch="x86_64",
129 cpu_model="Nehalem",
130 xpu_arch="fermi",
131 xpus="2",
132 xpu_model="Tesla 2050"))
133
134 instance_type = db.api.instance_type_get_by_name(
135 context.get_admin_context(),
136 "m1.small")
137 self.assertEquals(instance_type['extra_specs'], {})
138
139 def test_instance_type_get_by_id_with_extra_specs(self):
140 instance_type = db.api.instance_type_get_by_flavor_id(
141 context.get_admin_context(),
142 105)
143 self.assertEquals(instance_type['extra_specs'],
144 dict(cpu_arch="x86_64",
145 cpu_model="Nehalem",
146 xpu_arch="fermi",
147 xpus="2",
148 xpu_model="Tesla 2050"))
149
150 instance_type = db.api.instance_type_get_by_flavor_id(
151 context.get_admin_context(),
152 2)
153 self.assertEquals(instance_type['extra_specs'], {})
154
155 def test_instance_type_get_all(self):
156 specs = dict(cpu_arch="x86_64",
157 cpu_model="Nehalem",
158 xpu_arch="fermi",
159 xpus='2',
160 xpu_model="Tesla 2050")
161
162 types = db.api.instance_type_get_all(context.get_admin_context())
163
164 self.assertEquals(types['cg1.4xlarge']['extra_specs'], specs)
165 self.assertEquals(types['m1.small']['extra_specs'], {})
0166
=== modified file 'tools/pip-requires'
--- tools/pip-requires 2011-06-25 19:38:07 +0000
+++ tools/pip-requires 2011-06-27 00:06:02 +0000
@@ -1,5 +1,5 @@
1SQLAlchemy==0.6.31SQLAlchemy==0.6.3
2pep8==0.5.02pep8==0.6.1
3pylint==0.193pylint==0.19
4Cheetah==2.4.44Cheetah==2.4.4
5M2Crypto==0.20.25M2Crypto==0.20.2