diff --git a/cinderclient/api_versions.py b/cinderclient/api_versions.py index 2769e9648..3000aaa53 100644 --- a/cinderclient/api_versions.py +++ b/cinderclient/api_versions.py @@ -160,6 +160,9 @@ class APIVersion(object): return "%s.%s" % (self.ver_major, "latest") return "%s.%s" % (self.ver_major, self.ver_minor) + def get_major_version(self): + return "%s" % self.ver_major + class VersionedMethod(object): diff --git a/cinderclient/shell.py b/cinderclient/shell.py index ecc586298..75e42e908 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -516,6 +516,21 @@ class OpenStackCinderShell(object): else: return argv + @staticmethod + def _validate_input_api_version(options): + if not options.os_volume_api_version: + api_version = api_versions.APIVersion(api_versions.MAX_VERSION) + else: + api_version = api_versions.get_api_version( + options.os_volume_api_version) + return api_version + + @staticmethod + def downgrade_warning(requested, discovered): + logger.warning("API version %s requested, " % requested.get_string()) + logger.warning("downgrading to %s based on server support." % + discovered.get_string()) + def main(self, argv): # Parse args once to find version and debug settings parser = self.get_base_parser() @@ -527,14 +542,7 @@ class OpenStackCinderShell(object): do_help = ('help' in argv) or ( '--help' in argv) or ('-h' in argv) or not argv - if not options.os_volume_api_version: - use_version = DEFAULT_MAJOR_OS_VOLUME_API_VERSION - if do_help: - use_version = api_versions.MAX_VERSION - api_version = api_versions.get_api_version(use_version) - else: - api_version = api_versions.get_api_version( - options.os_volume_api_version) + api_version = self._validate_input_api_version(options) # build available subcommands based on version major_version_string = "%s" % api_version.ver_major @@ -670,9 +678,7 @@ class OpenStackCinderShell(object): insecure = self.options.insecure - self.cs = client.Client( - api_version, os_username, - os_password, os_project_name, os_auth_url, + client_args = dict( region_name=os_region_name, tenant_id=os_project_id, endpoint_type=endpoint_type, @@ -689,6 +695,11 @@ class OpenStackCinderShell(object): session=auth_session, logger=self.ks_logger if auth_session else self.client_logger) + self.cs = client.Client( + api_version, os_username, + os_password, os_project_name, os_auth_url, + **client_args) + try: if not utils.isunauthenticated(args.func): self.cs.authenticate() @@ -718,6 +729,28 @@ class OpenStackCinderShell(object): "to the default API version: %s", endpoint_api_version) + API_MAX_VERSION = api_versions.APIVersion(api_versions.MAX_VERSION) + if endpoint_api_version[0] == '3': + disc_client = client.Client(API_MAX_VERSION, + os_username, + os_password, + os_project_name, + os_auth_url, + **client_args) + self.cs, discovered_version = self._discover_client( + disc_client, + api_version, + args.os_endpoint_type, + args.service_type, + os_username, + os_password, + os_project_name, + os_auth_url, + client_args) + + if discovered_version < api_version: + self.downgrade_warning(api_version, discovered_version) + profile = osprofiler_profiler and options.profile if profile: osprofiler_profiler.init(options.profile) @@ -731,6 +764,56 @@ class OpenStackCinderShell(object): print("To display trace use next command:\n" "osprofiler trace show --html %s " % trace_id) + def _discover_client(self, + current_client, + os_api_version, + os_endpoint_type, + os_service_type, + os_username, + os_password, + os_project_name, + os_auth_url, + client_args): + + if (os_api_version.get_major_version() in + api_versions.DEPRECATED_VERSIONS): + discovered_version = api_versions.DEPRECATED_VERSION + os_service_type = 'volume' + else: + discovered_version = api_versions.discover_version( + current_client, + os_api_version) + + if not os_endpoint_type: + os_endpoint_type = DEFAULT_CINDER_ENDPOINT_TYPE + + if not os_service_type: + os_service_type = self._discover_service_type(discovered_version) + + API_MAX_VERSION = api_versions.APIVersion(api_versions.MAX_VERSION) + + if (discovered_version != API_MAX_VERSION or + os_service_type != 'volume' or + os_endpoint_type != DEFAULT_CINDER_ENDPOINT_TYPE): + client_args['service_type'] = os_service_type + client_args['endpoint_type'] = os_endpoint_type + + return (client.Client(discovered_version, + os_username, + os_password, + os_project_name, + os_auth_url, + **client_args), + discovered_version) + else: + return current_client, discovered_version + + def _discover_service_type(self, discovered_version): + SERVICE_TYPES = {'1': 'volume', '2': 'volumev2', '3': 'volumev3'} + major_version = discovered_version.get_major_version() + service_type = SERVICE_TYPES[major_version] + return service_type + def _run_extension_hooks(self, hook_type, *args, **kwargs): """Runs hooks for all registered extensions.""" for extension in self.extensions: diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py index cc3372ce5..f4aa59816 100644 --- a/cinderclient/tests/unit/v3/test_shell.py +++ b/cinderclient/tests/unit/v3/test_shell.py @@ -46,6 +46,7 @@ import six from six.moves.urllib import parse import cinderclient +from cinderclient import api_versions from cinderclient import base from cinderclient import client from cinderclient import exceptions @@ -92,7 +93,12 @@ class ShellTest(utils.TestCase): self.cs = mock.Mock() def run_command(self, cmd): - self.shell.main(cmd.split()) + # Ensure the version negotiation indicates that + # all versions are supported + with mock.patch('cinderclient.api_versions._get_server_version_range', + return_value=(api_versions.APIVersion('3.0'), + api_versions.APIVersion('3.99'))): + self.shell.main(cmd.split()) def assert_called(self, method, url, body=None, partial_body=None, **kwargs): @@ -295,6 +301,14 @@ class ShellTest(utils.TestCase): mock_print.assert_called_once_with(mock.ANY, key_list, exclude_unavailable=True, sortby_index=0) + @mock.patch("cinderclient.shell.OpenStackCinderShell.downgrade_warning") + def test_list_version_downgrade(self, mock_warning): + self.run_command('--os-volume-api-version 3.998 list') + mock_warning.assert_called_once_with( + api_versions.APIVersion('3.998'), + api_versions.APIVersion(api_versions.MAX_VERSION) + ) + def test_list_availability_zone(self): self.run_command('availability-zone-list') self.assert_called('GET', '/os-availability-zone') diff --git a/doc/source/user/shell.rst b/doc/source/user/shell.rst index 0d03b0156..50d8feb19 100644 --- a/doc/source/user/shell.rst +++ b/doc/source/user/shell.rst @@ -40,6 +40,14 @@ For example, in Bash you'd use:: export OS_AUTH_URL=http://auth.example.com:5000/v3 export OS_VOLUME_API_VERSION=3 +If OS_VOLUME_API_VERSION is not set, the highest version +supported by the server will be used. + +If OS_VOLUME_API_VERSION exceeds the highest version +supported by the server, the highest version supported by +both the client and server will be used. A warning +message is printed when this occurs. + From there, all shell commands take the form:: cinder <command> [arguments...] diff --git a/releasenotes/notes/cli-api-ver-negotiation-9f8fd8b77ae299fd.yaml b/releasenotes/notes/cli-api-ver-negotiation-9f8fd8b77ae299fd.yaml new file mode 100644 index 000000000..4501850d8 --- /dev/null +++ b/releasenotes/notes/cli-api-ver-negotiation-9f8fd8b77ae299fd.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Automatic version negotiation for the cinderclient CLI. + If an API version is not specified, the CLI will use the newest + supported by the client and the server. + If an API version newer than the server supports is requested, + the CLI will fall back to the newest version supported by the server + and issue a warning message. + This does not affect cinderclient library usage. + +