diff --git a/tuskarclient/tests/v1/test_racks_shell.py b/tuskarclient/tests/v1/test_racks_shell.py new file mode 100644 index 0000000..83c8112 --- /dev/null +++ b/tuskarclient/tests/v1/test_racks_shell.py @@ -0,0 +1,118 @@ +# 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 io +import mock + +import tuskarclient.tests.utils as tutils +from tuskarclient.v1 import racks_shell + + +def empty_args(): + args = mock.Mock(spec=[]) + for attr in ['id', 'name', 'subnet', 'capacities', 'slots', + 'resource_class']: + setattr(args, attr, None) + return args + + +def mock_rack(): + rack = mock.Mock() + rack.id = '5' + rack.name = 'test_rack' + return rack + + +class RacksShellTest(tutils.TestCase): + + def setUp(self): + self.outfile = io.StringIO() + self.tuskar = mock.MagicMock() + super(RacksShellTest, self).setUp() + + @mock.patch('tuskarclient.common.utils.find_resource') + @mock.patch('tuskarclient.v1.racks_shell.print_rack_detail') + def test_rack_show(self, mock_print_detail, mock_find_resource): + mock_find_resource.return_value = mock_rack() + args = empty_args() + args.id = '5' + + racks_shell.do_rack_show(self.tuskar, args, outfile=self.outfile) + mock_find_resource.assert_called_with(self.tuskar.racks, '5') + mock_print_detail.assert_called_with(mock_find_resource.return_value, + outfile=self.outfile) + + @mock.patch('tuskarclient.common.formatting.print_list') + def test_rack_list(self, mock_print_list): + args = empty_args() + + racks_shell.do_rack_list(self.tuskar, args, outfile=self.outfile) + # testing the other arguments would be just copy-paste + mock_print_list.assert_called_with( + self.tuskar.racks.list.return_value, mock.ANY, mock.ANY, mock.ANY, + outfile=self.outfile + ) + + @mock.patch('tuskarclient.common.utils.find_resource') + @mock.patch('tuskarclient.v1.racks_shell.print_rack_detail') + def test_rack_create(self, mock_print_detail, mock_find_resource): + mock_find_resource.return_value = mock_rack() + args = empty_args() + args.name = 'my_rack' + args.subnet = '1.2.3.4/20' + args.capacities = 'total_memory:2048:MB,total_cpu:3:CPU' + args.slots = '2' + args.resource_class = '1' + + racks_shell.do_rack_create(self.tuskar, args, outfile=self.outfile) + self.tuskar.racks.create.assert_called_with( + name='my_rack', + subnet='1.2.3.4/20', + capacities=[ + {'name': 'total_memory', 'value': '2048', 'unit': 'MB'}, + {'name': 'total_cpu', 'value': '3', 'unit': 'CPU'}], + slots='2', + resource_class={'id': '1'}) + mock_print_detail.assert_called_with( + self.tuskar.racks.create.return_value, outfile=self.outfile) + + @mock.patch('tuskarclient.common.utils.find_resource') + @mock.patch('tuskarclient.v1.racks_shell.print_rack_detail') + def test_rack_update(self, mock_print_detail, mock_find_resource): + mock_find_resource.return_value = mock_rack() + args = empty_args() + args.id = '5' + args.name = 'my_rack' + args.capacities = 'total_memory:2048:MB,total_cpu:3:CPU' + args.resource_class = '1' + + racks_shell.do_rack_update(self.tuskar, args, outfile=self.outfile) + self.tuskar.racks.update.assert_called_with( + '5', + name='my_rack', + capacities=[ + {'name': 'total_memory', 'value': '2048', 'unit': 'MB'}, + {'name': 'total_cpu', 'value': '3', 'unit': 'CPU'}], + resource_class={'id': '1'}) + mock_print_detail.assert_called_with( + self.tuskar.racks.update.return_value, outfile=self.outfile) + + @mock.patch('tuskarclient.common.utils.find_resource') + def test_rack_delete(self, mock_find_resource): + mock_find_resource.return_value = mock_rack() + args = empty_args() + args.id = '5' + + racks_shell.do_rack_delete(self.tuskar, args, outfile=self.outfile) + self.tuskar.racks.delete.assert_called_with('5') + self.assertEqual('Deleted rack "test_rack".\n', + self.outfile.getvalue()) diff --git a/tuskarclient/v1/racks_shell.py b/tuskarclient/v1/racks_shell.py index e3c18ae..1051d71 100644 --- a/tuskarclient/v1/racks_shell.py +++ b/tuskarclient/v1/racks_shell.py @@ -10,18 +10,80 @@ # License for the specific language governing permissions and limitations # under the License. +from __future__ import print_function + +import sys + import tuskarclient.common.formatting as fmt from tuskarclient.common import utils from tuskarclient import exc -# TODO(jistr): This is PoC, not final implementation @utils.arg('id', metavar="", help="Name or ID of rack to show.") -def do_rack_show(tuskar, args): - try: - rack = utils.find_resource(tuskar.racks, args.id) - except exc.HTTPNotFound: - raise exc.CommandError("Rack not found: %s" % args.id) +def do_rack_show(tuskar, args, outfile=sys.stdout): + rack = utils.find_resource(tuskar.racks, args.id) + print_rack_detail(rack, outfile=outfile) + + +def do_rack_list(tuskar, args, outfile=sys.stdout): + racks = tuskar.racks.list() + fields = ['id', 'name', 'subnet', 'state', 'nodes'] + labels = {'nodes': '# of nodes'} + formatters = {'nodes': len} + fmt.print_list(racks, fields, formatters, labels, outfile=outfile) + + +@utils.arg('name', help="Name of the rack to create.") +@utils.arg('--subnet', required=True, + help="Rack's network in IP/CIDR notation.") +@utils.arg('--slots', required=True, help="Number of slots in the rack.") +@utils.arg('--capacities', help="Total capacities of the rack.") +@utils.arg('--resource-class', help="Resource class to assign the rack to.") +def do_rack_create(tuskar, args, outfile=sys.stdout): + rack_dict = create_rack_dict(args) + rack = tuskar.racks.create(**rack_dict) + print_rack_detail(rack, outfile=outfile) + + +@utils.arg('id', metavar="", help="Name or ID of rack to show.") +@utils.arg('--name', help="Rack's updated name.") +@utils.arg('--subnet', help="Rack's network in IP/CIDR notation.") +@utils.arg('--capacities', help="Total capacities of the rack.") +@utils.arg('--slots', help="Number of slots in the rack.") +@utils.arg('--resource-class', help="Resource class to assign the rack to.") +def do_rack_update(tuskar, args, outfile=sys.stdout): + rack = utils.find_resource(tuskar.racks, args.id) + rack_dict = create_rack_dict(args) + updated_rack = tuskar.racks.update(rack.id, **rack_dict) + print_rack_detail(updated_rack, outfile=outfile) + + +@utils.arg('id', metavar="", help="Name or ID of rack to show.") +def do_rack_delete(tuskar, args, outfile=sys.stdout): + rack = utils.find_resource(tuskar.racks, args.id) + tuskar.racks.delete(args.id) + print(u'Deleted rack "%s".' % rack.name, file=outfile) + + +def create_rack_dict(args): + """Marshal command line arguments to an API request dict.""" + rack_dict = {} + simple_fields = ['name', 'subnet', 'slots'] + for field_name in simple_fields: + field_value = vars(args)[field_name] + if field_value is not None: + rack_dict[field_name] = field_value + + utils.marshal_association(args, rack_dict, 'resource_class') + + if args.capacities is not None: + rack_dict['capacities'] = parse_capacities(args.capacities) + + return rack_dict + + +def print_rack_detail(rack, outfile=sys.stdout): + """Print detailed rack information (for rack-show etc.).""" formatters = { 'capacities': fmt.capacities_formatter, 'chassis': fmt.resource_link_formatter, @@ -36,14 +98,28 @@ def do_rack_show(tuskar, args): if 'chassis' in rack_dict and not rack_dict['chassis']: del rack_dict['chassis'] - fmt.print_dict(rack_dict, formatters) + fmt.print_dict(rack_dict, formatters, outfile=outfile) -# TODO(jistr): This is PoC, not final implementation -def do_rack_list(tuskar, args): - racks = tuskar.racks.list() - fields = ['id', 'name', 'subnet', 'state', 'nodes'] - labels = {'nodes': '# of nodes'} - formatters = {'nodes': len} +def parse_capacities(capacities_str): + """Take capacities from CLI and parse them into format for API. - fmt.print_list(racks, fields, formatters, labels) + :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