From 80e91066687618da5785e8193b98df94d20f2c36 Mon Sep 17 00:00:00 2001 From: Petr Blaho Date: Wed, 4 Sep 2013 15:59:00 +0200 Subject: [PATCH] Adds Flavor CRUD CLI commands Adds tests for Flavor CRUD CLI commands. Implements: blueprint tripleo-flavors-cli https://blueprints.launchpad.net/python-tuskarclient/+spec/tripleo-flavors-cli Change-Id: If00f281b6287e34773dacd43af849b221a93cd8d --- tuskarclient/tests/v1/test_flavors_shell.py | 311 ++++++++++++++++++++ tuskarclient/v1/flavors_shell.py | 151 ++++++++++ 2 files changed, 462 insertions(+) create mode 100644 tuskarclient/tests/v1/test_flavors_shell.py diff --git a/tuskarclient/tests/v1/test_flavors_shell.py b/tuskarclient/tests/v1/test_flavors_shell.py new file mode 100644 index 0000000..7003b28 --- /dev/null +++ b/tuskarclient/tests/v1/test_flavors_shell.py @@ -0,0 +1,311 @@ +# Copyright (c) 2013 Red Hat, Inc. +# All Rights Reserved. + +# 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. + +import mock + +import tuskarclient.tests.utils as tutils +from tuskarclient.v1 import flavors_shell + + +class FlavorsShellListTest(tutils.TestCase): + + def empty_args(self): + args = mock.Mock(spec=[]) + for attr in ['id', 'resource_class_id', 'capacities', 'max_vms']: + setattr(args, attr, None) + return args + + @mock.patch.object(flavors_shell, 'fetch_resource_class') + @mock.patch.object(flavors_shell, 'fmt') + def test_list_works_if_resource_class_exists( + self, mocked_fmt, mocked_fetch_resource_class): + tuskar = mock.MagicMock() + args = self.empty_args() + args.resource_class_id = 1 + + flavors_shell.do_flavor_list(tuskar, args) + tuskar.flavors.list.assert_called_with( + mocked_fetch_resource_class.return_value.id) + mocked_fmt.print_list.assert_called_with( + tuskar.flavors.list.return_value, + mock.ANY, mock.ANY, mock.ANY + ) + + def test_show_works_with_integer_id(self): + self.show_works_with_id({'resource_class_id': 1, + 'flavor_id': 1}) + + def test_show_works_with_char_id(self): + self.show_works_with_id({'resource_class_id': 1, + 'flavor_id': '1'}) + + def test_show_works_with_string_id(self): + self.show_works_with_id({'resource_class_id': 1, + 'flavor_id': 'string'}) + + @mock.patch.object(flavors_shell, 'fetch_flavor') + @mock.patch.object(flavors_shell, 'print_flavor_detail') + def show_works_with_id(self, + parameters, + mocked_print_flavor_detail, + mocked_fetch_flavor): + flavor_id = parameters.get('flavor_id') + resource_class_id = parameters.get('resource_class_id') + tuskar = mock.MagicMock() + args = self.empty_args() + args.id = flavor_id + args.resource_class_id = resource_class_id + + flavors_shell.do_flavor_show(tuskar, args) + mocked_fetch_flavor.assert_called_with(tuskar, + resource_class_id, + args.id) + mocked_print_flavor_detail.assert_called_with( + mocked_fetch_flavor.return_value, + ) + + def test_delete_works_with_integer_id(self): + self.delete_works_with_id({'resource_class_id': 1, + 'flavor_id': 1}) + + def test_delete_works_with_char_id(self): + self.delete_works_with_id({'resource_class_id': 1, + 'flavor_id': '1'}) + + def test_delete_works_with_string_id(self): + self.delete_works_with_id({'resource_class_id': 1, + 'flavor_id': 'string'}) + + @mock.patch.object(flavors_shell, 'fetch_flavor') + def delete_works_with_id(self, parameters, mocked_fetch_flavor): + flavor_id = parameters.get('flavor_id') + tuskar = mock.MagicMock() + args = self.empty_args() + args.id = flavor_id + + flavors_shell.do_flavor_delete(tuskar, args) + mocked_fetch_flavor.assert_called_with(tuskar, args.id) + tuskar.flavors.delete.assert_called_with(args.id) + + def test_create_works_with_name(self): + self.create_works_with({'resource_class_id': 1, + 'name': 'name', + 'capacities': None, + 'max_vms': None}) + + def test_create_works_with_name_and_capacities(self): + self.create_works_with({'resource_class_id': 1, + 'name': 'name', + 'capacities': + 'total_memory:2048:MB,total_cpu:3:CPU', + 'max_vms': None}) + + @mock.patch.object(flavors_shell, 'fetch_resource_class') + @mock.patch.object(flavors_shell, 'print_flavor_detail') + def create_works_with( + self, parameters, mocked_print_flavor_detail, + mocked_fetch_resource_class): + name = parameters.get('name') + resource_class_id = parameters.get('resource_class_id') + capacities = parameters.get('capacities') + max_vms = parameters.get('max_vms') + tuskar = mock.MagicMock() + args = self.empty_args() + args.name = name + args.resource_class_id = resource_class_id + args.capacities = capacities + args.max_vms = max_vms + + expected_params = { + 'name': name, + 'max_vms': max_vms, + } + + if capacities is not None: + expected_params['capacities'] = \ + flavors_shell.parse_capacities(capacities) + + mocked_fetch_resource_class.return_value.id = resource_class_id + + flavors_shell.do_flavor_create(tuskar, + args) + tuskar.flavors.create.assert_called_with( + resource_class_id, + **expected_params + ) + mocked_print_flavor_detail.assert_called_with( + tuskar.flavors.create.return_value + ) + + def test_update_works_with_integer_id(self): + self.update_works({'flavor_id': 1, + 'resource_class_id': 1, + 'name': None, + 'capacities': None, + 'max_vms': None}, + {}) + + def test_update_works_with_char_id(self): + self.update_works({'flavor_id': '1', + 'resource_class_id': 1, + 'name': None, + 'capacities': None, + 'max_vms': None}, + {}) + + def test_update_works_with_string_id(self): + self.update_works({'flavor_id': 'string', + 'resource_class_id': 1, + 'name': None, + 'capacities': None, + 'max_vms': None}, + {}) + + def test_update_works_with_id_and_name(self): + self.update_works({'flavor_id': 1, + 'resource_class_id': 1, + 'name': 'name', + 'capacities': None, + 'max_vms': None}, + {'name': 'name'}) + + def test_update_works_with_id_and_empty_capacities(self): + self.update_works({'flavor_id': 1, + 'resource_class_id': 1, + 'name': None, + 'capacities': '', + 'max_vms': None}, + {'capacities': []}) + + def test_update_works_with_id_and_capacities(self): + self.update_works({'flavor_id': 1, + 'resource_class_id': 1, + 'name': None, + 'capacities': + 'total_memory:2048:MB,total_cpu:3:CPU', + 'max_vms': None}, + {'capacities': [ + {'unit': 'MB', + 'name': 'total_memory', + 'value': '2048'}, + {'unit': 'CPU', + 'name': 'total_cpu', + 'value': '3'}]}) + + def test_update_works_with_id_name_and_capacities(self): + self.update_works({'flavor_id': 1, + 'resource_class_id': 1, + 'name': 'name', + 'capacities': + 'total_memory:2048:MB,total_cpu:3:CPU', + 'max_vms': None}, + {'name': 'name', + 'capacities': [ + {'unit': 'MB', + 'name': 'total_memory', + 'value': '2048'}, + {'unit': 'CPU', + 'name': 'total_cpu', + 'value': '3'}]}) + + @mock.patch.object(flavors_shell, 'fetch_flavor') + @mock.patch.object(flavors_shell, 'print_flavor_detail') + def update_works(self, + parameters, + expected_parameters, + mocked_print_flavor_detail, + mocked_fetch_flavor): + flavor_id = parameters.get('flavor_id') + resource_class_id = parameters.get('resource_class_id') + name = parameters.get('name') + capacities = parameters.get('capacities') + max_vms = parameters.get('max_vms') + tuskar = mock.MagicMock() + args = self.empty_args() + args.id = flavor_id + args.resource_class_id = resource_class_id + args.name = name + args.max_vms = max_vms + args.capacities = capacities + + flavors_shell.do_flavor_update(tuskar, args) + mocked_fetch_flavor.assert_called_with(tuskar, + resource_class_id, + args.id) + tuskar.flavors.update.assert_called_with( + resource_class_id, + flavor_id, + **expected_parameters + ) + mocked_print_flavor_detail.assert_called_with( + tuskar.flavors.update.return_value + ) + + @mock.patch.object(flavors_shell.utils, 'find_resource') + def test_fetch_flavor_works(self, + mocked_find_resource): + tuskar = mock.MagicMock() + flavor_id = 1 + resource_class_id = 1 + + return_value = flavors_shell.fetch_flavor( + tuskar, resource_class_id, flavor_id) + tuskar.flavors.get.assert_called_with(resource_class_id, + flavor_id) + self.assertEqual(return_value, tuskar.flavors.get.return_value) + + @mock.patch.object(flavors_shell.utils, 'find_resource') + def test_fetch_flavor_blows(self, + mocked_find_resource): + tuskar = mock.MagicMock() + flavor_id = 1 + resource_class_id = 1 + e = flavors_shell.exc.HTTPNotFound( + "Flavor not found: 1") + + mocked_find_resource.side_effect = e + tuskar.flavors.get.side_effect = e + self.assertRaises(flavors_shell.exc.CommandError, + flavors_shell.fetch_flavor, + tuskar, resource_class_id, flavor_id) + tuskar.flavors.get.assert_called_with(resource_class_id, + flavor_id) + + @mock.patch.object(flavors_shell, 'fmt') + def test_print_flavor_detail_works(self, + mocked_fmt): + flavor = mock.MagicMock() + + flavors_shell.print_flavor_detail(flavor) + mocked_fmt.print_dict.assert_called_with( + flavor.to_dict.return_value, + mock.ANY) + + def test_create_flavor_dict_works(self): + args = self.empty_args() + args.name = 'name-value' + args.capacities = 'total_memory:2048:MB,total_cpu:3:CPU' + args.other = 'other-value' + + expected_flavor_dict = {'name': 'name-value', + 'capacities': [{'name': 'total_memory', + 'value': '2048', + 'unit': 'MB'}, + {'name': 'total_cpu', + 'value': '3', + 'unit': 'CPU'}] + } + flavor_dict = flavors_shell.create_flavor_dict(args) + self.assertEqual(flavor_dict, expected_flavor_dict) diff --git a/tuskarclient/v1/flavors_shell.py b/tuskarclient/v1/flavors_shell.py index 94e731d..1709fff 100644 --- a/tuskarclient/v1/flavors_shell.py +++ b/tuskarclient/v1/flavors_shell.py @@ -9,3 +9,154 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + +import tuskarclient.common.formatting as fmt +from tuskarclient.common import utils +from tuskarclient import exc + + +@utils.arg('resource_class_id', metavar="", + help="Name or ID of resource class which flavors to show.") +def do_flavor_list(tuskar, args): + # TODO(pblaho): + # temp. fetch resource_class - API do return 200 OK + # when getting list of flavors for non-existing resource class + resource_class = fetch_resource_class(tuskar, args.resource_class_id) + flavors = tuskar.flavors.list(resource_class.id) + fields = ['id', 'name', 'capacities', 'max_vms'] + labels = {'racks': '# of racks'} + formatters = {'racks': len, 'capacities': fmt.capacities_formatter} + + fmt.print_list(flavors, fields, formatters, labels) + + +@utils.arg('resource_class_id', metavar="", + help="Name or ID of resource class associated to.") +@utils.arg('id', metavar="", + help="Name or ID of the flavor to update.") +def do_flavor_show(tuskar, args): + fetch_resource_class(tuskar, args.resource_class_id) + flavor = fetch_flavor(tuskar, args.resource_class_id, args.id) + print_flavor_detail(flavor) + + +@utils.arg('resource_class_id', metavar="", + help="Name or ID of resource class associated to.") +@utils.arg('name', + help="Name of the flavor to create.") +@utils.arg('--capacities', + help="Capacities of the flavor to create.") +@utils.arg('--max-vms', + help="Maximum # of VMs of the flavor to create.") +def do_flavor_create(tuskar, args): + resource_class = fetch_resource_class(tuskar, args.resource_class_id) + flavor_dict = { + 'name': args.name, + 'max_vms': args.max_vms, + } + + if args.capacities is not None: + flavor_dict['capacities'] = parse_capacities(args.capacities) + + flavor = tuskar.flavors.create(resource_class.id, **flavor_dict) + print_flavor_detail(flavor) + + +@utils.arg('resource_class_id', metavar="", + help="Name or ID of resource class associated to.") +@utils.arg('id', metavar="", + help="Name or ID of the flavor to update.") +@utils.arg('--name', + help="New name of the flavor to update.") +@utils.arg('--capacities', + help="Capacities of the flavor to update.") +@utils.arg('--max-vms', + help="Maximum # of VMs of the flavor to update.") +def do_flavor_update(tuskar, args): + flavor_dict = create_flavor_dict(args) + fetch_resource_class(tuskar, args.resource_class_id) + fetch_flavor(tuskar, args.resource_class_id, args.id) + + flavor = tuskar.flavors.update(args.resource_class_id, + args.id, + **flavor_dict) + print_flavor_detail(flavor) + + +def do_flavor_delete(tuskar, args): + fetch_flavor(tuskar, args.id) + + tuskar.flavors.delete(args.id) + + +def print_flavor_detail(flavor): + flavor_dict = flavor.to_dict() + formatters = {'links': fmt.links_formatter, + 'capacities': fmt.capacities_formatter, + } + fmt.print_dict(flavor_dict, formatters) + + +def fetch_flavor(tuskar, resource_class_id, flavor_id): + try: + flavor = tuskar.flavors.get(resource_class_id, flavor_id) + except exc.HTTPNotFound: + raise exc.CommandError( + "Flavor not found: %s" % flavor_id) + + return flavor + + +# TODO(pblaho): +# temp. fetch resource_class - API do return 200 OK +# when getting list of flavors for non-existing resource class +def fetch_resource_class(tuskar, resource_class_id): + try: + resource_class = utils.find_resource(tuskar.resource_classes, + resource_class_id) + except exc.HTTPNotFound: + raise exc.CommandError( + "Resource Class not found: %s" % resource_class_id) + + return resource_class + + +# TODO(pblaho): duplicate now +# refactor after merged both this and https://review.openstack.org/#/c/44281 +def parse_capacities(capacities_str): + '''Take capacities from CLI and parse them into format for API. + + :param capacities_string: string of capacities like + 'total_cpu:64:CPU,total_memory:1024:MB' + :return: array of capacities dicts usable for requests to API + ''' + if capacities_str == '': + return [] + + capacities = [] + for capacity_str in capacities_str.split(','): + fields = capacity_str.split(':') + if len(fields) != 3: + raise exc.CommandError( + 'Capacity info "{0}" should be 3 fields separated by colons. ' + '(Use commas to separate multiple capacities.)' + .format(capacity_str)) + capacities.append( + {'name': fields[0], 'value': fields[1], 'unit': fields[2]}) + + return capacities + + +def create_flavor_dict(args): + flavor_dict = {} + + simple_fields = ['name', 'max_vms'] + for field_name in simple_fields: + field_value = vars(args)[field_name] + if field_value is not None: + flavor_dict[field_name] = field_value + + if args.capacities is not None: + flavor_dict['capacities'] = parse_capacities(args.capacities) + + return flavor_dict