diff options
Diffstat (limited to 'openstackclient')
| -rw-r--r-- | openstackclient/compute/v2/server.py | 47 | ||||
| -rw-r--r-- | openstackclient/compute/v2/server_group.py | 17 | ||||
| -rw-r--r-- | openstackclient/tests/functional/compute/v2/test_server.py | 58 | ||||
| -rw-r--r-- | openstackclient/tests/unit/compute/v2/fakes.py | 35 | ||||
| -rw-r--r-- | openstackclient/tests/unit/compute/v2/test_server.py | 70 | ||||
| -rw-r--r-- | openstackclient/tests/unit/compute/v2/test_server_group.py | 120 |
6 files changed, 309 insertions, 38 deletions
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 93e9f966..1d1fc741 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -24,6 +24,7 @@ import os from novaclient import api_versions from novaclient.v2 import servers from openstack import exceptions as sdk_exceptions +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -38,6 +39,8 @@ from openstackclient.network import common as network_common LOG = logging.getLogger(__name__) +IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)' + def _format_servers_list_networks(networks): """Return a formatted string of a server's networks @@ -147,6 +150,12 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): info['image'] = "%s (%s)" % (image.name, image_id) except Exception: info['image'] = image_id + else: + # NOTE(melwitt): An server booted from a volume will have no image + # associated with it. We fill in the image with "N/A (booted from + # volume)" to help users who want to be able to grep for + # boot-from-volume servers when using the CLI. + info['image'] = IMAGE_STRING_FOR_BFV # Convert the flavor blob to a name flavor_info = info.get('flavor', {}) @@ -166,14 +175,14 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): if 'os-extended-volumes:volumes_attached' in info: info.update( { - 'volumes_attached': utils.format_list_of_dicts( + 'volumes_attached': format_columns.ListDictColumn( info.pop('os-extended-volumes:volumes_attached')) } ) if 'security_groups' in info: info.update( { - 'security_groups': utils.format_list_of_dicts( + 'security_groups': format_columns.ListDictColumn( info.pop('security_groups')) } ) @@ -182,9 +191,14 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): info['addresses'] = _format_servers_list_networks(server.networks) # Map 'metadata' field to 'properties' - info.update( - {'properties': utils.format_dict(info.pop('metadata'))} - ) + if not info['metadata']: + info.update( + {'properties': utils.format_dict(info.pop('metadata'))} + ) + else: + info.update( + {'properties': format_columns.DictColumn(info.pop('metadata'))} + ) # Migrate tenant_id to project_id naming if 'tenant_id' in info: @@ -751,19 +765,27 @@ class CreateServer(command.ShowOne): images_matched = [] for img in image_list: img_dict = {} + # exclude any unhashable entries - for key, value in img.items(): + img_dict_items = list(img.items()) + if img.properties: + img_dict_items.extend(list(img.properties.items())) + for key, value in img_dict_items: try: set([key, value]) except TypeError: + if key != 'properties': + LOG.debug('Skipped the \'%s\' attribute. ' + 'That cannot be compared. ' + '(image: %s, value: %s)', + key, img.id, value) pass else: img_dict[key] = value + if all(k in img_dict and img_dict[k] == v for k, v in wanted_properties.items()): images_matched.append(img) - else: - return [] return images_matched images = _match_image(image_client, parsed_args.image_property) @@ -1520,8 +1542,12 @@ class ListServer(command.Lister): s.image_name = image.name s.image_id = s.image['id'] else: - s.image_name = '' - s.image_id = '' + # NOTE(melwitt): An server booted from a volume will have no + # image associated with it. We fill in the Image Name and ID + # with "N/A (booted from volume)" to help users who want to be + # able to grep for boot-from-volume servers when using the CLI. + s.image_name = IMAGE_STRING_FOR_BFV + s.image_id = IMAGE_STRING_FOR_BFV if 'id' in s.flavor: flavor = flavors.get(s.flavor['id']) if flavor: @@ -2530,7 +2556,6 @@ class ShowServer(command.ShowOne): data = _prep_server_detail(compute_client, self.app.client_manager.image, server, refresh=False) - return zip(*sorted(data.items())) diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index c49a552f..1af6e28d 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -17,6 +17,7 @@ import logging +from novaclient import api_versions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -67,9 +68,13 @@ class CreateServerGroup(command.ShowOne): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute info = {} + + policy_arg = {'policies': [parsed_args.policy]} + if compute_client.api_version >= api_versions.APIVersion("2.64"): + policy_arg = {'policy': parsed_args.policy} server_group = compute_client.server_groups.create( - name=parsed_args.name, - policies=[parsed_args.policy]) + name=parsed_args.name, **policy_arg) + info.update(server_group._info) columns = _get_columns(info) @@ -136,11 +141,15 @@ class ListServerGroup(command.Lister): compute_client = self.app.client_manager.compute data = compute_client.server_groups.list(parsed_args.all_projects) + policy_key = 'Policies' + if compute_client.api_version >= api_versions.APIVersion("2.64"): + policy_key = 'Policy' + if parsed_args.long: column_headers = columns = ( 'ID', 'Name', - 'Policies', + policy_key, 'Members', 'Project Id', 'User Id', @@ -149,7 +158,7 @@ class ListServerGroup(command.Lister): column_headers = columns = ( 'ID', 'Name', - 'Policies', + policy_key, ) return (column_headers, diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 6e080e9b..44d9c61f 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -16,6 +16,7 @@ import uuid from tempest.lib import exceptions +from openstackclient.compute.v2 import server as v2_server from openstackclient.tests.functional.compute.v2 import common from openstackclient.tests.functional.volume.v2 import common as volume_common @@ -230,7 +231,7 @@ class ServerTests(common.ComputeTestCase): )) # Really, shouldn't this be a list? self.assertEqual( - "a='b', c='d'", + {'a': 'b', 'c': 'd'}, cmd_output['properties'], ) @@ -244,7 +245,7 @@ class ServerTests(common.ComputeTestCase): name )) self.assertEqual( - "c='d'", + {'c': 'd'}, cmd_output['properties'], ) @@ -509,6 +510,20 @@ class ServerTests(common.ComputeTestCase): server['name'], ) + # check that image indicates server "booted from volume" + self.assertEqual( + v2_server.IMAGE_STRING_FOR_BFV, + server['image'], + ) + # check server list too + servers = json.loads(self.openstack( + 'server list -f json' + )) + self.assertEqual( + v2_server.IMAGE_STRING_FOR_BFV, + servers[0]['Image'] + ) + # check volumes cmd_output = json.loads(self.openstack( 'volume show -f json ' + @@ -619,8 +634,8 @@ class ServerTests(common.ComputeTestCase): server_name )) volumes_attached = cmd_output['volumes_attached'] - self.assertTrue(volumes_attached.startswith('id=')) - attached_volume_id = volumes_attached.replace('id=', '') + self.assertIsNotNone(volumes_attached) + attached_volume_id = volumes_attached[0]["id"] # check the volume that attached on server cmd_output = json.loads(self.openstack( @@ -699,8 +714,8 @@ class ServerTests(common.ComputeTestCase): server_name )) volumes_attached = cmd_output['volumes_attached'] - self.assertTrue(volumes_attached.startswith('id=')) - attached_volume_id = volumes_attached.replace('id=', '') + self.assertIsNotNone(volumes_attached) + attached_volume_id = volumes_attached[0]["id"] # check the volume that attached on server cmd_output = json.loads(self.openstack( @@ -773,19 +788,21 @@ class ServerTests(common.ComputeTestCase): server_name )) volumes_attached = cmd_output['volumes_attached'] - self.assertTrue(volumes_attached.startswith('id=')) - attached_volume_id = volumes_attached.replace('id=', '') - # Don't leak the volume when the test exits. - self.addCleanup(self.openstack, 'volume delete ' + attached_volume_id) + self.assertIsNotNone(volumes_attached) + attached_volume_id = volumes_attached[0]["id"] + for vol in volumes_attached: + self.assertIsNotNone(vol['id']) + # Don't leak the volume when the test exits. + self.addCleanup(self.openstack, 'volume delete ' + vol['id']) # Since the server is volume-backed the GET /servers/{server_id} - # response will have image=''. - self.assertEqual('', cmd_output['image']) + # response will have image='N/A (booted from volume)'. + self.assertEqual(v2_server.IMAGE_STRING_FOR_BFV, cmd_output['image']) # check the volume that attached on server cmd_output = json.loads(self.openstack( 'volume show -f json ' + - attached_volume_id + volumes_attached[0]["id"] )) # The volume size should be what we specified on the command line. self.assertEqual(1, int(cmd_output['size'])) @@ -879,14 +896,21 @@ class ServerTests(common.ComputeTestCase): self.assertIsNotNone(server['id']) self.assertEqual(server_name, server['name']) - self.assertIn(str(security_group1['id']), server['security_groups']) - self.assertIn(str(security_group2['id']), server['security_groups']) + sec_grp = "" + for sec in server['security_groups']: + sec_grp += sec['name'] + self.assertIn(str(security_group1['id']), sec_grp) + self.assertIn(str(security_group2['id']), sec_grp) self.wait_for_status(server_name, 'ACTIVE') server = json.loads(self.openstack( 'server show -f json ' + server_name )) - self.assertIn(sg_name1, server['security_groups']) - self.assertIn(sg_name2, server['security_groups']) + # check if security group exists in list + sec_grp = "" + for sec in server['security_groups']: + sec_grp += sec['name'] + self.assertIn(sg_name1, sec_grp) + self.assertIn(sg_name2, sec_grp) def test_server_create_with_empty_network_option_latest(self): """Test server create with empty network option in nova 2.latest.""" diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 6e12f735..31430984 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1244,7 +1244,7 @@ class FakeServerGroup(object): """Fake one server group""" @staticmethod - def create_one_server_group(attrs=None): + def _create_one_server_group(attrs=None): """Create a fake server group :param Dictionary attrs: @@ -1261,7 +1261,6 @@ class FakeServerGroup(object): 'members': [], 'metadata': {}, 'name': 'server-group-name-' + uuid.uuid4().hex, - 'policies': [], 'project_id': 'server-group-project-id-' + uuid.uuid4().hex, 'user_id': 'server-group-user-id-' + uuid.uuid4().hex, } @@ -1274,6 +1273,38 @@ class FakeServerGroup(object): loaded=True) return server_group + @staticmethod + def create_one_server_group(attrs=None): + """Create a fake server group + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id and other attributes + """ + if attrs is None: + attrs = {} + attrs.setdefault('policies', ['policy1', 'policy2']) + return FakeServerGroup._create_one_server_group(attrs) + + +class FakeServerGroupV264(object): + """Fake one server group fo API >= 2.64""" + + @staticmethod + def create_one_server_group(attrs=None): + """Create a fake server group + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id and other attributes + """ + if attrs is None: + attrs = {} + attrs.setdefault('policy', 'policy1') + return FakeServerGroup._create_one_server_group(attrs) + class FakeUsage(object): """Fake one or more usage.""" diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 7e4c71c5..02bb406c 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2048,6 +2048,65 @@ class TestServerCreate(TestServer): self.cmd.take_action, parsed_args) + def test_server_create_image_property_with_image_list(self): + arglist = [ + '--image-property', + 'owner_specified.openstack.object=image/cirros', + '--flavor', 'flavor1', + '--nic', 'none', + self.new_server.name, + ] + + verifylist = [ + ('image_property', + {'owner_specified.openstack.object': 'image/cirros'}), + ('flavor', 'flavor1'), + ('nic', ['none']), + ('server_name', self.new_server.name), + ] + # create a image_info as the side_effect of the fake image_list() + image_info = { + 'properties': { + 'owner_specified.openstack.object': 'image/cirros' + } + } + + target_image = image_fakes.FakeImage.create_one_image(image_info) + another_image = image_fakes.FakeImage.create_one_image({}) + self.images_mock.return_value = [target_image, another_image] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping_v2=[], + nics='none', + meta=None, + scheduler_hints={}, + config_drive=None, + ) + + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + target_image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + def test_server_create_invalid_hint(self): # Not a key-value pair arglist = [ @@ -2609,7 +2668,7 @@ class TestServerList(TestServer): s.status, server._format_servers_list_networks(s.networks), # Image will be an empty string if boot-from-volume - self.image.name if s.image else s.image, + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, self.flavor.name, )) self.data_long.append(( @@ -2622,8 +2681,8 @@ class TestServerList(TestServer): ), server._format_servers_list_networks(s.networks), # Image will be an empty string if boot-from-volume - self.image.name if s.image else s.image, - s.image['id'] if s.image else s.image, + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, self.flavor.name, s.flavor['id'], getattr(s, 'OS-EXT-AZ:availability_zone'), @@ -2636,7 +2695,7 @@ class TestServerList(TestServer): s.status, server._format_servers_list_networks(s.networks), # Image will be an empty string if boot-from-volume - s.image['id'] if s.image else s.image, + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, s.flavor['id'] )) @@ -5166,6 +5225,8 @@ class TestServerGeneral(TestServer): 'tenant_id': u'tenant-id-xxx', 'networks': {u'public': [u'10.20.30.40', u'2001:db8::f']}, 'links': u'http://xxx.yyy.com', + 'properties': '', + 'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}], } _server = compute_fakes.FakeServer.create_one_server(attrs=server_info) find_resource.side_effect = [_server, _flavor] @@ -5182,6 +5243,7 @@ class TestServerGeneral(TestServer): 'properties': '', 'OS-EXT-STS:power_state': server._format_servers_list_power_state( getattr(_server, 'OS-EXT-STS:power_state')), + 'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}], } # Call _prep_server_detail(). diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index 9cd876ea..359cd2bd 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -15,6 +15,7 @@ from unittest import mock +from novaclient import api_versions from osc_lib import exceptions from osc_lib import utils @@ -53,6 +54,33 @@ class TestServerGroup(compute_fakes.TestComputev2): self.server_groups_mock.reset_mock() +class TestServerGroupV264(TestServerGroup): + + fake_server_group = \ + compute_fakes.FakeServerGroupV264.create_one_server_group() + + columns = ( + 'id', + 'members', + 'name', + 'policy', + 'project_id', + 'user_id', + ) + + data = ( + fake_server_group.id, + utils.format_list(fake_server_group.members), + fake_server_group.name, + fake_server_group.policy, + fake_server_group.project_id, + fake_server_group.user_id, + ) + + def setUp(self): + super(TestServerGroupV264, self).setUp() + + class TestServerGroupCreate(TestServerGroup): def setUp(self): @@ -80,6 +108,28 @@ class TestServerGroupCreate(TestServerGroup): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_server_group_create_v264(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.64') + + arglist = [ + '--policy', 'soft-anti-affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', 'soft-anti-affinity'), + ('name', 'affinity_group'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.create.assert_called_once_with( + name=parsed_args.name, + policy=parsed_args.policy, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + class TestServerGroupDelete(TestServerGroup): @@ -230,6 +280,76 @@ class TestServerGroupList(TestServerGroup): self.assertEqual(self.list_data_long, tuple(data)) +class TestServerGroupListV264(TestServerGroupV264): + + list_columns = ( + 'ID', + 'Name', + 'Policy', + ) + + list_columns_long = ( + 'ID', + 'Name', + 'Policy', + 'Members', + 'Project Id', + 'User Id', + ) + + list_data = (( + TestServerGroupV264.fake_server_group.id, + TestServerGroupV264.fake_server_group.name, + TestServerGroupV264.fake_server_group.policy, + ),) + + list_data_long = (( + TestServerGroupV264.fake_server_group.id, + TestServerGroupV264.fake_server_group.name, + TestServerGroupV264.fake_server_group.policy, + utils.format_list(TestServerGroupV264.fake_server_group.members), + TestServerGroupV264.fake_server_group.project_id, + TestServerGroupV264.fake_server_group.user_id, + ),) + + def setUp(self): + super(TestServerGroupListV264, self).setUp() + + self.server_groups_mock.list.return_value = [self.fake_server_group] + self.cmd = server_group.ListServerGroup(self.app, None) + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.64') + + def test_server_group_list(self): + arglist = [] + verifylist = [ + ('all_projects', False), + ('long', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.list.assert_called_once_with(False) + + self.assertEqual(self.list_columns, columns) + self.assertEqual(self.list_data, tuple(data)) + + def test_server_group_list_with_all_projects_and_long(self): + arglist = [ + '--all-projects', + '--long', + ] + verifylist = [ + ('all_projects', True), + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.list.assert_called_once_with(True) + + self.assertEqual(self.list_columns_long, columns) + self.assertEqual(self.list_data_long, tuple(data)) + + class TestServerGroupShow(TestServerGroup): def setUp(self): |
