Clean up non-spec output in flavor extensions

Adds option to cache flavors in the request object like instances.
Modifies the flavorextradata extension to use the cache and optimizes
tests. Adds flavor_disabled extension to control the extra data and
includes tests. Fixes api samples to show the new extension.

Fixes bug 1043585

Change-Id: Ie89df24a2891e3869d3fb604e07c79e8c913f290
This commit is contained in:
Vishvananda Ishaya
2012-08-29 17:50:58 -07:00
parent 4a6193b5d2
commit f9fa7a68a3
17 changed files with 294 additions and 101 deletions

View File

@@ -36,6 +36,7 @@
"compute_extension:extended_server_attributes": [["rule:admin_api"]],
"compute_extension:extended_status": [],
"compute_extension:flavor_access": [],
"compute_extension:flavor_disabled": [],
"compute_extension:flavorextradata": [],
"compute_extension:flavorextraspecs": [],
"compute_extension:flavormanage": [["rule:admin_api"]],

View File

@@ -0,0 +1,89 @@
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""The Flavor Disabled API extension."""
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
authorize = extensions.soft_extension_authorizer('compute', 'flavor_disabled')
class FlavorDisabledController(wsgi.Controller):
def _extend_flavors(self, req, flavors):
for flavor in flavors:
db_flavor = req.get_db_flavor(flavor['id'])
key = "%s:disabled" % Flavor_disabled.alias
flavor[key] = db_flavor['disabled']
def _show(self, req, resp_obj):
if not authorize(req.environ['nova.context']):
return
if 'flavor' in resp_obj.obj:
resp_obj.attach(xml=FlavorDisabledTemplate())
self._extend_flavors(req, [resp_obj.obj['flavor']])
@wsgi.extends
def show(self, req, resp_obj, id):
return self._show(req, resp_obj)
@wsgi.extends(action='create')
def create(self, req, resp_obj, body):
return self._show(req, resp_obj)
@wsgi.extends
def detail(self, req, resp_obj):
if not authorize(req.environ['nova.context']):
return
resp_obj.attach(xml=FlavorsDisabledTemplate())
self._extend_flavors(req, list(resp_obj.obj['flavors']))
class Flavor_disabled(extensions.ExtensionDescriptor):
"""Support to show the disabled status of a flavor"""
name = "FlavorDisabled"
alias = "OS-FLV-DISABLED"
namespace = ("http://docs.openstack.org/compute/ext/"
"flavor_disabled/api/v1.1")
updated = "2012-08-29T00:00:00+00:00"
def get_controller_extensions(self):
controller = FlavorDisabledController()
extension = extensions.ControllerExtension(self, 'flavors', controller)
return [extension]
def make_flavor(elem):
elem.set('{%s}disabled' % Flavor_disabled.namespace,
'%s:disabled' % Flavor_disabled.alias)
class FlavorDisabledTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('flavor', selector='flavor')
make_flavor(root)
return xmlutil.SlaveTemplate(root, 1, nsmap={
Flavor_disabled.alias: Flavor_disabled.namespace})
class FlavorsDisabledTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('flavors')
elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
make_flavor(elem)
return xmlutil.SlaveTemplate(root, 1, nsmap={
Flavor_disabled.alias: Flavor_disabled.namespace})

View File

@@ -27,71 +27,39 @@ attributes. This extension adds to that list:
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
from nova.compute import instance_types
from nova import exception
authorize = extensions.soft_extension_authorizer('compute', 'flavorextradata')
class FlavorextradataController(wsgi.Controller):
def _get_flavor_refs(self, context):
"""Return a dictionary mapping flavorid to flavor_ref."""
def _extend_flavors(self, req, flavors):
for flavor in flavors:
db_flavor = req.get_db_flavor(flavor['id'])
key = "%s:ephemeral" % Flavorextradata.alias
flavor[key] = db_flavor['ephemeral_gb']
flavor_refs = instance_types.get_all_types(context)
rval = {}
for name, obj in flavor_refs.iteritems():
rval[obj['flavorid']] = obj
return rval
def _extend_flavor(self, flavor_rval, flavor_ref):
key = "%s:ephemeral" % (Flavorextradata.alias)
flavor_rval[key] = flavor_ref['ephemeral_gb']
def _show(self, req, resp_obj):
if not authorize(req.environ['nova.context']):
return
if 'flavor' in resp_obj.obj:
resp_obj.attach(xml=FlavorextradatumTemplate())
self._extend_flavors(req, [resp_obj.obj['flavor']])
@wsgi.extends
def show(self, req, resp_obj, id):
context = req.environ['nova.context']
if authorize(context):
# Attach our slave template to the response object
resp_obj.attach(xml=FlavorextradatumTemplate())
return self._show(req, resp_obj)
try:
flavor_ref = instance_types.get_instance_type_by_flavor_id(id)
except exception.FlavorNotFound:
explanation = _("Flavor not found.")
raise exception.HTTPNotFound(explanation=explanation)
self._extend_flavor(resp_obj.obj['flavor'], flavor_ref)
@wsgi.extends(action='create')
def create(self, req, resp_obj, body):
return self._show(req, resp_obj)
@wsgi.extends
def detail(self, req, resp_obj):
context = req.environ['nova.context']
if authorize(context):
# Attach our slave template to the response object
resp_obj.attach(xml=FlavorextradataTemplate())
flavors = list(resp_obj.obj['flavors'])
flavor_refs = self._get_flavor_refs(context)
for flavor_rval in flavors:
flavor_ref = flavor_refs[flavor_rval['id']]
self._extend_flavor(flavor_rval, flavor_ref)
@wsgi.extends(action='create')
def create(self, req, body, resp_obj):
context = req.environ['nova.context']
if authorize(context):
# Attach our slave template to the response object
resp_obj.attach(xml=FlavorextradatumTemplate())
try:
fid = resp_obj.obj['flavor']['id']
flavor_ref = instance_types.get_instance_type_by_flavor_id(fid)
except exception.FlavorNotFound:
explanation = _("Flavor not found.")
raise exception.HTTPNotFound(explanation=explanation)
self._extend_flavor(resp_obj.obj['flavor'], flavor_ref)
if not authorize(req.environ['nova.context']):
return
resp_obj.attach(xml=FlavorextradataTemplate())
self._extend_flavors(req, list(resp_obj.obj['flavors']))
class Flavorextradata(extensions.ExtensionDescriptor):

View File

@@ -71,6 +71,7 @@ class FlavorManageController(wsgi.Controller):
flavor = instance_types.create(name, memory_mb, vcpus,
root_gb, ephemeral_gb, flavorid,
swap, rxtx_factor, is_public)
req.cache_db_flavor(flavor)
except exception.InstanceTypeExists as err:
raise webob.exc.HTTPConflict(explanation=str(err))

View File

@@ -79,6 +79,7 @@ class Controller(wsgi.Controller):
def detail(self, req):
"""Return all flavors in detail."""
flavors = self._get_flavors(req)
req.cache_db_flavors(flavors)
return self._view_builder.detail(req, flavors)
@wsgi.serializers(xml=FlavorTemplate)
@@ -86,6 +87,7 @@ class Controller(wsgi.Controller):
"""Return data about the given flavor id."""
try:
flavor = instance_types.get_instance_type_by_flavor_id(id)
req.cache_db_flavor(flavor)
except exception.NotFound:
raise webob.exc.HTTPNotFound()

View File

@@ -49,12 +49,6 @@ class ViewBuilder(common.ViewBuilder):
},
}
# NOTE(sirp): disabled attribute is namespaced for now for
# compatability with the OpenStack API. This should ultimately be made
# a first class attribute.
flavor_dict["flavor"]["OS-FLV-DISABLED:disabled"] =\
flavor.get("disabled", "")
return flavor_dict
def index(self, request, flavors):

View File

@@ -66,49 +66,62 @@ class Request(webob.Request):
def __init__(self, *args, **kwargs):
super(Request, self).__init__(*args, **kwargs)
self._extension_data = {'db_instances': {}}
self._extension_data = {'db_items': {}}
def cache_db_instances(self, instances):
def cache_db_items(self, key, items, item_key='id'):
"""
Allow API methods to store instances from a DB query to be
Allow API methods to store objects from a DB query to be
used by API extensions within the same API request.
An instance of this class only lives for the lifetime of a
single API request, so there's no need to implement full
cache management.
"""
db_instances = self._extension_data['db_instances']
for instance in instances:
db_instances[instance['uuid']] = instance
db_items = self._extension_data['db_items'].setdefault(key, {})
for item in items:
db_items[item[item_key]] = item
def cache_db_instance(self, instance):
def get_db_items(self, key):
"""
Allow API methods to store an instance from a DB query to be
used by API extensions within the same API request.
An instance of this class only lives for the lifetime of a
single API request, so there's no need to implement full
cache management.
"""
self.cache_db_instances([instance])
def get_db_instances(self):
"""
Allow an API extension to get previously stored instances within
Allow an API extension to get previously stored objects within
the same API request.
Note that the instance data will be slightly stale.
Note that the object data will be slightly stale.
"""
return self._extension_data['db_instances']
return self._extension_data['db_items'][key]
def get_db_instance(self, instance_uuid):
def get_db_item(self, key, item_key):
"""
Allow an API extension to get a previously stored instance
Allow an API extension to get a previously stored object
within the same API request.
Note that the instance data will be slightly stale.
Note that the object data will be slightly stale.
"""
return self._extension_data['db_instances'].get(instance_uuid)
return self.get_db_items(key).get(item_key)
def cache_db_instances(self, instances):
self.cache_db_items('instances', instances, 'uuid')
def cache_db_instance(self, instance):
self.cache_db_items('instances', [instance], 'uuid')
def get_db_instances(self):
return self.get_db_items('instances')
def get_db_instance(self, instance_uuid):
return self.get_db_item('instances', instance_uuid)
def cache_db_flavors(self, flavors):
self.cache_db_items('flavors', flavors, 'flavorid')
def cache_db_flavor(self, flavor):
self.cache_db_items('flavors', [flavor], 'flavorid')
def get_db_flavors(self):
return self.get_db_items('flavors')
def get_db_flavor(self, flavorid):
return self.get_db_item('flavors', flavorid)
def best_match_content_type(self):
"""Determine the requested response content-type."""

View File

@@ -0,0 +1,112 @@
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from lxml import etree
import webob
from nova.api.openstack.compute.contrib import flavor_disabled
from nova.compute import instance_types
from nova import flags
from nova.openstack.common import jsonutils
from nova import test
from nova.tests.api.openstack import fakes
FLAGS = flags.FLAGS
FAKE_FLAVORS = {
'flavor 1': {
"flavorid": '1',
"name": 'flavor 1',
"memory_mb": '256',
"root_gb": '10',
"disabled": False,
},
'flavor 2': {
"flavorid": '2',
"name": 'flavor 2',
"memory_mb": '512',
"root_gb": '20',
"disabled": True,
},
}
def fake_instance_type_get_by_flavor_id(flavorid):
return FAKE_FLAVORS['flavor %s' % flavorid]
def fake_instance_type_get_all(*args, **kwargs):
return FAKE_FLAVORS
class FlavorDisabledTest(test.TestCase):
content_type = 'application/json'
prefix = '%s:' % flavor_disabled.Flavor_disabled.alias
def setUp(self):
super(FlavorDisabledTest, self).setUp()
ext = ('nova.api.openstack.compute.contrib'
'.flavor_disabled.Flavor_disabled')
self.flags(osapi_compute_extension=[ext])
fakes.stub_out_nw_api(self.stubs)
self.stubs.Set(instance_types, "get_all_types",
fake_instance_type_get_all)
self.stubs.Set(instance_types,
"get_instance_type_by_flavor_id",
fake_instance_type_get_by_flavor_id)
def _make_request(self, url):
req = webob.Request.blank(url)
req.headers['Accept'] = self.content_type
res = req.get_response(fakes.wsgi_app())
return res
def _get_flavor(self, body):
return jsonutils.loads(body).get('flavor')
def _get_flavors(self, body):
return jsonutils.loads(body).get('flavors')
def assertFlavorDisabled(self, flavor, disabled):
self.assertEqual(str(flavor.get('%sdisabled' % self.prefix)), disabled)
def test_show(self):
url = '/v2/fake/flavors/1'
res = self._make_request(url)
self.assertEqual(res.status_int, 200)
self.assertFlavorDisabled(self._get_flavor(res.body), 'False')
def test_detail(self):
url = '/v2/fake/flavors/detail'
res = self._make_request(url)
self.assertEqual(res.status_int, 200)
flavors = self._get_flavors(res.body)
self.assertFlavorDisabled(flavors[0], 'False')
self.assertFlavorDisabled(flavors[1], 'True')
class FlavorDisabledXmlTest(FlavorDisabledTest):
content_type = 'application/xml'
prefix = '{%s}' % flavor_disabled.Flavor_disabled.namespace
def _get_flavor(self, body):
return etree.XML(body)
def _get_flavors(self, body):
return etree.XML(body).getchildren()

View File

@@ -47,7 +47,8 @@ def fake_get_instance_type_by_flavor_id(flavorid):
'deleted_at': None,
'vcpu_weight': None,
'id': 7,
'is_public': True
'is_public': True,
'disabled': False,
}

View File

@@ -40,7 +40,6 @@ def fake_get_instance_type_by_flavor_id(flavorid):
'extra_specs': {},
'deleted_at': None,
'vcpu_weight': None,
'is_public': True
}
@@ -54,11 +53,14 @@ def fake_get_all_types(inactive=0, filters=None):
class FlavorextradataTest(test.TestCase):
def setUp(self):
super(FlavorextradataTest, self).setUp()
ext = ('nova.api.openstack.compute.contrib'
'.flavorextradata.Flavorextradata')
self.flags(osapi_compute_extension=[ext])
self.stubs.Set(instance_types, 'get_instance_type_by_flavor_id',
fake_get_instance_type_by_flavor_id)
self.stubs.Set(instance_types, 'get_all_types', fake_get_all_types)
def _verify_server_response(self, flavor, expected):
def _verify_flavor_response(self, flavor, expected):
for key in expected:
self.assertEquals(flavor[key], expected[key])
@@ -73,7 +75,6 @@ class FlavorextradataTest(test.TestCase):
'OS-FLV-EXT-DATA:ephemeral': 1,
'swap': 512,
'rxtx_factor': 1,
'os-flavor-access:is_public': True,
}
}
@@ -82,7 +83,7 @@ class FlavorextradataTest(test.TestCase):
req.headers['Content-Type'] = 'application/json'
res = req.get_response(fakes.wsgi_app())
body = jsonutils.loads(res.body)
self._verify_server_response(body['flavor'], expected['flavor'])
self._verify_flavor_response(body['flavor'], expected['flavor'])
def test_detail(self):
expected = [
@@ -95,7 +96,6 @@ class FlavorextradataTest(test.TestCase):
'OS-FLV-EXT-DATA:ephemeral': 1,
'swap': 512,
'rxtx_factor': 1,
'os-flavor-access:is_public': True,
},
{
'id': '2',
@@ -106,7 +106,6 @@ class FlavorextradataTest(test.TestCase):
'OS-FLV-EXT-DATA:ephemeral': 1,
'swap': 512,
'rxtx_factor': 1,
'os-flavor-access:is_public': True,
},
]
@@ -116,4 +115,4 @@ class FlavorextradataTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
body = jsonutils.loads(res.body)
for i, flavor in enumerate(body['flavors']):
self._verify_server_response(flavor, expected[i])
self._verify_flavor_response(flavor, expected[i])

View File

@@ -167,6 +167,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"ExtendedStatus",
"ExtendedServerAttributes",
"FlavorAccess",
"FlavorDisabled",
"FlavorExtraSpecs",
"FlavorExtraData",
"FlavorManage",

View File

@@ -113,7 +113,6 @@ class FlavorsTest(test.TestCase):
expected = {
"flavor": {
"id": "1",
"OS-FLV-DISABLED:disabled": False,
"name": "flavor 1",
"ram": "256",
"disk": "10",
@@ -142,7 +141,6 @@ class FlavorsTest(test.TestCase):
expected = {
"flavor": {
"id": "1",
"OS-FLV-DISABLED:disabled": False,
"name": "flavor 1",
"ram": "256",
"disk": "10",
@@ -313,7 +311,6 @@ class FlavorsTest(test.TestCase):
"flavors": [
{
"id": "1",
"OS-FLV-DISABLED:disabled": False,
"name": "flavor 1",
"ram": "256",
"disk": "10",
@@ -333,7 +330,6 @@ class FlavorsTest(test.TestCase):
},
{
"id": "2",
"OS-FLV-DISABLED:disabled": False,
"name": "flavor 2",
"ram": "512",
"disk": "20",
@@ -435,7 +431,6 @@ class FlavorsTest(test.TestCase):
"flavors": [
{
"id": "2",
"OS-FLV-DISABLED:disabled": False,
"name": "flavor 2",
"ram": "512",
"disk": "20",
@@ -793,11 +788,6 @@ class DisabledFlavorsWithRealDBTest(test.TestCase):
self.assertEqual(flavor['name'], self.disabled_type['name'])
# FIXME(sirp): the disabled field is currently namespaced so that we
# don't impact the OpenStack API. Eventually this should probably be
# made a first-class attribute in the next OSAPI version.
self.assert_('OS-FLV-DISABLED:disabled' in flavor)
def test_show_should_include_disabled_flavor_for_admin(self):
self.context.is_admin = True
@@ -805,4 +795,3 @@ class DisabledFlavorsWithRealDBTest(test.TestCase):
self.req, self.disabled_type['flavorid'])['flavor']
self.assertEqual(flavor['name'], self.disabled_type['name'])
self.assert_('OS-FLV-DISABLED:disabled' in flavor)

View File

@@ -32,6 +32,14 @@
"namespace": "http://docs.openstack.org/compute/ext/extended_status/api/v1.1",
"updated": "2011-11-03T00:00:00+00:00"
},
{
"alias": "OS-FLV-DISABLED",
"description": "Support to show the disabled status of a flavor",
"links": [],
"name": "FlavorDisabled",
"namespace": "http://docs.openstack.org/compute/ext/flavor_disabled/api/v1.1",
"updated": "2012-08-29T00:00:00+00:00"
},
{
"alias": "OS-FLV-EXT-DATA",
"description": "Provide additional data for flavors",

View File

@@ -32,6 +32,14 @@
"namespace": "http://docs.openstack.org/compute/ext/extended_status/api/v1.1",
"updated": "%(timestamp)s"
},
{
"alias": "OS-FLV-DISABLED",
"description": "%(text)s",
"links": [],
"name": "FlavorDisabled",
"namespace": "http://docs.openstack.org/compute/ext/flavor_disabled/api/v1.1",
"updated": "%(timestamp)s"
},
{
"alias": "OS-FLV-EXT-DATA",
"description": "%(text)s",

View File

@@ -12,6 +12,9 @@
<extension alias="OS-EXT-STS" updated="2011-11-03T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/extended_status/api/v1.1" name="ExtendedStatus">
<description>Extended Status support</description>
</extension>
<extension alias="OS-FLV-DISABLED" updated="2012-08-29T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/flavor_disabled/api/v1.1" name="FlavorDisabled">
<description>Support to show the disabled status of a flavor</description>
</extension>
<extension alias="OS-FLV-EXT-DATA" updated="2011-09-14T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/flavor_extra_data/api/v1.1" name="FlavorExtraData">
<description>Provide additional data for flavors</description>
</extension>

View File

@@ -12,6 +12,9 @@
<extension alias="OS-EXT-STS" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/extended_status/api/v1.1" name="ExtendedStatus">
<description>%(text)s</description>
</extension>
<extension alias="OS-FLV-DISABLED" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/flavor_disabled/api/v1.1" name="FlavorDisabled">
<description>%(text)s</description>
</extension>
<extension alias="OS-FLV-EXT-DATA" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/flavor_extra_data/api/v1.1" name="FlavorExtraData">
<description>%(text)s</description>
</extension>

View File

@@ -93,6 +93,7 @@
"compute_extension:extended_server_attributes": [],
"compute_extension:extended_status": [],
"compute_extension:flavor_access": [],
"compute_extension:flavor_disabled": [],
"compute_extension:flavorextradata": [],
"compute_extension:flavorextraspecs": [],
"compute_extension:flavormanage": [],