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
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