Add support for microversion 2.55 - flavor description

This adds the support for microversion 2.55 which allows creating
a flavor with a description, showing the description in flavor details,
and updating the description on an existing flavor. Related python API
bindings are added, and the new "nova flavor-update <description>" CLI
is added.

Implements blueprint flavor-description

Change-Id: I0a09c0a63d2e91ef5aa31a8e43e28f8745faae14
This commit is contained in:
Matt Riedemann 2017-11-16 16:00:06 -05:00
parent c9e7a64ca8
commit 4f78a4217c
7 changed files with 232 additions and 11 deletions

View File

@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1")
# when client supported the max version, and bumped sequentially, otherwise
# the client may break due to server side new version may include some
# backward incompatible change.
API_MAX_VERSION = api_versions.APIVersion("2.54")
API_MAX_VERSION = api_versions.APIVersion("2.55")

View File

@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import datetime
import re
@ -832,9 +833,12 @@ class FakeSessionClient(base_client.SessionClient):
def get_flavors(self, **kw):
status, header, flavors = self.get_flavors_detail(**kw)
included_fields = ['id', 'name']
if self.api_version >= api_versions.APIVersion('2.55'):
included_fields.append('description')
for flavor in flavors['flavors']:
for k in list(flavor):
if k not in ['id', 'name']:
if k not in included_fields:
del flavor[k]
return (200, FAKE_RESPONSE_HEADERS, flavors)
@ -880,6 +884,18 @@ class FakeSessionClient(base_client.SessionClient):
if not v['os-flavor-access:is_public']
]
# Add description in the response for all flavors.
if self.api_version >= api_versions.APIVersion('2.55'):
for flavor in flavors['flavors']:
flavor['description'] = None
# Add a new flavor that is a copy of the first but with a different
# name, flavorid and a description set.
new_flavor = copy.deepcopy(flavors['flavors'][0])
new_flavor['id'] = 'with-description'
new_flavor['name'] = 'with-description'
new_flavor['description'] = 'test description'
flavors['flavors'].append(new_flavor)
return (200, FAKE_RESPONSE_HEADERS, flavors)
def get_flavors_1(self, **kw):
@ -937,6 +953,14 @@ class FakeSessionClient(base_client.SessionClient):
self.get_flavors_detail(is_public='None')[2]['flavors'][2]}
)
def get_flavors_with_description(self, **kw):
return (
200,
{},
{'flavor':
self.get_flavors_detail(is_public='None')[2]['flavors'][-1]}
)
def delete_flavors_flavordelete(self, **kw):
return (202, FAKE_RESPONSE_HEADERS, None)
@ -951,6 +975,14 @@ class FakeSessionClient(base_client.SessionClient):
self.get_flavors_detail(is_public='None')[2]['flavors'][0]}
)
def put_flavors_with_description(self, body, **kw):
assert 'flavor' in body
assert 'description' in body['flavor']
flavor = self.get_flavors_with_description(**kw)[2]
# Fake out the actual update of the flavor description for the response
flavor['description'] = body['flavor']['description']
return (200, {}, {'flavor': flavor})
def get_flavors_1_os_extra_specs(self, **kw):
return (
200,

View File

@ -131,7 +131,8 @@ class FlavorsTest(utils.TestCase):
self.assertRaises(exceptions.NotFound, self.cs.flavors.find,
disk=12345)
def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap,
@staticmethod
def _create_body(name, ram, vcpus, disk, ephemeral, id, swap,
rxtx_factor, is_public):
return {
"flavor": {
@ -258,3 +259,65 @@ class FlavorsTest(utils.TestCase):
mock.call("/flavors/1/os-extra_specs/k1"),
mock.call("/flavors/1/os-extra_specs/k2")
])
class FlavorsTest_v2_55(utils.TestCase):
"""Tests creating/showing/updating a flavor with a description."""
def setUp(self):
super(FlavorsTest_v2_55, self).setUp()
self.cs = fakes.FakeClient(api_versions.APIVersion('2.55'))
def test_list_flavors(self):
fl = self.cs.flavors.list()
self.cs.assert_called('GET', '/flavors/detail')
for flavor in fl:
self.assertTrue(hasattr(flavor, 'description'),
"%s does not have a description set." % flavor)
def test_list_flavors_undetailed(self):
fl = self.cs.flavors.list(detailed=False)
self.cs.assert_called('GET', '/flavors')
for flavor in fl:
self.assertTrue(hasattr(flavor, 'description'),
"%s does not have a description set." % flavor)
def test_get_flavor_details(self):
f = self.cs.flavors.get('with-description')
self.cs.assert_called('GET', '/flavors/with-description')
self.assertEqual('test description', f.description)
def test_create(self):
self.cs.flavors.create(
'with-description', 512, 1, 10, 'with-description', ephemeral=10,
is_public=False, description='test description')
body = FlavorsTest._create_body(
"with-description", 512, 1, 10, 10, 'with-description',
0, 1.0, False)
body['flavor']['description'] = 'test description'
self.cs.assert_called('POST', '/flavors', body)
def test_create_bad_version(self):
"""Tests trying to create a flavor with a description before 2.55."""
self.cs.api_version = api_versions.APIVersion('2.54')
self.assertRaises(exceptions.UnsupportedAttribute,
self.cs.flavors.create,
'with-description', 512, 1, 10, 'with-description',
description='test description')
def test_update(self):
updated_flavor = self.cs.flavors.update(
'with-description', 'new description')
body = {
'flavor': {
'description': 'new description'
}
}
self.cs.assert_called('PUT', '/flavors/with-description', body)
self.assertEqual('new description', updated_flavor.description)
def test_update_bad_version(self):
"""Tests trying to update a flavor with a description before 2.55."""
self.cs.api_version = api_versions.APIVersion('2.54')
self.assertRaises(exceptions.VersionNotFoundForAPIMethod,
self.cs.flavors.update, 'foo', 'bar')

View File

@ -1131,8 +1131,15 @@ class ShellTest(utils.TestCase):
cmd, api_version='2.51')
def test_flavor_list(self):
self.run_command('flavor-list')
out, _ = self.run_command('flavor-list')
self.assert_called_anytime('GET', '/flavors/detail')
self.assertNotIn('Description', out)
def test_flavor_list_with_description(self):
"""Tests that the description column is added for version >= 2.55."""
out, _ = self.run_command('flavor-list', api_version='2.55')
self.assert_called_anytime('GET', '/flavors/detail')
self.assertIn('Description', out)
def test_flavor_list_with_extra_specs(self):
self.run_command('flavor-list --extra-specs')
@ -1160,8 +1167,15 @@ class ShellTest(utils.TestCase):
self.assert_called('GET', '/flavors/detail?sort_dir=asc&sort_key=id')
def test_flavor_show(self):
self.run_command('flavor-show 1')
out, _ = self.run_command('flavor-show 1')
self.assert_called_anytime('GET', '/flavors/1')
self.assertNotIn('description', out)
def test_flavor_show_with_description(self):
"""Tests that the description is shown in version >= 2.55."""
out, _ = self.run_command('flavor-show 1', api_version='2.55')
self.assert_called_anytime('GET', '/flavors/1')
self.assertIn('description', out)
def test_flavor_show_with_alphanum_id(self):
self.run_command('flavor-show aa1')
@ -1910,6 +1924,41 @@ class ShellTest(utils.TestCase):
self.assert_called('POST', '/flavors', pos=-2)
self.assert_called('GET', '/flavors/1', pos=-1)
def test_flavor_create_with_description(self):
"""Tests creating a flavor with a description."""
self.run_command("flavor-create description "
"1234 512 10 1 --description foo", api_version='2.55')
expected_post_body = {
"flavor": {
"name": "description",
"ram": 512,
"vcpus": 1,
"disk": 10,
"id": "1234",
"swap": 0,
"OS-FLV-EXT-DATA:ephemeral": 0,
"rxtx_factor": 1.0,
"os-flavor-access:is_public": True,
"description": "foo"
}
}
self.assert_called('POST', '/flavors', expected_post_body, pos=-2)
def test_flavor_update(self):
"""Tests creating a flavor with a description."""
out, _ = self.run_command(
"flavor-update with-description new-description",
api_version='2.55')
expected_put_body = {
"flavor": {
"description": "new-description"
}
}
self.assert_called('GET', '/flavors/with-description', pos=-2)
self.assert_called('PUT', '/flavors/with-description',
expected_put_body, pos=-1)
self.assertIn('new-description', out)
def test_aggregate_list(self):
out, err = self.run_command('aggregate-list')
self.assert_called('GET', '/os-aggregates')

View File

@ -19,6 +19,7 @@ Flavor interface.
from oslo_utils import strutils
from six.moves.urllib import parse
from novaclient import api_versions
from novaclient import base
from novaclient import exceptions
from novaclient.i18n import _
@ -86,6 +87,16 @@ class Flavor(base.Resource):
"""
return self.manager.delete(self)
@api_versions.wraps('2.55')
def update(self, description=None):
"""
Update the description for this flavor.
:param description: The description to set on the flavor.
:returns: :class:`Flavor`
"""
return self.manager.update(self, description=description)
class FlavorManager(base.ManagerWithFind):
"""Manage :class:`Flavor` resources."""
@ -170,7 +181,8 @@ class FlavorManager(base.ManagerWithFind):
}
def create(self, name, ram, vcpus, disk, flavorid="auto",
ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True):
ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True,
description=None):
"""Create a flavor.
:param name: Descriptive name of the flavor
@ -180,8 +192,14 @@ class FlavorManager(base.ManagerWithFind):
:param flavorid: ID for the flavor (optional). You can use the reserved
value ``"auto"`` to have Nova generate a UUID for the
flavor in cases where you cannot simply pass ``None``.
:param ephemeral: Ephemeral disk space in GB.
:param swap: Swap space in MB
:param rxtx_factor: RX/TX factor
:param is_public: Whether or not the flavor is public.
:param description: A free form description of the flavor.
Limited to 65535 characters in length.
Only printable characters are allowed.
(Available starting with microversion 2.55)
:returns: :class:`Flavor`
"""
@ -219,7 +237,28 @@ class FlavorManager(base.ManagerWithFind):
except Exception:
raise exceptions.CommandError(_("is_public must be a boolean."))
supports_description = api_versions.APIVersion('2.55')
if description and self.api_version < supports_description:
raise exceptions.UnsupportedAttribute('description', '2.55')
body = self._build_body(name, ram, vcpus, disk, flavorid, swap,
ephemeral, rxtx_factor, is_public)
if description:
body['flavor']['description'] = description
return self._create("/flavors", body, "flavor")
@api_versions.wraps('2.55')
def update(self, flavor, description=None):
"""
Update the description of the flavor.
:param flavor: The :class:`Flavor` (or its ID) to update.
:param description: The description to set on the flavor.
"""
body = {
'flavor': {
'description': description
}
}
return self._update('/flavors/%s' % base.getid(flavor), body, 'flavor')

View File

@ -1052,7 +1052,7 @@ def _print_flavor_extra_specs(flavor):
return "N/A"
def _print_flavor_list(flavors, show_extra_specs=False):
def _print_flavor_list(cs, flavors, show_extra_specs=False):
_translate_flavor_keys(flavors)
headers = [
@ -1073,6 +1073,9 @@ def _print_flavor_list(flavors, show_extra_specs=False):
else:
formatters = {}
if cs.api_version >= api_versions.APIVersion('2.55'):
headers.append('Description')
utils.print_list(flavors, headers, formatters)
@ -1138,7 +1141,7 @@ def do_flavor_list(cs, args):
flavors = cs.flavors.list(marker=args.marker, min_disk=args.min_disk,
min_ram=args.min_ram, sort_key=args.sort_key,
sort_dir=args.sort_dir, limit=args.limit)
_print_flavor_list(flavors, args.extra_specs)
_print_flavor_list(cs, flavors, args.extra_specs)
@utils.arg(
@ -1149,7 +1152,7 @@ def do_flavor_delete(cs, args):
"""Delete a specific flavor"""
flavorid = _find_flavor(cs, args.flavor)
cs.flavors.delete(flavorid)
_print_flavor_list([flavorid])
_print_flavor_list(cs, [flavorid])
@utils.arg(
@ -1204,12 +1207,39 @@ def do_flavor_show(cs, args):
help=_("Make flavor accessible to the public (default true)."),
type=lambda v: strutils.bool_from_string(v, True),
default=True)
@utils.arg(
'--description',
metavar='<description>',
help=_('A free form description of the flavor. Limited to 65535 '
'characters in length. Only printable characters are allowed.'),
start_version='2.55')
def do_flavor_create(cs, args):
"""Create a new flavor."""
if cs.api_version >= api_versions.APIVersion('2.55'):
description = args.description
else:
description = None
f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id,
args.ephemeral, args.swap, args.rxtx_factor,
args.is_public)
_print_flavor_list([f])
args.is_public, description)
_print_flavor_list(cs, [f])
@api_versions.wraps('2.55')
@utils.arg(
'flavor',
metavar='<flavor>',
help=_('Name or ID of the flavor to update.'))
@utils.arg(
'description',
metavar='<description>',
help=_('A free form description of the flavor. Limited to 65535 '
'characters in length. Only printable characters are allowed.'))
def do_flavor_update(cs, args):
"""Update the description of an existing flavor."""
flavorid = _find_flavor(cs, args.flavor)
flavor = cs.flavors.update(flavorid, args.description)
_print_flavor_list(cs, [flavor])
@utils.arg(

View File

@ -0,0 +1,8 @@
---
features:
- |
Support is added for compute API version 2.55. This adds the ability
to create a flavor with a description, show the description of a flavor,
and update the description on an existing flavor.
A new ``nova flavor-update <flavor> <description>`` command is added.