Adds flavor support for Nova V3 API

The first part of adding flavor support for the Nova V3 API (support
for flavor-access API calls is still to come).

Restructures v1_1 testcases so as much as possible so they can be
reused for the V3 version of the tests. If it looks too ugly an
alternative would be to just cut and paste the fakes and test
from v1_1 to v3, which is simpler to understand but comes at the
cost of duplicated code. The upside though is it would be much
easier to remove v1_1/v2 support in the future.

Partially implements blueprint v3-api

Change-Id: Ic7cb3c43db02c07d37aea2675b310aaa50639c40
This commit is contained in:
Chris Yeoh 2013-12-04 00:34:08 +10:30
parent 02074d5d4d
commit 9329f435bf
6 changed files with 313 additions and 110 deletions

View File

@ -19,186 +19,177 @@ from novaclient.tests.v1_1 import fakes
from novaclient.v1_1 import flavors
cs = fakes.FakeClient()
class FlavorsTest(utils.TestCase):
def setUp(self):
super(FlavorsTest, self).setUp()
self.cs = self._get_fake_client()
self.flavor_type = self._get_flavor_type()
def _get_fake_client(self):
return fakes.FakeClient()
def _get_flavor_type(self):
return flavors.Flavor
def test_list_flavors(self):
fl = cs.flavors.list()
cs.assert_called('GET', '/flavors/detail')
fl = self.cs.flavors.list()
self.cs.assert_called('GET', '/flavors/detail')
for flavor in fl:
self.assertTrue(isinstance(flavor, flavors.Flavor))
self.assertTrue(isinstance(flavor, self.flavor_type))
def test_list_flavors_undetailed(self):
fl = cs.flavors.list(detailed=False)
cs.assert_called('GET', '/flavors')
fl = self.cs.flavors.list(detailed=False)
self.cs.assert_called('GET', '/flavors')
for flavor in fl:
self.assertTrue(isinstance(flavor, flavors.Flavor))
self.assertTrue(isinstance(flavor, self.flavor_type))
def test_list_flavors_is_public_none(self):
fl = cs.flavors.list(is_public=None)
cs.assert_called('GET', '/flavors/detail?is_public=None')
fl = self.cs.flavors.list(is_public=None)
self.cs.assert_called('GET', '/flavors/detail?is_public=None')
for flavor in fl:
self.assertTrue(isinstance(flavor, flavors.Flavor))
self.assertTrue(isinstance(flavor, self.flavor_type))
def test_list_flavors_is_public_false(self):
fl = cs.flavors.list(is_public=False)
cs.assert_called('GET', '/flavors/detail?is_public=False')
fl = self.cs.flavors.list(is_public=False)
self.cs.assert_called('GET', '/flavors/detail?is_public=False')
for flavor in fl:
self.assertTrue(isinstance(flavor, flavors.Flavor))
self.assertTrue(isinstance(flavor, self.flavor_type))
def test_list_flavors_is_public_true(self):
fl = cs.flavors.list(is_public=True)
cs.assert_called('GET', '/flavors/detail')
fl = self.cs.flavors.list(is_public=True)
self.cs.assert_called('GET', '/flavors/detail')
for flavor in fl:
self.assertTrue(isinstance(flavor, flavors.Flavor))
self.assertTrue(isinstance(flavor, self.flavor_type))
def test_get_flavor_details(self):
f = cs.flavors.get(1)
cs.assert_called('GET', '/flavors/1')
self.assertTrue(isinstance(f, flavors.Flavor))
f = self.cs.flavors.get(1)
self.cs.assert_called('GET', '/flavors/1')
self.assertTrue(isinstance(f, self.flavor_type))
self.assertEqual(f.ram, 256)
self.assertEqual(f.disk, 10)
self.assertEqual(f.ephemeral, 10)
self.assertEqual(f.is_public, True)
def test_get_flavor_details_alphanum_id(self):
f = cs.flavors.get('aa1')
cs.assert_called('GET', '/flavors/aa1')
self.assertTrue(isinstance(f, flavors.Flavor))
f = self.cs.flavors.get('aa1')
self.cs.assert_called('GET', '/flavors/aa1')
self.assertTrue(isinstance(f, self.flavor_type))
self.assertEqual(f.ram, 128)
self.assertEqual(f.disk, 0)
self.assertEqual(f.ephemeral, 0)
self.assertEqual(f.is_public, True)
def test_get_flavor_details_diablo(self):
f = cs.flavors.get(3)
cs.assert_called('GET', '/flavors/3')
self.assertTrue(isinstance(f, flavors.Flavor))
f = self.cs.flavors.get(3)
self.cs.assert_called('GET', '/flavors/3')
self.assertTrue(isinstance(f, self.flavor_type))
self.assertEqual(f.ram, 256)
self.assertEqual(f.disk, 10)
self.assertEqual(f.ephemeral, 'N/A')
self.assertEqual(f.is_public, 'N/A')
def test_find(self):
f = cs.flavors.find(ram=256)
cs.assert_called('GET', '/flavors/detail')
f = self.cs.flavors.find(ram=256)
self.cs.assert_called('GET', '/flavors/detail')
self.assertEqual(f.name, '256 MB Server')
f = cs.flavors.find(disk=0)
f = self.cs.flavors.find(disk=0)
self.assertEqual(f.name, '128 MB Server')
self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345)
self.assertRaises(exceptions.NotFound, self.cs.flavors.find,
disk=12345)
def test_create(self):
f = cs.flavors.create("flavorcreate", 512, 1, 10, 1234, ephemeral=10,
is_public=False)
body = {
def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap,
rxtx_factor, is_public):
return {
"flavor": {
"name": "flavorcreate",
"ram": 512,
"vcpus": 1,
"disk": 10,
"OS-FLV-EXT-DATA:ephemeral": 10,
"id": 1234,
"swap": 0,
"rxtx_factor": 1.0,
"os-flavor-access:is_public": False,
"name": name,
"ram": ram,
"vcpus": vcpus,
"disk": disk,
"OS-FLV-EXT-DATA:ephemeral": ephemeral,
"id": id,
"swap": swap,
"rxtx_factor": rxtx_factor,
"os-flavor-access:is_public": is_public,
}
}
cs.assert_called('POST', '/flavors', body)
self.assertTrue(isinstance(f, flavors.Flavor))
def test_create(self):
f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234,
ephemeral=10, is_public=False)
body = self._create_body("flavorcreate", 512, 1, 10, 10, 1234, 0, 1.0,
False)
self.cs.assert_called('POST', '/flavors', body)
self.assertTrue(isinstance(f, self.flavor_type))
def test_create_with_id_as_string(self):
flavor_id = 'foobar'
f = cs.flavors.create("flavorcreate", 512,
f = self.cs.flavors.create("flavorcreate", 512,
1, 10, flavor_id, ephemeral=10,
is_public=False)
body = {
"flavor": {
"name": "flavorcreate",
"ram": 512,
"vcpus": 1,
"disk": 10,
"OS-FLV-EXT-DATA:ephemeral": 10,
"id": flavor_id,
"swap": 0,
"rxtx_factor": 1.0,
"os-flavor-access:is_public": False,
}
}
body = self._create_body("flavorcreate", 512, 1, 10, 10, flavor_id, 0,
1.0, False)
cs.assert_called('POST', '/flavors', body)
self.assertTrue(isinstance(f, flavors.Flavor))
self.cs.assert_called('POST', '/flavors', body)
self.assertTrue(isinstance(f, self.flavor_type))
def test_create_ephemeral_ispublic_defaults(self):
f = cs.flavors.create("flavorcreate", 512, 1, 10, 1234)
f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234)
body = {
"flavor": {
"name": "flavorcreate",
"ram": 512,
"vcpus": 1,
"disk": 10,
"OS-FLV-EXT-DATA:ephemeral": 0,
"id": 1234,
"swap": 0,
"rxtx_factor": 1.0,
"os-flavor-access:is_public": True,
}
}
body = self._create_body("flavorcreate", 512, 1, 10, 0, 1234, 0,
1.0, True)
cs.assert_called('POST', '/flavors', body)
self.assertTrue(isinstance(f, flavors.Flavor))
self.cs.assert_called('POST', '/flavors', body)
self.assertTrue(isinstance(f, self.flavor_type))
def test_invalid_parameters_create(self):
self.assertRaises(exceptions.CommandError, cs.flavors.create,
self.assertRaises(exceptions.CommandError, self.cs.flavors.create,
"flavorcreate", "invalid", 1, 10, 1234, swap=0,
ephemeral=0, rxtx_factor=1.0, is_public=True)
self.assertRaises(exceptions.CommandError, cs.flavors.create,
self.assertRaises(exceptions.CommandError, self.cs.flavors.create,
"flavorcreate", 512, "invalid", 10, 1234, swap=0,
ephemeral=0, rxtx_factor=1.0, is_public=True)
self.assertRaises(exceptions.CommandError, cs.flavors.create,
self.assertRaises(exceptions.CommandError, self.cs.flavors.create,
"flavorcreate", 512, 1, "invalid", 1234, swap=0,
ephemeral=0, rxtx_factor=1.0, is_public=True)
self.assertRaises(exceptions.CommandError, cs.flavors.create,
self.assertRaises(exceptions.CommandError, self.cs.flavors.create,
"flavorcreate", 512, 1, 10, 1234, swap="invalid",
ephemeral=0, rxtx_factor=1.0, is_public=True)
self.assertRaises(exceptions.CommandError, cs.flavors.create,
self.assertRaises(exceptions.CommandError, self.cs.flavors.create,
"flavorcreate", 512, 1, 10, 1234, swap=0,
ephemeral="invalid", rxtx_factor=1.0, is_public=True)
self.assertRaises(exceptions.CommandError, cs.flavors.create,
self.assertRaises(exceptions.CommandError, self.cs.flavors.create,
"flavorcreate", 512, 1, 10, 1234, swap=0,
ephemeral=0, rxtx_factor="invalid", is_public=True)
self.assertRaises(exceptions.CommandError, cs.flavors.create,
self.assertRaises(exceptions.CommandError, self.cs.flavors.create,
"flavorcreate", 512, 1, 10, 1234, swap=0,
ephemeral=0, rxtx_factor=1.0, is_public='invalid')
def test_delete(self):
cs.flavors.delete("flavordelete")
cs.assert_called('DELETE', '/flavors/flavordelete')
self.cs.flavors.delete("flavordelete")
self.cs.assert_called('DELETE', '/flavors/flavordelete')
def test_delete_with_flavor_instance(self):
f = cs.flavors.get(2)
cs.flavors.delete(f)
cs.assert_called('DELETE', '/flavors/2')
f = self.cs.flavors.get(2)
self.cs.flavors.delete(f)
self.cs.assert_called('DELETE', '/flavors/2')
def test_delete_with_flavor_instance_method(self):
f = cs.flavors.get(2)
f = self.cs.flavors.get(2)
f.delete()
cs.assert_called('DELETE', '/flavors/2')
self.cs.assert_called('DELETE', '/flavors/2')
def test_set_keys(self):
f = cs.flavors.get(1)
f = self.cs.flavors.get(1)
f.set_keys({'k1': 'v1'})
cs.assert_called('POST', '/flavors/1/os-extra_specs',
self.cs.assert_called('POST', '/flavors/1/os-extra_specs',
{"extra_specs": {'k1': 'v1'}})
def test_unset_keys(self):
f = cs.flavors.get(1)
f = self.cs.flavors.get(1)
f.unset_keys(['k1'])
cs.assert_called('DELETE', '/flavors/1/os-extra_specs/k1')
self.cs.assert_called('DELETE', '/flavors/1/os-extra_specs/k1')

View File

@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from novaclient.openstack.common import strutils
from novaclient.tests import fakes
from novaclient.tests.v1_1 import fakes as fakes_v1_1
from novaclient.v3 import client
@ -56,3 +57,51 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient):
def get_os_hosts_sample_host_shutdown(self, **kw):
return (200, {}, {'host': {'host': 'sample_host',
'power_action': 'shutdown'}})
#
# Flavors
#
post_flavors_1_flavor_extra_specs = (
fakes_v1_1.FakeHTTPClient.post_flavors_1_os_extra_specs)
delete_flavors_1_flavor_extra_specs_k1 = (
fakes_v1_1.FakeHTTPClient.delete_flavors_1_os_extra_specs_k1)
def get_flavors_detail(self, **kw):
flavors = {'flavors': [
{'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10,
'ephemeral': 10,
'flavor-access:is_public': True,
'links': {}},
{'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20,
'ephemeral': 20,
'flavor-access:is_public': False,
'links': {}},
{'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0,
'ephemeral': 0,
'flavor-access:is_public': True,
'links': {}}
]}
if 'is_public' not in kw:
filter_is_public = True
else:
if kw['is_public'].lower() == 'none':
filter_is_public = None
else:
filter_is_public = strutils.bool_from_string(kw['is_public'],
True)
if filter_is_public is not None:
if filter_is_public:
flavors['flavors'] = [
v for v in flavors['flavors']
if v['flavor-access:is_public']
]
else:
flavors['flavors'] = [
v for v in flavors['flavors']
if not v['flavor-access:is_public']
]
return (200, {}, flavors)

View File

@ -0,0 +1,57 @@
# Copyright (c) 2013, OpenStack
# Copyright 2013 IBM Corp.
#
# 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 novaclient.tests.v1_1 import test_flavors
from novaclient.tests.v3 import fakes
from novaclient.v3 import flavors
class FlavorsTest(test_flavors.FlavorsTest):
def _get_fake_client(self):
return fakes.FakeClient()
def _get_flavor_type(self):
return flavors.Flavor
def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap,
rxtx_factor, is_public):
return {
"flavor": {
"name": name,
"ram": ram,
"vcpus": vcpus,
"disk": disk,
"ephemeral": ephemeral,
"id": id,
"swap": swap,
"rxtx_factor": rxtx_factor,
"flavor-access:is_public": is_public,
}
}
def test_set_keys(self):
f = self.cs.flavors.get(1)
f.set_keys({'k1': 'v1'})
self.cs.assert_called('POST', '/flavors/1/flavor-extra-specs',
{"extra_specs": {'k1': 'v1'}})
def test_unset_keys(self):
f = self.cs.flavors.get(1)
f.unset_keys(['k1'])
self.cs.assert_called('DELETE', '/flavors/1/flavor-extra-specs/k1')
def test_get_flavor_details_diablo(self):
# Don't need for V3 API to work against diablo
pass

View File

@ -132,6 +132,22 @@ class FlavorManager(base.ManagerWithFind):
"""
self._delete("/flavors/%s" % base.getid(flavor))
def _build_body(self, name, ram, vcpus, disk, id, swap,
ephemeral, rxtx_factor, is_public):
return {
"flavor": {
"name": name,
"ram": ram,
"vcpus": vcpus,
"disk": disk,
"id": id,
"swap": swap,
"OS-FLV-EXT-DATA:ephemeral": ephemeral,
"rxtx_factor": rxtx_factor,
"os-flavor-access:is_public": is_public,
}
}
def create(self, name, ram, vcpus, disk, flavorid="auto",
ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True):
"""
@ -183,18 +199,7 @@ class FlavorManager(base.ManagerWithFind):
except Exception:
raise exceptions.CommandError("is_public must be a boolean.")
body = {
"flavor": {
"name": name,
"ram": ram,
"vcpus": vcpus,
"disk": disk,
"id": flavorid,
"swap": swap,
"OS-FLV-EXT-DATA:ephemeral": ephemeral,
"rxtx_factor": rxtx_factor,
"os-flavor-access:is_public": is_public,
}
}
body = self._build_body(name, ram, vcpus, disk, flavorid, swap,
ephemeral, rxtx_factor, is_public)
return self._create("/flavors", body, "flavor")

View File

@ -15,6 +15,7 @@
# under the License.
from novaclient import client
from novaclient.v3 import flavors
from novaclient.v3 import hosts
@ -51,6 +52,7 @@ class Client(object):
self.os_cache = os_cache or not no_cache
#TODO(bnemec): Add back in v3 extensions
self.hosts = hosts.HostManager(self)
self.flavors = flavors.FlavorManager(self)
# Add in any extensions...
if extensions:

99
novaclient/v3/flavors.py Normal file
View File

@ -0,0 +1,99 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2013 IBM Corp.
#
# 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.
"""
Flavor interface.
"""
from novaclient import base
from novaclient.v1_1 import flavors
class Flavor(base.Resource):
"""
A flavor is an available hardware configuration for a server.
"""
HUMAN_ID = True
def __repr__(self):
return "<Flavor: %s>" % self.name
@property
def is_public(self):
"""
Provide a user-friendly accessor to flavor-access:is_public
"""
return self._info.get("flavor-access:is_public", 'N/A')
def get_keys(self):
"""
Get extra specs from a flavor.
:param flavor: The :class:`Flavor` to get extra specs from
"""
_resp, body = self.manager.api.client.get(
"/flavors/%s/flavor-extra-specs" %
base.getid(self))
return body["extra_specs"]
def set_keys(self, metadata):
"""
Set extra specs on a flavor.
:param flavor: The :class:`Flavor` to set extra spec on
:param metadata: A dict of key/value pairs to be set
"""
body = {'extra_specs': metadata}
return self.manager._create(
"/flavors/%s/flavor-extra-specs" %
base.getid(self), body, "extra_specs",
return_raw=True)
def unset_keys(self, keys):
"""
Unset extra specs on a flavor.
:param flavor: The :class:`Flavor` to unset extra spec on
:param keys: A list of keys to be unset
"""
for k in keys:
return self.manager._delete(
"/flavors/%s/flavor-extra-specs/%s" % (
base.getid(self), k))
def delete(self):
"""
Delete this flavor.
"""
self.manager.delete(self)
class FlavorManager(flavors.FlavorManager):
resource_class = Flavor
def _build_body(self, name, ram, vcpus, disk, id, swap,
ephemeral, rxtx_factor, is_public):
return {
"flavor": {
"name": name,
"ram": ram,
"vcpus": vcpus,
"disk": disk,
"id": id,
"swap": swap,
"ephemeral": ephemeral,
"rxtx_factor": rxtx_factor,
"flavor-access:is_public": is_public,
}
}