diff --git a/glanceclient/shell.py b/glanceclient/shell.py index 82195c8c..d52d9b72 100755 --- a/glanceclient/shell.py +++ b/glanceclient/shell.py @@ -22,6 +22,7 @@ from __future__ import print_function import argparse import copy import getpass +import hashlib import json import logging import os @@ -263,7 +264,7 @@ class OpenStackImagesShell(object): parser.add_argument('--os-image-api-version', default=utils.env('OS_IMAGE_API_VERSION', default=None), - help='Defaults to env[OS_IMAGE_API_VERSION] or 1.') + help='Defaults to env[OS_IMAGE_API_VERSION] or 2.') parser.add_argument('--os_image_api_version', help=argparse.SUPPRESS) @@ -551,9 +552,13 @@ class OpenStackImagesShell(object): def _cache_schemas(self, options, home_dir='~/.glanceclient'): homedir = os.path.expanduser(home_dir) - if not os.path.exists(homedir): + path_prefix = homedir + if options.os_auth_url: + hash_host = hashlib.sha1(options.os_auth_url.encode('utf-8')) + path_prefix = os.path.join(path_prefix, hash_host.hexdigest()) + if not os.path.exists(path_prefix): try: - os.makedirs(homedir) + os.makedirs(path_prefix) except OSError as e: # This avoids glanceclient to crash if it can't write to # ~/.glanceclient, which may happen on some env (for me, @@ -561,12 +566,12 @@ class OpenStackImagesShell(object): # /var/lib/jenkins). msg = '%s' % e print(encodeutils.safe_decode(msg), file=sys.stderr) - resources = ['image', 'metadefs/namespace', 'metadefs/resource_type'] - schema_file_paths = [homedir + os.sep + x + '_schema.json' + schema_file_paths = [os.path.join(path_prefix, x + '_schema.json') for x in ['image', 'namespace', 'resource_type']] client = None + failed_download_schema = 0 for resource, schema_file_path in zip(resources, schema_file_paths): if (not os.path.exists(schema_file_path)) or options.get_schema: try: @@ -580,8 +585,11 @@ class OpenStackImagesShell(object): except Exception: # NOTE(esheffield) do nothing here, we'll get a message # later if the schema is missing + failed_download_schema += 1 pass + return failed_download_schema >= len(resources) + def main(self, argv): # Parse args once to find version @@ -605,7 +613,7 @@ class OpenStackImagesShell(object): # build available subcommands based on version try: - api_version = int(options.os_image_api_version or url_version or 1) + api_version = int(options.os_image_api_version or url_version or 2) if api_version not in SUPPORTED_VERSIONS: raise ValueError except ValueError: @@ -614,7 +622,12 @@ class OpenStackImagesShell(object): utils.exit(msg=msg) if api_version == 2: - self._cache_schemas(options) + switch_version = self._cache_schemas(options) + if switch_version: + print('WARNING: The client is falling back to v1 because' + ' the accessing to v2 failed. This behavior will' + ' be removed in future versions') + api_version = 1 try: subcommand_parser = self.get_subcommand_parser(api_version) diff --git a/glanceclient/tests/functional/test_readonly_glance.py b/glanceclient/tests/functional/test_readonly_glance.py index e3f09666..821fe5b8 100644 --- a/glanceclient/tests/functional/test_readonly_glance.py +++ b/glanceclient/tests/functional/test_readonly_glance.py @@ -24,26 +24,61 @@ class SimpleReadOnlyGlanceClientTest(base.ClientTestBase): This only exercises client commands that are read only. """ - def test_list(self): - out = self.glance('image-list') + def test_list_v1(self): + out = self.glance('--os-image-api-version 1 image-list') endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, [ 'ID', 'Name', 'Disk Format', 'Container Format', 'Size', 'Status']) + def test_list_v2(self): + out = self.glance('--os-image-api-version 2 image-list') + endpoints = self.parser.listing(out) + self.assertTableStruct(endpoints, ['ID', 'Name']) + def test_fake_action(self): self.assertRaises(exceptions.CommandFailed, self.glance, 'this-does-not-exist') - def test_member_list(self): + def test_member_list_v1(self): tenant_name = '--tenant-id %s' % self.tenant_name - out = self.glance('member-list', + out = self.glance('--os-image-api-version 1 member-list', params=tenant_name) endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, ['Image ID', 'Member ID', 'Can Share']) + def test_member_list_v2(self): + try: + # NOTE(flwang): If set disk-format and container-format, Jenkins + # will raise an error said can't recognize the params, thouhg it + # works fine at local. Without the two params, Glance will + # complain. So we just catch the exception can skip it. + self.glance('--os-image-api-version 2 image-create --name temp') + except Exception: + pass + out = self.glance('--os-image-api-version 2 image-list' + ' --visibility private') + image_list = self.parser.listing(out) + # NOTE(flwang): Because the member-list command of v2 is using + # image-id as required parameter, so we have to get a valid image id + # based on current environment. If there is no valid image id, we will + # pass in a fake one and expect a 404 error. + if len(image_list) > 0: + param_image_id = '--image-id %s' % image_list[0]['ID'] + out = self.glance('--os-image-api-version 2 member-list', + params=param_image_id) + endpoints = self.parser.listing(out) + self.assertTableStruct(endpoints, + ['Image ID', 'Member ID', 'Status']) + else: + param_image_id = '--image-id fake_image_id' + self.assertRaises(exceptions.CommandFailed, + self.glance, + '--os-image-api-version 2 member-list', + params=param_image_id) + def test_help(self): help_text = self.glance('help') lines = help_text.split('\n') diff --git a/glanceclient/tests/unit/test_shell.py b/glanceclient/tests/unit/test_shell.py index a38608e0..f0dfb67c 100644 --- a/glanceclient/tests/unit/test_shell.py +++ b/glanceclient/tests/unit/test_shell.py @@ -19,6 +19,7 @@ try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict +import hashlib import os import sys import uuid @@ -204,8 +205,8 @@ class ShellTest(testutils.TestCase): def test_no_auth_with_token_and_image_url_with_v1(self, v1_client): # test no authentication is required if both token and endpoint url # are specified - args = ('--os-auth-token mytoken --os-image-url https://image:1234/v1 ' - 'image-list') + args = ('--os-image-api-version 1 --os-auth-token mytoken' + ' --os-image-url https://image:1234/v1 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) assert v1_client.called @@ -213,7 +214,8 @@ class ShellTest(testutils.TestCase): self.assertEqual('mytoken', kwargs['token']) self.assertEqual('https://image:1234', args[0]) - @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas') + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', + return_value=False) def test_no_auth_with_token_and_image_url_with_v2(self, cache_schemas): with mock.patch('glanceclient.v2.client.Client') as v2_client: @@ -244,13 +246,14 @@ class ShellTest(testutils.TestCase): @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_v1(self, v1_client): - args = 'image-list' + args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self._assert_auth_plugin_args() @mock.patch('glanceclient.v2.client.Client') - @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas') + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', + return_value=False) def test_auth_plugin_invocation_with_v2(self, v2_client, cache_schemas): @@ -262,13 +265,15 @@ class ShellTest(testutils.TestCase): @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_unversioned_auth_url_with_v1( self, v1_client): - args = '--os-auth-url %s image-list' % DEFAULT_UNVERSIONED_AUTH_URL + args = ('--os-image-api-version 1 --os-auth-url %s image-list' % + DEFAULT_UNVERSIONED_AUTH_URL) glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self._assert_auth_plugin_args() @mock.patch('glanceclient.v2.client.Client') - @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas') + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', + return_value=False) def test_auth_plugin_invocation_with_unversioned_auth_url_with_v2( self, v2_client, cache_schemas): args = ('--os-auth-url %s --os-image-api-version 2 ' @@ -300,7 +305,8 @@ class ShellTest(testutils.TestCase): @mock.patch( 'glanceclient.shell.OpenStackImagesShell._get_keystone_session') - @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas') + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', + return_value=False) def test_no_auth_with_proj_name(self, cache_schemas, session): with mock.patch('glanceclient.v2.client.Client'): args = ('--os-project-name myname ' @@ -414,7 +420,10 @@ class ShellTest(testutils.TestCase): self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v2.client.Client') - def test_auth_plugin_invocation_without_tenant_with_v2(self, v2_client): + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', + return_value=False) + def test_auth_plugin_invocation_without_tenant_with_v2(self, v2_client, + cache_schemas): if 'OS_TENANT_NAME' in os.environ: self.make_env(exclude='OS_TENANT_NAME') if 'OS_PROJECT_ID' in os.environ: @@ -445,13 +454,14 @@ class ShellTestWithKeystoneV3Auth(ShellTest): @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_v1(self, v1_client): - args = 'image-list' + args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self._assert_auth_plugin_args() @mock.patch('glanceclient.v2.client.Client') - @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas') + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', + return_value=False) def test_auth_plugin_invocation_with_v2(self, v2_client, cache_schemas): args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() @@ -489,9 +499,12 @@ class ShellCacheSchemaTest(testutils.TestCase): self._mock_client_setup() self._mock_shell_setup() self.cache_dir = '/dir_for_cached_schema' - self.cache_files = [self.cache_dir + '/image_schema.json', - self.cache_dir + '/namespace_schema.json', - self.cache_dir + '/resource_type_schema.json'] + self.os_auth_url = 'http://localhost:5000/v2' + url_hex = hashlib.sha1(self.os_auth_url.encode('utf-8')).hexdigest() + self.prefix_path = (self.cache_dir + '/' + url_hex) + self.cache_files = [self.prefix_path + '/image_schema.json', + self.prefix_path + '/namespace_schema.json', + self.prefix_path + '/resource_type_schema.json'] def tearDown(self): super(ShellCacheSchemaTest, self).tearDown() @@ -524,7 +537,8 @@ class ShellCacheSchemaTest(testutils.TestCase): @mock.patch('os.path.exists', return_value=True) def test_cache_schemas_gets_when_forced(self, exists_mock): options = { - 'get_schema': True + 'get_schema': True, + 'os_auth_url': self.os_auth_url } schema_odict = OrderedDict(self.schema_dict) @@ -545,7 +559,8 @@ class ShellCacheSchemaTest(testutils.TestCase): @mock.patch('os.path.exists', side_effect=[True, False, False, False]) def test_cache_schemas_gets_when_not_exists(self, exists_mock): options = { - 'get_schema': False + 'get_schema': False, + 'os_auth_url': self.os_auth_url } schema_odict = OrderedDict(self.schema_dict) @@ -566,14 +581,29 @@ class ShellCacheSchemaTest(testutils.TestCase): @mock.patch('os.path.exists', return_value=True) def test_cache_schemas_leaves_when_present_not_forced(self, exists_mock): options = { - 'get_schema': False + 'get_schema': False, + 'os_auth_url': self.os_auth_url } self.shell._cache_schemas(self._make_args(options), home_dir=self.cache_dir) - os.path.exists.assert_any_call(self.cache_dir) + os.path.exists.assert_any_call(self.prefix_path) os.path.exists.assert_any_call(self.cache_files[0]) os.path.exists.assert_any_call(self.cache_files[1]) self.assertEqual(4, exists_mock.call_count) self.assertEqual(0, open.mock_calls.__len__()) + + @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True) + @mock.patch('os.path.exists', return_value=True) + def test_cache_schemas_leaves_auto_switch(self, exists_mock): + options = { + 'get_schema': True, + 'os_auth_url': self.os_auth_url + } + + self.client.schemas.get.return_value = Exception() + + switch_version = self.shell._cache_schemas(self._make_args(options), + home_dir=self.cache_dir) + self.assertEqual(switch_version, True)