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:
parent
c9e7a64ca8
commit
4f78a4217c
@ -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")
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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(
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user