From 3dd0393fbba7af225f9cc2262ba0ebdeea214456 Mon Sep 17 00:00:00 2001 From: "Yunhong, Jiang" Date: Tue, 11 Sep 2012 19:58:32 +0800 Subject: [PATCH] Support flavor extra specs in nova client Add flavor extra specs so that user can list/set/unset extra specs in nova client blueprint extra-specs-in-nova-client Change-Id: I6ad7293e29764648c79943c4d05f3a09931af411 Signed-off-by: Yunhong, Jiang --- novaclient/v1_1/flavors.py | 38 +++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 46 +++++++++++++++++++++++++++++++++----- tests/v1_1/fakes.py | 18 +++++++++++++++ tests/v1_1/test_flavors.py | 11 +++++++++ tests/v1_1/test_shell.py | 8 ++++--- 5 files changed, 112 insertions(+), 9 deletions(-) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 523b9b71e..88686a131 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -4,6 +4,7 @@ Flavor interface. """ from novaclient import base +from novaclient import exceptions class Flavor(base.Resource): @@ -29,6 +30,43 @@ class Flavor(base.Resource): """ return self._info.get("os-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/os-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/os-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/os-extra_specs/%s" % ( + base.getid(self), k)) + class FlavorManager(base.ManagerWithFind): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e76a7f197..641d34f79 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -314,8 +314,16 @@ def _translate_flavor_keys(collection): setattr(item, to_key, item._info[from_key]) -def _print_flavor_list(flavors): +def _print_flavor_extra_specs(flavor): + try: + return flavor.get_keys() + except exceptions.NotFound: + return "N/A" + + +def _print_flavor_list(cs, flavors): _translate_flavor_keys(flavors) + formatters = {'extra_specs': _print_flavor_extra_specs} utils.print_list(flavors, [ 'ID', 'Name', @@ -325,7 +333,8 @@ def _print_flavor_list(flavors): 'Swap', 'VCPUs', 'RXTX_Factor', - 'Is_Public']) + 'Is_Public', + 'extra_specs'], formatters) def do_flavor_list(cs, _args): @@ -334,7 +343,7 @@ def do_flavor_list(cs, _args): for flavor in flavors: # int needed for numerical sort flavor.id = int(flavor.id) - _print_flavor_list(flavors) + _print_flavor_list(cs, flavors) @utils.arg('id', @@ -351,7 +360,7 @@ def do_flavor_delete(cs, args): def do_flavor_show(cs, args): """Show details about the given flavor.""" flavor = _find_flavor(cs, args.flavor) - _print_flavor(flavor) + _print_flavor(cs, flavor) @utils.arg('name', @@ -391,7 +400,31 @@ def do_flavor_create(cs, args): 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]) + _print_flavor_list(cs, [f]) + + +@utils.arg('flavor', + metavar='', + help="Name or ID of flavor") +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="Actions: 'set' or 'unset'") +@utils.arg('metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Extra_specs to set/unset (only key is necessary on unset)') +def do_flavor_key(cs, args): + """Set or unset extra_spec for a flavor.""" + flavor = _find_flavor(cs, args.flavor) + keypair = _extract_metadata(args) + + if args.action == 'set': + flavor.set_keys(keypair) + elif args.action == 'unset': + flavor.unset_keys(keypair.keys()) @utils.arg('--flavor', @@ -530,10 +563,11 @@ def _print_image(image): utils.print_dict(info) -def _print_flavor(flavor): +def _print_flavor(cs, flavor): info = flavor._info.copy() # ignore links, we don't need to present those info.pop('links') + info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) utils.print_dict(info) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 2b5dc0242..30af70f3b 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -400,6 +400,24 @@ class FakeHTTPClient(base_client.HTTPClient): def post_flavors(self, body, **kw): return (202, {'flavor': self.get_flavors_detail()[1]['flavors'][0]}) + def get_flavors_1_os_extra_specs(self, **kw): + return (200, + {'extra_specs': {"k1": "v1"}}) + + def get_flavors_2_os_extra_specs(self, **kw): + return (200, + {'extra_specs': {"k2": "v2"}}) + + def post_flavors_1_os_extra_specs(self, body, **kw): + assert body.keys() == ['extra_specs'] + fakes.assert_has_keys(body['extra_specs'], + required=['k1']) + return (200, + {'extra_specs': {"k1": "v1"}}) + + def delete_flavors_1_os_extra_specs_k1(self, **kw): + return (204, None) + # # Flavor access # diff --git a/tests/v1_1/test_flavors.py b/tests/v1_1/test_flavors.py index 2634168ba..de4035c3a 100644 --- a/tests/v1_1/test_flavors.py +++ b/tests/v1_1/test_flavors.py @@ -91,3 +91,14 @@ class FlavorsTest(utils.TestCase): def test_delete(self): cs.flavors.delete("flavordelete") cs.assert_called('DELETE', '/flavors/flavordelete') + + def test_set_keys(self): + f = cs.flavors.get(1) + f.set_keys({'k1': 'v1'}) + cs.assert_called('POST', '/flavors/1/os-extra_specs', + {"extra_specs": {'k1': 'v1'}}) + + def test_unset_keys(self): + f = cs.flavors.get(1) + f.unset_keys(['k1']) + cs.assert_called('DELETE', '/flavors/1/os-extra_specs/k1') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index c1dfb6b14..230ba01c8 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -167,11 +167,12 @@ class ShellTest(utils.TestCase): def test_flavor_list(self): self.run_command('flavor-list') + self.assert_called('GET', '/flavors/2/os-extra_specs') self.assert_called_anytime('GET', '/flavors/detail') def test_flavor_show(self): self.run_command('flavor-show 1') - self.assert_called('GET', '/flavors/1') + self.assert_called_anytime('GET', '/flavors/1') def test_image_show(self): self.run_command('image-show 1') @@ -389,8 +390,9 @@ class ShellTest(utils.TestCase): } } - self.assert_called('POST', '/flavors', body, pos=-2) - self.assert_called('GET', '/flavors/1') + self.assert_called('POST', '/flavors', pos=-3) + self.assert_called('GET', '/flavors/1', pos=-2) + self.assert_called('GET', '/flavors/1/os-extra_specs', pos=-1) def test_aggregate_list(self): self.run_command('aggregate-list')