summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--nova/api/openstack/compute/plugins/v3/flavors.py167
-rw-r--r--nova/tests/api/openstack/compute/plugins/v3/test_flavors.py792
-rw-r--r--setup.cfg1
3 files changed, 960 insertions, 0 deletions
diff --git a/nova/api/openstack/compute/plugins/v3/flavors.py b/nova/api/openstack/compute/plugins/v3/flavors.py
new file mode 100644
index 000000000..733ff3750
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/flavors.py
@@ -0,0 +1,167 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack Foundation
+# 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.
+
+import webob
+
+from nova.api.openstack import common
+from nova.api.openstack.compute.views import flavors as flavors_view
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.compute import flavors
+from nova import exception
+from nova.openstack.common import strutils
+
+
+def make_flavor(elem, detailed=False):
+ elem.set('name')
+ elem.set('id')
+ if detailed:
+ elem.set('ram')
+ elem.set('disk')
+ elem.set('vcpus', xmlutil.EmptyStringSelector('vcpus'))
+
+ xmlutil.make_links(elem, 'links')
+
+
+flavor_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
+
+
+class FlavorTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor', selector='flavor')
+ make_flavor(root, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap)
+
+
+class MinimalFlavorsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem)
+ return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap)
+
+
+class FlavorsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap)
+
+
+class FlavorsController(wsgi.Controller):
+ """Flavor controller for the OpenStack API."""
+
+ _view_builder_class = flavors_view.ViewBuilder
+
+ @wsgi.serializers(xml=MinimalFlavorsTemplate)
+ def index(self, req):
+ """Return all flavors in brief."""
+ limited_flavors = self._get_flavors(req)
+ return self._view_builder.index(req, limited_flavors)
+
+ @wsgi.serializers(xml=FlavorsTemplate)
+ def detail(self, req):
+ """Return all flavors in detail."""
+ limited_flavors = self._get_flavors(req)
+ req.cache_db_flavors(limited_flavors)
+ return self._view_builder.detail(req, limited_flavors)
+
+ @wsgi.serializers(xml=FlavorTemplate)
+ def show(self, req, id):
+ """Return data about the given flavor id."""
+ try:
+ flavor = flavors.get_flavor_by_flavor_id(id)
+ req.cache_db_flavor(flavor)
+ except exception.NotFound:
+ raise webob.exc.HTTPNotFound()
+
+ return self._view_builder.show(req, flavor)
+
+ def _parse_is_public(self, is_public):
+ """Parse is_public into something usable."""
+
+ if is_public is None:
+ # preserve default value of showing only public flavors
+ return True
+ elif is_public == 'none':
+ return None
+ else:
+ try:
+ return strutils.bool_from_string(is_public, strict=True)
+ except ValueError:
+ msg = _('Invalid is_public filter [%s]') % is_public
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ def _get_flavors(self, req):
+ """Helper function that returns a list of flavor dicts."""
+ filters = {}
+
+ context = req.environ['nova.context']
+ if context.is_admin:
+ # Only admin has query access to all flavor types
+ filters['is_public'] = self._parse_is_public(
+ req.params.get('is_public', None))
+ else:
+ filters['is_public'] = True
+ filters['disabled'] = False
+
+ if 'minRam' in req.params:
+ try:
+ filters['min_memory_mb'] = int(req.params['minRam'])
+ except ValueError:
+ msg = _('Invalid minRam filter [%s]') % req.params['minRam']
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ if 'minDisk' in req.params:
+ try:
+ filters['min_root_gb'] = int(req.params['minDisk'])
+ except ValueError:
+ msg = _('Invalid minDisk filter [%s]') % req.params['minDisk']
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ limited_flavors = flavors.get_all_flavors(context, filters=filters)
+ flavors_list = limited_flavors.values()
+ sorted_flavors = sorted(flavors_list,
+ key=lambda item: item['flavorid'])
+ limited_flavors = common.limited_by_marker(sorted_flavors, req)
+ return limited_flavors
+
+
+class Flavors(extensions.V3APIExtensionBase):
+ """ Flavors Extension. """
+ name = "flavors"
+ alias = "flavors"
+ namespace = "http://docs.openstack.org/compute/core/flavors/v3"
+ version = 1
+
+ def get_resources(self):
+ collection_actions = {'detail': 'GET'}
+ member_actions = {'action': 'POST'}
+
+ resources = [
+ extensions.ResourceExtension('flavors',
+ FlavorsController(),
+ member_name='flavor',
+ collection_actions=collection_actions,
+ member_actions=member_actions)
+ ]
+ return resources
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_flavors.py b/nova/tests/api/openstack/compute/plugins/v3/test_flavors.py
new file mode 100644
index 000000000..6680460de
--- /dev/null
+++ b/nova/tests/api/openstack/compute/plugins/v3/test_flavors.py
@@ -0,0 +1,792 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# 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
+
+import urlparse
+
+from nova.api.openstack.compute.plugins.v3 import flavors
+from nova.api.openstack import xmlutil
+import nova.compute.flavors
+from nova import context
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.api.openstack import fakes
+from nova.tests import matchers
+
+NS = "{http://docs.openstack.org/compute/api/v1.1}"
+ATOMNS = "{http://www.w3.org/2005/Atom}"
+
+
+FAKE_FLAVORS = {
+ 'flavor 1': {
+ "flavorid": '1',
+ "name": 'flavor 1',
+ "memory_mb": '256',
+ "root_gb": '10',
+ },
+ 'flavor 2': {
+ "flavorid": '2',
+ "name": 'flavor 2',
+ "memory_mb": '512',
+ "root_gb": '20',
+ },
+}
+
+
+def fake_flavor_get_by_flavor_id(flavorid):
+ return FAKE_FLAVORS['flavor %s' % flavorid]
+
+
+def fake_flavor_get_all(inactive=False, filters=None):
+ def reject_min(db_attr, filter_attr):
+ return (filter_attr in filters and
+ int(flavor[db_attr]) < int(filters[filter_attr]))
+
+ filters = filters or {}
+ output = {}
+ for (flavor_name, flavor) in FAKE_FLAVORS.items():
+ if reject_min('memory_mb', 'min_memory_mb'):
+ continue
+ elif reject_min('root_gb', 'min_root_gb'):
+ continue
+
+ output[flavor_name] = flavor
+
+ return output
+
+
+def empty_flavor_get_all(inactive=False, filters=None):
+ return {}
+
+
+def return_flavor_not_found(flavor_id):
+ raise exception.InstanceTypeNotFound(instance_type_id=flavor_id)
+
+
+class FlavorsTest(test.TestCase):
+ def setUp(self):
+ super(FlavorsTest, self).setUp()
+ self.flags(osapi_compute_extension=[])
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+ self.stubs.Set(nova.compute.flavors, "get_all_flavors",
+ fake_flavor_get_all)
+ self.stubs.Set(nova.compute.flavors,
+ "get_flavor_by_flavor_id",
+ fake_flavor_get_by_flavor_id)
+
+ self.controller = flavors.FlavorsController()
+
+ def test_get_flavor_by_invalid_id(self):
+ self.stubs.Set(nova.compute.flavors,
+ "get_flavor_by_flavor_id",
+ return_flavor_not_found)
+ req = fakes.HTTPRequestV3.blank('/flavors/asdf')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, 'asdf')
+
+ def test_get_flavor_by_id(self):
+ req = fakes.HTTPRequestV3.blank('/flavors/1')
+ flavor = self.controller.show(req, '1')
+ expected = {
+ "flavor": {
+ "id": "1",
+ "name": "flavor 1",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/1",
+ },
+ ],
+ },
+ }
+ self.assertEqual(flavor, expected)
+
+ def test_get_flavor_with_custom_link_prefix(self):
+ self.flags(osapi_compute_link_prefix='http://zoo.com:42',
+ osapi_glance_link_prefix='http://circus.com:34')
+ req = fakes.HTTPRequestV3.blank('/flavors/1')
+ flavor = self.controller.show(req, '1')
+ expected = {
+ "flavor": {
+ "id": "1",
+ "name": "flavor 1",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://zoo.com:42/v3/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://zoo.com:42/flavors/1",
+ },
+ ],
+ },
+ }
+ self.assertEqual(flavor, expected)
+
+ def test_get_flavor_list(self):
+ req = fakes.HTTPRequestV3.blank('/flavors')
+ flavor = self.controller.index(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/1",
+ },
+ ],
+ },
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self.assertEqual(flavor, expected)
+
+ def test_get_flavor_list_with_marker(self):
+ self.maxDiff = None
+ req = fakes.HTTPRequestV3.blank('/flavors?limit=1&marker=1')
+ flavor = self.controller.index(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/2",
+ },
+ ],
+ },
+ ],
+ 'flavors_links': [
+ {'href': 'http://localhost/v3/flavors?limit=1&marker=2',
+ 'rel': 'next'}
+ ]
+ }
+ self.assertThat(flavor, matchers.DictMatches(expected))
+
+ def test_get_flavor_detail_with_limit(self):
+ req = fakes.HTTPRequestV3.blank('/flavors/detail?limit=1')
+ response = self.controller.index(req)
+ response_list = response["flavors"]
+ response_links = response["flavors_links"]
+
+ expected_flavors = [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/1",
+ },
+ ],
+ },
+ ]
+ self.assertEqual(response_list, expected_flavors)
+ self.assertEqual(response_links[0]['rel'], 'next')
+
+ href_parts = urlparse.urlparse(response_links[0]['href'])
+ self.assertEqual('/v3/flavors', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ self.assertThat({'limit': ['1'], 'marker': ['1']},
+ matchers.DictMatches(params))
+
+ def test_get_flavor_with_limit(self):
+ req = fakes.HTTPRequestV3.blank('/flavors?limit=2')
+ response = self.controller.index(req)
+ response_list = response["flavors"]
+ response_links = response["flavors_links"]
+
+ expected_flavors = [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/1",
+ },
+ ],
+ },
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/2",
+ },
+ ],
+ }
+ ]
+ self.assertEqual(response_list, expected_flavors)
+ self.assertEqual(response_links[0]['rel'], 'next')
+
+ href_parts = urlparse.urlparse(response_links[0]['href'])
+ self.assertEqual('/v3/flavors', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ self.assertThat({'limit': ['2'], 'marker': ['2']},
+ matchers.DictMatches(params))
+
+ def test_get_flavor_list_detail(self):
+ req = fakes.HTTPRequestV3.blank('/flavors/detail')
+ flavor = self.controller.detail(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/1",
+ },
+ ],
+ },
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "ram": "512",
+ "disk": "20",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self.assertEqual(flavor, expected)
+
+ def test_get_empty_flavor_list(self):
+ self.stubs.Set(nova.compute.flavors, "get_all_flavors",
+ empty_flavor_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/flavors')
+ flavors = self.controller.index(req)
+ expected = {'flavors': []}
+ self.assertEqual(flavors, expected)
+
+ def test_get_flavor_list_filter_min_ram(self):
+ # Flavor lists may be filtered by minRam.
+ req = fakes.HTTPRequestV3.blank('/flavors?minRam=512')
+ flavor = self.controller.index(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self.assertEqual(flavor, expected)
+
+ def test_get_flavor_list_filter_invalid_min_ram(self):
+ # Ensure you cannot list flavors with invalid minRam param.
+ req = fakes.HTTPRequestV3.blank('/flavors?minRam=NaN')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_flavor_list_filter_min_disk(self):
+ # Flavor lists may be filtered by minDisk.
+ req = fakes.HTTPRequestV3.blank('/flavors?minDisk=20')
+ flavor = self.controller.index(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self.assertEqual(flavor, expected)
+
+ def test_get_flavor_list_filter_invalid_min_disk(self):
+ # Ensure you cannot list flavors with invalid minDisk param.
+ req = fakes.HTTPRequestV3.blank('/flavors?minDisk=NaN')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_flavor_list_detail_min_ram_and_min_disk(self):
+ """Tests that filtering work on flavor details and that minRam and
+ minDisk filters can be combined
+ """
+ req = fakes.HTTPRequestV3.blank('/flavors/detail'
+ '?minRam=256&minDisk=20')
+ flavor = self.controller.detail(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "ram": "512",
+ "disk": "20",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self.assertEqual(flavor, expected)
+
+
+class FlavorsXMLSerializationTest(test.TestCase):
+
+ def test_xml_declaration(self):
+ serializer = flavors.FlavorTemplate()
+
+ fixture = {
+ "flavor": {
+ "id": "12",
+ "name": "asdf",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/12",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/12",
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
+ self.assertTrue(has_dec)
+
+ def test_show(self):
+ serializer = flavors.FlavorTemplate()
+
+ fixture = {
+ "flavor": {
+ "id": "12",
+ "name": "asdf",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/12",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/12",
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavor')
+ flavor_dict = fixture['flavor']
+
+ for key in ['name', 'id', 'ram', 'disk']:
+ self.assertEqual(root.get(key), str(flavor_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(flavor_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_show_handles_integers(self):
+ serializer = flavors.FlavorTemplate()
+
+ fixture = {
+ "flavor": {
+ "id": 12,
+ "name": "asdf",
+ "ram": 256,
+ "disk": 10,
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/12",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/12",
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavor')
+ flavor_dict = fixture['flavor']
+
+ for key in ['name', 'id', 'ram', 'disk']:
+ self.assertEqual(root.get(key), str(flavor_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(flavor_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_detail(self):
+ serializer = flavors.FlavorsTemplate()
+
+ fixture = {
+ "flavors": [
+ {
+ "id": "23",
+ "name": "flavor 23",
+ "ram": "512",
+ "disk": "20",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/23",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/23",
+ },
+ ],
+ },
+ {
+ "id": "13",
+ "name": "flavor 13",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/13",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/13",
+ },
+ ],
+ },
+ ],
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavors')
+ flavor_elems = root.findall('{0}flavor'.format(NS))
+ self.assertEqual(len(flavor_elems), 2)
+ for i, flavor_elem in enumerate(flavor_elems):
+ flavor_dict = fixture['flavors'][i]
+
+ for key in ['name', 'id', 'ram', 'disk']:
+ self.assertEqual(flavor_elem.get(key), str(flavor_dict[key]))
+
+ link_nodes = flavor_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(flavor_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_index(self):
+ serializer = flavors.MinimalFlavorsTemplate()
+
+ fixture = {
+ "flavors": [
+ {
+ "id": "23",
+ "name": "flavor 23",
+ "ram": "512",
+ "disk": "20",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/23",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/23",
+ },
+ ],
+ },
+ {
+ "id": "13",
+ "name": "flavor 13",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/flavors/13",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/flavors/13",
+ },
+ ],
+ },
+ ],
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavors_index')
+ flavor_elems = root.findall('{0}flavor'.format(NS))
+ self.assertEqual(len(flavor_elems), 2)
+ for i, flavor_elem in enumerate(flavor_elems):
+ flavor_dict = fixture['flavors'][i]
+
+ for key in ['name', 'id']:
+ self.assertEqual(flavor_elem.get(key), str(flavor_dict[key]))
+
+ link_nodes = flavor_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(flavor_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_index_empty(self):
+ serializer = flavors.MinimalFlavorsTemplate()
+
+ fixture = {
+ "flavors": [],
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavors_index')
+ flavor_elems = root.findall('{0}flavor'.format(NS))
+ self.assertEqual(len(flavor_elems), 0)
+
+
+class DisabledFlavorsWithRealDBTest(test.TestCase):
+ """
+ Tests that disabled flavors should not be shown nor listed.
+ """
+ def setUp(self):
+ super(DisabledFlavorsWithRealDBTest, self).setUp()
+ self.controller = flavors.FlavorsController()
+
+ # Add a new disabled type to the list of flavors
+ self.req = fakes.HTTPRequestV3.blank('/flavors')
+ self.context = self.req.environ['nova.context']
+ self.admin_context = context.get_admin_context()
+
+ self.disabled_type = self._create_disabled_instance_type()
+ self.inst_types = db.api.instance_type_get_all(self.admin_context)
+
+ def tearDown(self):
+ db.api.instance_type_destroy(self.admin_context,
+ self.disabled_type['name'])
+
+ super(DisabledFlavorsWithRealDBTest, self).tearDown()
+
+ def _create_disabled_instance_type(self):
+ inst_types = db.api.instance_type_get_all(self.admin_context)
+
+ inst_type = inst_types[0]
+
+ del inst_type['id']
+ inst_type['name'] += '.disabled'
+ inst_type['flavorid'] = unicode(max(
+ [int(flavor['flavorid']) for flavor in inst_types]) + 1)
+ inst_type['disabled'] = True
+
+ disabled_type = db.api.instance_type_create(self.admin_context,
+ inst_type)
+
+ return disabled_type
+
+ def test_index_should_not_list_disabled_flavors_to_user(self):
+ self.context.is_admin = False
+
+ flavor_list = self.controller.index(self.req)['flavors']
+ api_flavorids = set(f['id'] for f in flavor_list)
+
+ db_flavorids = set(i['flavorid'] for i in self.inst_types)
+ disabled_flavorid = str(self.disabled_type['flavorid'])
+
+ self.assert_(disabled_flavorid in db_flavorids)
+ self.assertEqual(db_flavorids - set([disabled_flavorid]),
+ api_flavorids)
+
+ def test_index_should_list_disabled_flavors_to_admin(self):
+ self.context.is_admin = True
+
+ flavor_list = self.controller.index(self.req)['flavors']
+ api_flavorids = set(f['id'] for f in flavor_list)
+
+ db_flavorids = set(i['flavorid'] for i in self.inst_types)
+ disabled_flavorid = str(self.disabled_type['flavorid'])
+
+ self.assert_(disabled_flavorid in db_flavorids)
+ self.assertEqual(db_flavorids, api_flavorids)
+
+ def test_show_should_include_disabled_flavor_for_user(self):
+ """
+ Counterintuitively we should show disabled flavors to all users and not
+ just admins. The reason is that, when a user performs a server-show
+ request, we want to be able to display the pretty flavor name ('512 MB
+ Instance') and not just the flavor-id even if the flavor id has been
+ marked disabled.
+ """
+ self.context.is_admin = False
+
+ flavor = self.controller.show(
+ self.req, self.disabled_type['flavorid'])['flavor']
+
+ self.assertEqual(flavor['name'], self.disabled_type['name'])
+
+ def test_show_should_include_disabled_flavor_for_admin(self):
+ self.context.is_admin = True
+
+ flavor = self.controller.show(
+ self.req, self.disabled_type['flavorid'])['flavor']
+
+ self.assertEqual(flavor['name'], self.disabled_type['name'])
+
+
+class ParseIsPublicTest(test.TestCase):
+ def setUp(self):
+ super(ParseIsPublicTest, self).setUp()
+ self.controller = flavors.FlavorsController()
+
+ def assertPublic(self, expected, is_public):
+ self.assertIs(expected, self.controller._parse_is_public(is_public),
+ '%s did not return %s' % (is_public, expected))
+
+ def test_None(self):
+ self.assertPublic(True, None)
+
+ def test_truthy(self):
+ self.assertPublic(True, True)
+ self.assertPublic(True, 't')
+ self.assertPublic(True, 'true')
+ self.assertPublic(True, 'yes')
+ self.assertPublic(True, '1')
+
+ def test_falsey(self):
+ self.assertPublic(False, False)
+ self.assertPublic(False, 'f')
+ self.assertPublic(False, 'false')
+ self.assertPublic(False, 'no')
+ self.assertPublic(False, '0')
+
+ def test_string_none(self):
+ self.assertPublic(None, 'none')
+
+ def test_other(self):
+ self.assertRaises(
+ webob.exc.HTTPBadRequest, self.assertPublic, None, 'other')
diff --git a/setup.cfg b/setup.cfg
index 1baa22940..156ed3acb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -57,6 +57,7 @@ nova.api.v3.extensions =
consoles = nova.api.openstack.compute.plugins.v3.consoles:Consoles
extension_info = nova.api.openstack.compute.plugins.v3.extension_info:ExtensionInfo
fixed_ips = nova.api.openstack.compute.plugins.v3.fixed_ips:FixedIPs
+ flavors = nova.api.openstack.compute.plugins.v3.flavors:Flavors
ips = nova.api.openstack.compute.plugins.v3.ips:IPs
keypairs = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs
servers = nova.api.openstack.compute.plugins.v3.servers:Servers