diff --git a/novaclient/__init__.py b/novaclient/__init__.py index d84695cec..7fb218c76 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -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") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 4138e8a98..b59f55496 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -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, diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index c882ddb38..f05e7cddb 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -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') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 341f5e51f..7198715ca 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -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') diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index a0a7b6f95..30fa2a8e6 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -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') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 837526c41..c7a366fbe 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -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='', + 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='', + help=_('Name or ID of the flavor to update.')) +@utils.arg( + 'description', + metavar='', + 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( diff --git a/releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml b/releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml new file mode 100644 index 000000000..9c3e6c00d --- /dev/null +++ b/releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml @@ -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 `` command is added.