diff options
-rw-r--r-- | .zuul.yaml | 3 | ||||
-rw-r--r-- | lower-constraints.txt | 142 | ||||
-rw-r--r-- | openstackclient/compute/v2/server.py | 192 | ||||
-rw-r--r-- | openstackclient/compute/v2/server_group.py | 63 | ||||
-rw-r--r-- | openstackclient/image/v1/image.py | 6 | ||||
-rw-r--r-- | openstackclient/image/v2/image.py | 6 | ||||
-rw-r--r-- | openstackclient/tests/unit/compute/v2/test_server.py | 316 | ||||
-rw-r--r-- | openstackclient/tests/unit/compute/v2/test_server_group.py | 90 | ||||
-rw-r--r-- | openstackclient/tests/unit/image/v2/test_image.py | 3 | ||||
-rw-r--r-- | releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml | 5 | ||||
-rw-r--r-- | releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml | 8 | ||||
-rw-r--r-- | releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml | 7 | ||||
-rw-r--r-- | releasenotes/notes/story-2007727-69b705c561309742.yaml | 6 | ||||
-rw-r--r-- | requirements.txt | 4 | ||||
-rw-r--r-- | tox.ini | 6 |
15 files changed, 495 insertions, 362 deletions
@@ -1,3 +1,4 @@ +--- - job: name: osc-tox-unit-tips parent: openstack-tox @@ -220,12 +221,10 @@ - openstackclient-plugin-jobs - osc-tox-unit-tips - openstack-cover-jobs - - openstack-lower-constraints-jobs - openstack-python3-victoria-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 - - lib-forward-testing-python3 check: jobs: - osc-build-image diff --git a/lower-constraints.txt b/lower-constraints.txt deleted file mode 100644 index d880484b..00000000 --- a/lower-constraints.txt +++ /dev/null @@ -1,142 +0,0 @@ -amqp==2.1.1 -aodhclient==0.9.0 -appdirs==1.3.0 -asn1crypto==0.23.0 -bandit==1.1.0 -cachetools==2.0.0 -cffi==1.14.0 -cliff==2.8.0 -cmd2==0.8.0 -contextlib2==0.4.0 -coverage==4.0 -cryptography==2.7 -ddt==1.0.1 -debtcollector==1.2.0 -decorator==4.4.1 -deprecation==1.0 -docker-pycreds==0.2.1 -docker==2.4.2 -dogpile.cache==0.6.5 -eventlet==0.18.2 -extras==1.0.0 -fasteners==0.7.0 -fixtures==3.0.0 -flake8-import-order==0.13 -flake8==2.6.2 -future==0.16.0 -futurist==2.1.0 -gitdb==0.6.4 -GitPython==1.0.1 -gnocchiclient==3.3.1 -greenlet==0.4.15 -hacking==2.0.0 -httplib2==0.9.1 -idna==2.6 -iso8601==0.1.11 -Jinja2==2.10 -jmespath==0.9.0 -jsonpatch==1.16 -jsonpointer==1.13 -jsonschema==2.6.0 -keystoneauth1==3.18.0 -kombu==4.0.0 -linecache2==1.0.0 -MarkupSafe==1.1.0 -mccabe==0.2.1 -monotonic==0.6 -mox3==0.20.0 -msgpack-python==0.4.0 -munch==2.1.0 -netaddr==0.7.18 -netifaces==0.10.4 -openstacksdk==0.48.0 -os-service-types==1.7.0 -os-testr==1.0.0 -osc-lib==2.0.0 -osc-placement==1.7.0 -oslo.concurrency==3.26.0 -oslo.config==5.2.0 -oslo.context==2.19.2 -oslo.i18n==3.15.3 -oslo.log==3.36.0 -oslo.messaging==5.29.0 -oslo.middleware==3.31.0 -oslo.serialization==2.18.0 -oslo.service==1.24.0 -oslo.utils==3.33.0 -oslotest==3.2.0 -osprofiler==1.4.0 -paramiko==2.0.0 -Paste==2.0.2 -PasteDeploy==1.5.0 -pbr==2.0.0 -pika-pool==0.1.3 -pika==0.10.0 -ply==3.10 -positional==1.2.1 -prettytable==0.7.2 -pyasn1==0.1.8 -pycodestyle==2.0.0 -pycparser==2.18 -pyflakes==0.8.1 -pyinotify==0.9.6 -pyOpenSSL==17.1.0 -pyparsing==2.1.0 -pyperclip==1.5.27 -python-barbicanclient==4.5.2 -python-cinderclient==3.3.0 -python-dateutil==2.5.3 -python-designateclient==2.7.0 -python-glanceclient==2.8.0 -python-heatclient==1.10.0 -python-ironic-inspector-client==1.5.0 -python-ironicclient==2.3.0 -python-karborclient==0.6.0 -python-keystoneclient==3.22.0 -python-mimeparse==1.6.0 -python-mistralclient==3.1.0 -python-muranoclient==0.8.2 -python-neutronclient==6.7.0 -python-novaclient==15.1.0 -python-octaviaclient==1.11.0 -python-rsdclient==1.0.1 -python-saharaclient==1.4.0 -python-searchlightclient==1.0.0 -python-senlinclient==1.1.0 -python-subunit==1.0.0 -python-swiftclient==3.2.0 -python-troveclient==3.1.0 -python-watcherclient==2.5.0 -python-zaqarclient==1.0.0 -python-zunclient==3.6.0 -pytz==2013.6 -PyYAML==3.13 -repoze.lru==0.7 -requests-mock==1.2.0 -requests==2.14.2 -requestsexceptions==1.2.0 -rfc3986==0.3.1 -Routes==2.3.1 -rsd-lib==0.1.0 -simplejson==3.5.1 -six==1.10.0 -smmap==0.9.0 -statsd==3.2.1 -stestr==1.0.0 -stevedore==2.0.1 -sushy==0.1.0 -tempest==17.1.0 -tenacity==3.2.1 -testrepository==0.0.18 -testtools==2.2.0 -traceback2==1.4.0 -ujson==1.35 -unittest2==1.1.0 -urllib3==1.21.1 -validictory==1.1.1 -vine==1.1.4 -warlock==1.2.0 -WebOb==1.7.1 -websocket-client==0.44.0 -wrapt==1.7.0 -yaql==1.1.3 diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1d1fc741..f4d49a74 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1337,15 +1337,19 @@ class ListServer(command.Lister): # flavor name is given, map it to ID. flavor_id = None if parsed_args.flavor: - flavor_id = utils.find_resource(compute_client.flavors, - parsed_args.flavor).id + flavor_id = utils.find_resource( + compute_client.flavors, + parsed_args.flavor, + ).id # Nova only supports list servers searching by image ID. So if a # image name is given, map it to ID. image_id = None if parsed_args.image: - image_id = image_client.find_image(parsed_args.image, - ignore_missing=False).id + image_id = image_client.find_image( + parsed_args.image, + ignore_missing=False, + ).id search_opts = { 'reservation_id': parsed_args.reservation_id, @@ -1395,76 +1399,85 @@ class ListServer(command.Lister): try: timeutils.parse_isotime(search_opts['changes-since']) except ValueError: + msg = _('Invalid changes-since value: %s') raise exceptions.CommandError( - _('Invalid changes-since value: %s') % - search_opts['changes-since'] + msg % search_opts['changes-since'] ) + columns = ( + 'id', + 'name', + 'status', + ) + column_headers = ( + 'ID', + 'Name', + 'Status', + ) + if parsed_args.long: - columns = ( - 'ID', - 'Name', - 'Status', + columns += ( 'OS-EXT-STS:task_state', 'OS-EXT-STS:power_state', - 'Networks', - 'Image Name', - 'Image ID', - 'Flavor Name', - 'Flavor ID', - 'OS-EXT-AZ:availability_zone', - 'OS-EXT-SRV-ATTR:host', - 'Metadata', ) - column_headers = ( - 'ID', - 'Name', - 'Status', + column_headers += ( 'Task State', 'Power State', - 'Networks', + ) + + columns += ('networks',) + column_headers += ('Networks',) + + if parsed_args.long: + columns += ( + 'image_name', + 'image_id', + ) + column_headers += ( 'Image Name', 'Image ID', - 'Flavor Name', - 'Flavor ID', - 'Availability Zone', - 'Host', - 'Properties', ) - mixed_case_fields = [ - 'OS-EXT-STS:task_state', - 'OS-EXT-STS:power_state', - 'OS-EXT-AZ:availability_zone', - 'OS-EXT-SRV-ATTR:host', - ] else: if parsed_args.no_name_lookup: - columns = ( - 'ID', - 'Name', - 'Status', - 'Networks', - 'Image ID', - 'Flavor ID', - ) + columns += ('image_id',) else: - columns = ( - 'ID', - 'Name', - 'Status', - 'Networks', - 'Image Name', + columns += ('image_name',) + column_headers += ('Image',) + + # microversion 2.47 puts the embedded flavor into the server response + # body but omits the id, so if not present we just expose the original + # flavor name in the output + if compute_client.api_version >= api_versions.APIVersion('2.47'): + columns += ('flavor_name',) + column_headers += ('Flavor',) + else: + if parsed_args.long: + columns += ( + 'flavor_name', + 'flavor_id', + ) + column_headers += ( 'Flavor Name', + 'Flavor ID', ) - column_headers = ( - 'ID', - 'Name', - 'Status', - 'Networks', - 'Image', - 'Flavor', + else: + if parsed_args.no_name_lookup: + columns += ('flavor_id',) + else: + columns += ('flavor_name',) + column_headers += ('Flavor',) + + if parsed_args.long: + columns += ( + 'OS-EXT-AZ:availability_zone', + 'OS-EXT-SRV-ATTR:host', + 'metadata', + ) + column_headers += ( + 'Availability Zone', + 'Host', + 'Properties', ) - mixed_case_fields = [] marker_id = None @@ -1476,25 +1489,29 @@ class ListServer(command.Lister): if parsed_args.deleted: marker_id = parsed_args.marker else: - marker_id = utils.find_resource(compute_client.servers, - parsed_args.marker).id + marker_id = utils.find_resource( + compute_client.servers, + parsed_args.marker, + ).id - data = compute_client.servers.list(search_opts=search_opts, - marker=marker_id, - limit=parsed_args.limit) + data = compute_client.servers.list( + search_opts=search_opts, + marker=marker_id, + limit=parsed_args.limit) images = {} flavors = {} if data and not parsed_args.no_name_lookup: - # Create a dict that maps image_id to image object. - # Needed so that we can display the "Image Name" column. - # "Image Name" is not crucial, so we swallow any exceptions. - # The 'image' attribute can be an empty string if the server was - # booted from a volume. + # create a dict that maps image_id to image object, which is used + # to display the "Image Name" column. Note that 'image.id' can be + # empty for BFV instances and 'image' can be missing entirely if + # there are infra failures if parsed_args.name_lookup_one_by_one or image_id: - for i_id in set(filter(lambda x: x is not None, - (s.image.get('id') for s in data - if s.image))): + for i_id in set( + s.image['id'] for s in data + if s.image and s.image.get('id') + ): + # "Image Name" is not crucial, so we swallow any exceptions try: images[i_id] = image_client.get_image(i_id) except Exception: @@ -1507,12 +1524,17 @@ class ListServer(command.Lister): except Exception: pass - # Create a dict that maps flavor_id to flavor object. - # Needed so that we can display the "Flavor Name" column. - # "Flavor Name" is not crucial, so we swallow any exceptions. + # create a dict that maps flavor_id to flavor object, which is used + # to display the "Flavor Name" column. Note that 'flavor.id' is not + # present on microversion 2.47 or later and 'flavor' won't be + # present if there are infra failures if parsed_args.name_lookup_one_by_one or flavor_id: - for f_id in set(filter(lambda x: x is not None, - (s.flavor.get('id') for s in data))): + for f_id in set( + s.flavor['id'] for s in data + if s.flavor and s.flavor.get('id') + ): + # "Flavor Name" is not crucial, so we swallow any + # exceptions try: flavors[f_id] = compute_client.flavors.get(f_id) except Exception: @@ -1536,6 +1558,7 @@ class ListServer(command.Lister): # processing of the image and flavor informations. if not hasattr(s, 'image') or not hasattr(s, 'flavor'): continue + if 'id' in s.image: image = images.get(s.image['id']) if image: @@ -1548,28 +1571,29 @@ class ListServer(command.Lister): # 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: + + if compute_client.api_version < api_versions.APIVersion('2.47'): flavor = flavors.get(s.flavor['id']) if flavor: s.flavor_name = flavor.name s.flavor_id = s.flavor['id'] else: - # TODO(mriedem): Fix this for microversion >= 2.47 where the - # flavor is embedded in the server response without the id. - # We likely need to drop the Flavor ID column in that case if - # --long is specified. - s.flavor_name = '' - s.flavor_id = '' + s.flavor_name = s.flavor['original_name'] table = (column_headers, (utils.get_item_properties( s, columns, - mixed_case_fields=mixed_case_fields, + mixed_case_fields=( + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:power_state', + 'OS-EXT-AZ:availability_zone', + 'OS-EXT-SRV-ATTR:host', + ), formatters={ 'OS-EXT-STS:power_state': _format_servers_list_power_state, - 'Networks': _format_servers_list_networks, - 'Metadata': utils.format_dict, + 'networks': _format_servers_list_networks, + 'metadata': utils.format_dict, }, ) for s in data)) return table diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index 1af6e28d..35f537db 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -56,12 +56,18 @@ class CreateServerGroup(command.ShowOne): parser.add_argument( '--policy', metavar='<policy>', + choices=[ + 'affinity', + 'anti-affinity', + 'soft-affinity', + 'soft-anti-affinity', + ], default='affinity', - help=_("Add a policy to <name> " - "('affinity' or 'anti-affinity', " - "defaults to 'affinity'). Specify --os-compute-api-version " - "2.15 or higher for the 'soft-affinity' or " - "'soft-anti-affinity' policy.") + help=_( + "Add a policy to <name> " + "Specify --os-compute-api-version 2.15 or higher for the " + "'soft-affinity' or 'soft-anti-affinity' policy." + ) ) return parser @@ -69,9 +75,18 @@ class CreateServerGroup(command.ShowOne): compute_client = self.app.client_manager.compute info = {} + if parsed_args.policy in ('soft-affinity', 'soft-anti-affinity'): + if compute_client.api_version < api_versions.APIVersion('2.15'): + msg = _( + '--os-compute-api-version 2.15 or greater is required to ' + 'support the %s policy' + ) + raise exceptions.CommandError(msg % parsed_args.policy) + 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, **policy_arg) @@ -135,11 +150,47 @@ class ListServerGroup(command.Lister): default=False, help=_("List additional fields in output") ) + # TODO(stephenfin): This should really be a --marker option, but alas + # the API doesn't support that for some reason + parser.add_argument( + '--offset', + metavar='<offset>', + type=int, + default=None, + help=_( + 'Index from which to start listing servers. This should ' + 'typically be a factor of --limit. Display all servers groups ' + 'if not specified.' + ), + ) + parser.add_argument( + '--limit', + metavar='<limit>', + type=int, + default=None, + help=_( + "Maximum number of server groups to display. " + "If limit is greater than 'osapi_max_limit' option of Nova " + "API, 'osapi_max_limit' will be used instead." + ), + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - data = compute_client.server_groups.list(parsed_args.all_projects) + + kwargs = {} + + if parsed_args.all_projects: + kwargs['all_projects'] = parsed_args.all_projects + + if parsed_args.offset: + kwargs['offset'] = parsed_args.offset + + if parsed_args.limit: + kwargs['limit'] = parsed_args.limit + + data = compute_client.server_groups.list(**kwargs) policy_key = 'Policies' if compute_client.api_version >= api_versions.APIVersion("2.64"): diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index cf1d6817..64aa3fcd 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -478,7 +478,11 @@ class SaveImage(command.Command): image_client = self.app.client_manager.image image = image_client.find_image(parsed_args.image) - image_client.download_image(image.id, output=parsed_args.file) + output_file = parsed_args.file + if output_file is None: + output_file = getattr(sys.stdout, "buffer", sys.stdout) + + image_client.download_image(image.id, stream=True, output=output_file) class SetImage(command.Command): diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 029f57a3..2836e764 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -803,7 +803,11 @@ class SaveImage(command.Command): image_client = self.app.client_manager.image image = image_client.find_image(parsed_args.image) - image_client.download_image(image.id, output=parsed_args.file) + output_file = parsed_args.file + if output_file is None: + output_file = getattr(sys.stdout, "buffer", sys.stdout) + + image_client.download_image(image.id, stream=True, output=output_file) class SetImage(command.Command): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 02bb406c..af589228 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2557,7 +2557,7 @@ class TestServerDumpCreate(TestServer): self.run_method_with_servers('trigger_crash_dump', 3) -class TestServerList(TestServer): +class _TestServerList(TestServer): # Columns to be listed up. columns = ( @@ -2585,7 +2585,7 @@ class TestServerList(TestServer): ) def setUp(self): - super(TestServerList, self).setUp() + super(_TestServerList, self).setUp() self.search_opts = { 'reservation_id': None, @@ -2643,10 +2643,11 @@ class TestServerList(TestServer): # Get the command object to test self.cmd = server.ListServer(self.app, None) - # Prepare data returned by fake Nova API. - self.data = [] - self.data_long = [] - self.data_no_name_lookup = [] + +class TestServerList(_TestServerList): + + def setUp(self): + super(TestServerList, self).setUp() Image = collections.namedtuple('Image', 'id name') self.images_mock.return_value = [ @@ -2661,8 +2662,8 @@ class TestServerList(TestServer): for s in self.servers ] - for s in self.servers: - self.data.append(( + self.data = tuple( + ( s.id, s.name, s.status, @@ -2670,34 +2671,8 @@ class TestServerList(TestServer): # Image will be an empty string if boot-from-volume self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, self.flavor.name, - )) - self.data_long.append(( - s.id, - s.name, - s.status, - getattr(s, 'OS-EXT-STS:task_state'), - server._format_servers_list_power_state( - getattr(s, 'OS-EXT-STS:power_state') - ), - server._format_servers_list_networks(s.networks), - # Image will be an empty string if boot-from-volume - 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'), - getattr(s, 'OS-EXT-SRV-ATTR:host'), - s.Metadata, - )) - self.data_no_name_lookup.append(( - s.id, - s.name, - 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 server.IMAGE_STRING_FOR_BFV, - s.flavor['id'] - )) + ) for s in self.servers + ) def test_server_list_no_option(self): arglist = [] @@ -2718,7 +2693,7 @@ class TestServerList(TestServer): self.assertFalse(self.flavors_mock.get.call_count) self.assertFalse(self.get_image_mock.call_count) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_no_servers(self): arglist = [] @@ -2737,9 +2712,28 @@ class TestServerList(TestServer): self.assertEqual(0, self.images_mock.list.call_count) self.assertEqual(0, self.flavors_mock.list.call_count) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_long_option(self): + self.data = tuple( + ( + s.id, + s.name, + s.status, + getattr(s, 'OS-EXT-STS:task_state'), + server._format_servers_list_power_state( + getattr(s, 'OS-EXT-STS:power_state') + ), + server._format_servers_list_networks(s.networks), + # Image will be an empty string if boot-from-volume + 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'), + getattr(s, 'OS-EXT-SRV-ATTR:host'), + s.Metadata, + ) for s in self.servers) arglist = [ '--long', ] @@ -2750,12 +2744,23 @@ class TestServerList(TestServer): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns_long, columns) - self.assertEqual(tuple(self.data_long), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_no_name_lookup_option(self): + self.data = tuple( + ( + s.id, + s.name, + 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 server.IMAGE_STRING_FOR_BFV, + s.flavor['id'] + ) for s in self.servers + ) + arglist = [ '--no-name-lookup', ] @@ -2769,9 +2774,21 @@ class TestServerList(TestServer): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data_no_name_lookup), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_n_option(self): + self.data = tuple( + ( + s.id, + s.name, + 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 server.IMAGE_STRING_FOR_BFV, + s.flavor['id'] + ) for s in self.servers + ) + arglist = [ '-n', ] @@ -2785,7 +2802,7 @@ class TestServerList(TestServer): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data_no_name_lookup), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_name_lookup_one_by_one(self): arglist = [ @@ -2807,7 +2824,7 @@ class TestServerList(TestServer): self.flavors_mock.get.assert_called() self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_with_image(self): @@ -2828,145 +2845,213 @@ class TestServerList(TestServer): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) - def test_server_list_with_locked_pre_v273(self): + def test_server_list_with_flavor(self): arglist = [ - '--locked' + '--flavor', self.flavor.id ] verifylist = [ - ('locked', True) + ('flavor', self.flavor.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - ex = self.assertRaises(exceptions.CommandError, - self.cmd.take_action, - parsed_args) - self.assertIn( - '--os-compute-api-version 2.73 or greater is required', str(ex)) + columns, data = self.cmd.take_action(parsed_args) - def test_server_list_with_locked_v273(self): + self.flavors_mock.get.has_calls(self.flavor.id) + + self.search_opts['flavor'] = self.flavor.id + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, tuple(data)) + + def test_server_list_with_changes_since(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.73') arglist = [ - '--locked' + '--changes-since', '2016-03-04T06:27:59Z', + '--deleted' ] verifylist = [ - ('locked', True) + ('changes_since', '2016-03-04T06:27:59Z'), + ('deleted', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.search_opts['locked'] = True + self.search_opts['changes-since'] = '2016-03-04T06:27:59Z' + self.search_opts['deleted'] = True self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) - def test_server_list_with_unlocked_v273(self): + @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError) + def test_server_list_with_invalid_changes_since(self, mock_parse_isotime): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.73') arglist = [ - '--unlocked' + '--changes-since', 'Invalid time value', ] verifylist = [ - ('unlocked', True) + ('changes_since', 'Invalid time value'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('Invalid changes-since value: Invalid time ' + 'value', str(e)) + mock_parse_isotime.assert_called_once_with( + 'Invalid time value' + ) - self.search_opts['locked'] = False - self.servers_mock.list.assert_called_with(**self.kwargs) - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) +class TestServerListV273(_TestServerList): + + # Columns to be listed up. + columns = ( + 'ID', + 'Name', + 'Status', + 'Networks', + 'Image', + 'Flavor', + ) + columns_long = ( + 'ID', + 'Name', + 'Status', + 'Task State', + 'Power State', + 'Networks', + 'Image Name', + 'Image ID', + 'Flavor', + 'Availability Zone', + 'Host', + 'Properties', + ) - def test_server_list_with_locked_and_unlocked_v273(self): + def setUp(self): + super(TestServerListV273, self).setUp() + + # The fake servers' attributes. Use the original attributes names in + # nova, not the ones printed by "server list" command. + self.attrs['flavor'] = { + 'vcpus': self.flavor.vcpus, + 'ram': self.flavor.ram, + 'disk': self.flavor.disk, + 'ephemeral': self.flavor.ephemeral, + 'swap': self.flavor.swap, + 'original_name': self.flavor.name, + 'extra_specs': self.flavor.properties, + } + + # The servers to be listed. + self.servers = self.setup_servers_mock(3) + self.servers_mock.list.return_value = self.servers + + Image = collections.namedtuple('Image', 'id name') + self.images_mock.return_value = [ + Image(id=s.image['id'], name=self.image.name) + # Image will be an empty string if boot-from-volume + for s in self.servers if s.image + ] + + # The flavor information is embedded, so now reason for this to be + # called + self.flavors_mock.list = mock.NonCallableMock() + + self.data = tuple( + ( + s.id, + s.name, + 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 server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + ) for s in self.servers) + + def test_server_list_with_locked_pre_v273(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.73') arglist = [ - '--locked', - '--unlocked' + '--locked' ] verifylist = [ - ('locked', True), - ('unlocked', True) + ('locked', True) ] - ex = self.assertRaises( - utils.ParserException, - self.check_parser, self.cmd, arglist, verifylist) - self.assertIn('Argument parse failed', str(ex)) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.73 or greater is required', str(ex)) - def test_server_list_with_flavor(self): + def test_server_list_with_locked(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') arglist = [ - '--flavor', self.flavor.id + '--locked' ] verifylist = [ - ('flavor', self.flavor.id) + ('locked', True) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.flavors_mock.get.has_calls(self.flavor.id) - - self.search_opts['flavor'] = self.flavor.id + self.search_opts['locked'] = True self.servers_mock.list.assert_called_with(**self.kwargs) - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertItemsEqual(self.columns, columns) + self.assertItemsEqual(self.data, tuple(data)) - def test_server_list_with_changes_since(self): + def test_server_list_with_unlocked_v273(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') arglist = [ - '--changes-since', '2016-03-04T06:27:59Z', - '--deleted' + '--unlocked' ] verifylist = [ - ('changes_since', '2016-03-04T06:27:59Z'), - ('deleted', True), + ('unlocked', True) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.search_opts['changes-since'] = '2016-03-04T06:27:59Z' - self.search_opts['deleted'] = True + self.search_opts['locked'] = False self.servers_mock.list.assert_called_with(**self.kwargs) - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertItemsEqual(self.columns, columns) + self.assertItemsEqual(self.data, tuple(data)) - @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError) - def test_server_list_with_invalid_changes_since(self, mock_parse_isotime): + def test_server_list_with_locked_and_unlocked(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') arglist = [ - '--changes-since', 'Invalid time value', + '--locked', + '--unlocked' ] verifylist = [ - ('changes_since', 'Invalid time value'), + ('locked', True), + ('unlocked', True) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('Invalid changes-since value: Invalid time ' - 'value', str(e)) - mock_parse_isotime.assert_called_once_with( - 'Invalid time value' - ) + ex = self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + self.assertIn('Argument parse failed', str(ex)) - def test_server_list_v266_with_changes_before(self): + def test_server_list_with_changes_before(self): self.app.client_manager.compute.api_version = ( api_versions.APIVersion('2.66')) arglist = [ @@ -2983,13 +3068,14 @@ class TestServerList(TestServer): self.search_opts['changes-before'] = '2016-03-05T06:27:59Z' self.search_opts['deleted'] = True + self.servers_mock.list.assert_called_with(**self.kwargs) - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertItemsEqual(self.columns, columns) + self.assertItemsEqual(self.data, tuple(data)) @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError) - def test_server_list_v266_with_invalid_changes_before( + def test_server_list_with_invalid_changes_before( self, mock_parse_isotime): self.app.client_manager.compute.api_version = ( api_versions.APIVersion('2.66')) diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index 359cd2bd..50d2a57e 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -91,6 +91,28 @@ class TestServerGroupCreate(TestServerGroup): def test_server_group_create(self): arglist = [ + '--policy', 'anti-affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', '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, + policies=[parsed_args.policy], + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_server_group_create_with_soft_policies(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.15') + + arglist = [ '--policy', 'soft-anti-affinity', 'affinity_group', ] @@ -108,6 +130,27 @@ class TestServerGroupCreate(TestServerGroup): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_server_group_create_with_soft_policies_pre_v215(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.14') + + arglist = [ + '--policy', 'soft-anti-affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', 'soft-anti-affinity'), + ('name', 'affinity_group'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.15 or greater is required', + str(ex)) + def test_server_group_create_v264(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.64') @@ -255,10 +298,13 @@ class TestServerGroupList(TestServerGroup): verifylist = [ ('all_projects', False), ('long', False), + ('limit', None), + ('offset', None), ] 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.server_groups_mock.list.assert_called_once_with() self.assertEqual(self.list_columns, columns) self.assertEqual(self.list_data, tuple(data)) @@ -271,14 +317,49 @@ class TestServerGroupList(TestServerGroup): verifylist = [ ('all_projects', True), ('long', True), + ('limit', None), + ('offset', None), ] 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.server_groups_mock.list.assert_called_once_with( + all_projects=True) self.assertEqual(self.list_columns_long, columns) self.assertEqual(self.list_data_long, tuple(data)) + def test_server_group_list_with_limit(self): + arglist = [ + '--limit', '1', + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('limit', 1), + ('offset', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.server_groups_mock.list.assert_called_once_with(limit=1) + + def test_server_group_list_with_offset(self): + arglist = [ + '--offset', '5', + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('limit', None), + ('offset', 5), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.server_groups_mock.list.assert_called_once_with(offset=5) + class TestServerGroupListV264(TestServerGroupV264): @@ -328,7 +409,7 @@ class TestServerGroupListV264(TestServerGroupV264): ] 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.server_groups_mock.list.assert_called_once_with() self.assertEqual(self.list_columns, columns) self.assertEqual(self.list_data, tuple(data)) @@ -344,7 +425,8 @@ class TestServerGroupListV264(TestServerGroupV264): ] 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.server_groups_mock.list.assert_called_once_with( + all_projects=True) self.assertEqual(self.list_columns_long, columns) self.assertEqual(self.list_data_long, tuple(data)) diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 310f6b76..a7f60398 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -1614,7 +1614,7 @@ class TestImageSave(TestImage): verifylist = [ ('file', '/path/to/file'), - ('image', self.image.id) + ('image', self.image.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1622,6 +1622,7 @@ class TestImageSave(TestImage): self.client.download_image.assert_called_once_with( self.image.id, + stream=True, output='/path/to/file') diff --git a/releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml b/releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml new file mode 100644 index 00000000..b359e77c --- /dev/null +++ b/releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--limit`` and ``--offset`` options to ``server group list`` command, + to configure pagination of results. diff --git a/releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml b/releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml new file mode 100644 index 00000000..fdb37bbb --- /dev/null +++ b/releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add support for compute API microversion 2.47, which changes how flavor + details are included in server detail responses. In 2.46 and below, + only the flavor ID was shown in the server detail response. Starting in + 2.47, flavor information is embedded in the server response. The newer + behavior is now supported. diff --git a/releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml b/releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml new file mode 100644 index 00000000..857f3c50 --- /dev/null +++ b/releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - Stream image download to avoid buffering data in memory which rapidly + exhausts memory resulting in OOM kill or system crash for all but the + smallest of images. Fixes https://storyboard.openstack.org/#!/story/2007672 + - Restore default behavior of 'openstack image save' to send data to stdout + Relates to https://storyboard.openstack.org/#!/story/2007672.
\ No newline at end of file diff --git a/releasenotes/notes/story-2007727-69b705c561309742.yaml b/releasenotes/notes/story-2007727-69b705c561309742.yaml new file mode 100644 index 00000000..4a9eeae3 --- /dev/null +++ b/releasenotes/notes/story-2007727-69b705c561309742.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + The ``openstack server group create`` command will now validate the policy + value requested with ``--policy``, restricting it to the valid values + allowed by given microversions. diff --git a/requirements.txt b/requirements.txt index 2b7976e5..b8fa148f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ +# Requirements lower bounds listed here are our best effort to keep them up to +# date but we do not test them so no guarantee of having them all correct. If +# you find any incorrect lower bounds, let us know or propose a fix. + # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. @@ -138,9 +138,3 @@ exclude = .git,.tox,dist,doc,*lib/python*,*egg,build,tools ignore = W504 import-order-style = pep8 application_import_names = openstackclient - -[testenv:lower-constraints] -deps = - -c{toxinidir}/lower-constraints.txt - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt |