Merge "Add version discover and check in CLI"

This commit is contained in:
Jenkins 2015-08-07 13:33:19 +00:00 committed by Gerrit Code Review
commit 59abce6462
6 changed files with 343 additions and 39 deletions

@ -14,5 +14,10 @@
import pbr.version
from novaclient import api_versions
__version__ = pbr.version.VersionInfo('python-novaclient').version_string()
API_MIN_VERSION = api_versions.APIVersion("2.1")
API_MAX_VERSION = api_versions.APIVersion("2.1")

@ -19,6 +19,7 @@ import re
from oslo_utils import strutils
import novaclient
from novaclient import exceptions
from novaclient.i18n import _, _LW
from novaclient import utils
@ -229,6 +230,75 @@ def get_api_version(version_string):
return api_version
def _get_server_version_range(client):
version = client.versions.get_current()
if not hasattr(version, 'version') or not version.version:
return APIVersion(), APIVersion()
return APIVersion(version.min_version), APIVersion(version.version)
def discover_version(client, requested_version):
"""Returns latest version supported by both API and client.
:param client: client object
:returns: APIVersion
"""
server_start_version, server_end_version = _get_server_version_range(
client)
if (not requested_version.is_latest() and
requested_version != APIVersion('2.0')):
if server_start_version.is_null() and server_end_version.is_null():
raise exceptions.UnsupportedVersion(
_("Server doesn't support microversions"))
if not requested_version.matches(server_start_version,
server_end_version):
raise exceptions.UnsupportedVersion(
_("The specified version isn't supported by server. The valid "
"version range is '%(min)s' to '%(max)s'") % {
"min": server_start_version.get_string(),
"max": server_end_version.get_string()})
return requested_version
if requested_version == APIVersion('2.0'):
if (server_start_version == APIVersion('2.1') or
(server_start_version.is_null() and
server_end_version.is_null())):
return APIVersion('2.0')
else:
raise exceptions.UnsupportedVersion(
_("The server isn't backward compatible with Nova V2 REST "
"API"))
if server_start_version.is_null() and server_end_version.is_null():
return APIVersion('2.0')
elif novaclient.API_MIN_VERSION > server_end_version:
raise exceptions.UnsupportedVersion(
_("Server version is too old. The client valid version range is "
"'%(client_min)s' to '%(client_max)s'. The server valid version "
"range is '%(server_min)s' to '%(server_max)s'.") % {
'client_min': novaclient.API_MIN_VERSION.get_string(),
'client_max': novaclient.API_MAX_VERSION.get_string(),
'server_min': server_start_version.get_string(),
'server_max': server_end_version.get_string()})
elif novaclient.API_MAX_VERSION < server_start_version:
raise exceptions.UnsupportedVersion(
_("Server version is too new. The client valid version range is "
"'%(client_min)s' to '%(client_max)s'. The server valid version "
"range is '%(server_min)s' to '%(server_max)s'.") % {
'client_min': novaclient.API_MIN_VERSION.get_string(),
'client_max': novaclient.API_MAX_VERSION.get_string(),
'server_min': server_start_version.get_string(),
'server_max': server_end_version.get_string()})
elif novaclient.API_MAX_VERSION <= server_end_version:
return novaclient.API_MAX_VERSION
elif server_end_version < novaclient.API_MAX_VERSION:
return server_end_version
def update_headers(headers, api_version):
"""Set 'X-OpenStack-Nova-API-Version' header if api_version is not null"""

@ -403,8 +403,8 @@ class OpenStackComputeShell(object):
metavar='<compute-api-ver>',
default=cliutils.env('OS_COMPUTE_API_VERSION',
default=DEFAULT_OS_COMPUTE_API_VERSION),
help=_('Accepts X, X.Y (where X is major and Y is minor part), '
'defaults to env[OS_COMPUTE_API_VERSION].'))
help=_('Accepts X, X.Y (where X is major and Y is minor part) or '
'"X.latest", defaults to env[OS_COMPUTE_API_VERSION].'))
parser.add_argument(
'--os_compute_api_version',
help=argparse.SUPPRESS)
@ -547,18 +547,6 @@ class OpenStackComputeShell(object):
def main(self, argv):
# Parse args once to find version and debug settings
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self.setup_debugging(options.debug)
# Discover available auth plugins
novaclient.auth_plugin.discover_auth_systems()
api_version = api_versions.get_api_version(
options.os_compute_api_version)
# build available subcommands based on version
self.extensions = self._discover_extensions(api_version)
self._run_extension_hooks('__pre_parse_args__')
# NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse
# thinking usage-list --end is ambiguous; but it
@ -568,24 +556,17 @@ class OpenStackComputeShell(object):
spot = argv.index('--endpoint_type')
argv[spot] = '--endpoint-type'
subcommand_parser = self.get_subcommand_parser(
api_version, do_help=("help" in args))
self.parser = subcommand_parser
(args, args_list) = parser.parse_known_args(argv)
if options.help or not argv:
subcommand_parser.print_help()
return 0
self.setup_debugging(args.debug)
self.extensions = []
do_help = ('help' in argv) or not argv
args = subcommand_parser.parse_args(argv)
self._run_extension_hooks('__post_parse_args__', args)
# Discover available auth plugins
novaclient.auth_plugin.discover_auth_systems()
# Short-circuit and deal with help right away.
if args.func == self.do_help:
self.do_help(args)
return 0
elif args.func == self.do_bash_completion:
self.do_bash_completion(args)
return 0
api_version = api_versions.get_api_version(
args.os_compute_api_version)
os_username = args.os_username
os_user_id = args.os_user_id
@ -631,13 +612,13 @@ class OpenStackComputeShell(object):
endpoint_type += 'URL'
if not service_type:
service_type = (cliutils.get_service_type(args.func) or
DEFAULT_NOVA_SERVICE_TYPE)
# Note(alex_xu): We need discover version first, so if there isn't
# service type specified, we use default nova service type.
service_type = DEFAULT_NOVA_SERVICE_TYPE
# If we have an auth token but no management_url, we must auth anyway.
# Expired tokens are handled by client.py:_cs_request
must_auth = not (cliutils.isunauthenticated(args.func)
or (auth_token and management_url))
must_auth = not (auth_token and management_url)
# Do not use Keystone session for cases with no session support. The
# presence of auth_plugin means os_auth_system is present and is not
@ -648,7 +629,7 @@ class OpenStackComputeShell(object):
# FIXME(usrleon): Here should be restrict for project id same as
# for os_username or os_password but for compatibility it is not.
if must_auth:
if must_auth and not do_help:
if auth_plugin:
auth_plugin.parse_opts(args)
@ -702,8 +683,8 @@ class OpenStackComputeShell(object):
project_domain_id=args.os_project_domain_id,
project_domain_name=args.os_project_domain_name)
if not any([args.os_tenant_id, args.os_tenant_name,
args.os_project_id, args.os_project_name]):
if not do_help and not any([args.os_tenant_id, args.os_tenant_name,
args.os_project_id, args.os_project_name]):
raise exc.CommandError(_("You must provide a project name or"
" project id via --os-project-name,"
" --os-project-id, env[OS_PROJECT_ID]"
@ -711,11 +692,77 @@ class OpenStackComputeShell(object):
" use os-project and os-tenant"
" interchangeably."))
if not os_auth_url:
if not os_auth_url and not do_help:
raise exc.CommandError(
_("You must provide an auth url "
"via either --os-auth-url or env[OS_AUTH_URL]"))
# This client is just used to discover api version. Version API needn't
# microversion, so we just pass version 2 at here.
self.cs = client.Client(
api_versions.APIVersion("2.0"),
os_username, os_password, os_tenant_name,
tenant_id=os_tenant_id, user_id=os_user_id,
auth_url=os_auth_url, insecure=insecure,
region_name=os_region_name, endpoint_type=endpoint_type,
extensions=self.extensions, service_type=service_type,
service_name=service_name, auth_system=os_auth_system,
auth_plugin=auth_plugin, auth_token=auth_token,
volume_service_name=volume_service_name,
timings=args.timings, bypass_url=bypass_url,
os_cache=os_cache, http_log_debug=args.debug,
cacert=cacert, timeout=timeout,
session=keystone_session, auth=keystone_auth)
if not do_help:
if not api_version.is_latest():
if api_version > api_versions.APIVersion("2.0"):
if not api_version.matches(novaclient.API_MIN_VERSION,
novaclient.API_MAX_VERSION):
raise exc.CommandError(
_("The specified version isn't supported by "
"client. The valid version range is '%(min)s' "
"to '%(max)s'") % {
"min": novaclient.API_MIN_VERSION.get_string(),
"max": novaclient.API_MAX_VERSION.get_string()}
)
api_version = api_versions.discover_version(self.cs, api_version)
# build available subcommands based on version
self.extensions = self._discover_extensions(api_version)
self._run_extension_hooks('__pre_parse_args__')
subcommand_parser = self.get_subcommand_parser(
api_version, do_help=do_help)
self.parser = subcommand_parser
if args.help or not argv:
subcommand_parser.print_help()
return 0
args = subcommand_parser.parse_args(argv)
self._run_extension_hooks('__post_parse_args__', args)
# Short-circuit and deal with help right away.
if args.func == self.do_help:
self.do_help(args)
return 0
elif args.func == self.do_bash_completion:
self.do_bash_completion(args)
return 0
if not args.service_type:
service_type = (cliutils.get_service_type(args.func) or
DEFAULT_NOVA_SERVICE_TYPE)
if cliutils.isunauthenticated(args.func):
# NOTE(alex_xu): We need authentication for discover microversion.
# But the subcommands may needn't it. If the subcommand needn't,
# we clear the session arguements.
keystone_session = None
keystone_auth = None
# Recreate client object with discovered version.
self.cs = client.Client(
api_version,
os_username, os_password, os_tenant_name,
@ -727,7 +774,7 @@ class OpenStackComputeShell(object):
auth_plugin=auth_plugin, auth_token=auth_token,
volume_service_name=volume_service_name,
timings=args.timings, bypass_url=bypass_url,
os_cache=os_cache, http_log_debug=options.debug,
os_cache=os_cache, http_log_debug=args.debug,
cacert=cacert, timeout=timeout,
session=keystone_session, auth=keystone_auth)

@ -1,4 +1,4 @@
# Copyright 2015 Mirantis
# Copyright 2016 Mirantis
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -15,9 +15,11 @@
import mock
import novaclient
from novaclient import api_versions
from novaclient import exceptions
from novaclient.tests.unit import utils
from novaclient.v2 import versions
class APIVersionTestCase(utils.TestCase):
@ -247,3 +249,88 @@ class WrapsTestCase(utils.TestCase):
some_func(obj, *some_args, **some_kwargs)
checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs)
class DiscoverVersionTestCase(utils.TestCase):
def setUp(self):
super(DiscoverVersionTestCase, self).setUp()
self.orig_max = novaclient.API_MAX_VERSION
self.orig_min = novaclient.API_MIN_VERSION
self.addCleanup(self._clear_fake_version)
def _clear_fake_version(self):
novaclient.API_MAX_VERSION = self.orig_max
novaclient.API_MIN_VERSION = self.orig_min
def test_server_is_too_new(self):
fake_client = mock.MagicMock()
fake_client.versions.get_current.return_value = mock.MagicMock(
version="2.7", min_version="2.4")
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.3")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1")
self.assertRaises(exceptions.UnsupportedVersion,
api_versions.discover_version, fake_client,
api_versions.APIVersion('2.latest'))
def test_server_is_too_old(self):
fake_client = mock.MagicMock()
fake_client.versions.get_current.return_value = mock.MagicMock(
version="2.7", min_version="2.4")
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.10")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.9")
self.assertRaises(exceptions.UnsupportedVersion,
api_versions.discover_version, fake_client,
api_versions.APIVersion('2.latest'))
def test_server_end_version_is_the_latest_one(self):
fake_client = mock.MagicMock()
fake_client.versions.get_current.return_value = mock.MagicMock(
version="2.7", min_version="2.4")
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1")
self.assertEqual(
"2.7",
api_versions.discover_version(
fake_client,
api_versions.APIVersion('2.latest')).get_string())
def test_client_end_version_is_the_latest_one(self):
fake_client = mock.MagicMock()
fake_client.versions.get_current.return_value = mock.MagicMock(
version="2.16", min_version="2.4")
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1")
self.assertEqual(
"2.11",
api_versions.discover_version(
fake_client,
api_versions.APIVersion('2.latest')).get_string())
def test_server_without_microversion(self):
fake_client = mock.MagicMock()
fake_client.versions.get_current.return_value = mock.MagicMock(
version='', min_version='')
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1")
self.assertEqual(
"2.0",
api_versions.discover_version(
fake_client,
api_versions.APIVersion('2.latest')).get_string())
def test_server_without_microversion_and_no_version_field(self):
fake_client = mock.MagicMock()
fake_client.versions.get_current.return_value = versions.Version(
None, {})
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1")
self.assertEqual(
"2.0",
api_versions.discover_version(
fake_client,
api_versions.APIVersion('2.latest')).get_string())

@ -104,6 +104,19 @@ class ShellTest(utils.TestCase):
self.nc_util = mock.patch(
'novaclient.openstack.common.cliutils.isunauthenticated').start()
self.nc_util.return_value = False
self.mock_server_version_range = mock.patch(
'novaclient.api_versions._get_server_version_range').start()
self.mock_server_version_range.return_value = (
novaclient.API_MIN_VERSION,
novaclient.API_MIN_VERSION)
self.orig_max_ver = novaclient.API_MAX_VERSION
self.orig_min_ver = novaclient.API_MIN_VERSION
self.addCleanup(self._clear_fake_version)
self.addCleanup(mock.patch.stopall)
def _clear_fake_version(self):
novaclient.API_MAX_VERSION = self.orig_max_ver
novaclient.API_MIN_VERSION = self.orig_min_ver
def shell(self, argstr, exitcodes=(0,)):
orig = sys.stdout
@ -168,6 +181,7 @@ class ShellTest(utils.TestCase):
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def test_help_no_options(self):
self.make_env()
required = [
'.*?^usage: ',
'.*?^\s+set-password\s+Change the admin password',
@ -179,6 +193,7 @@ class ShellTest(utils.TestCase):
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def test_bash_completion(self):
self.make_env()
stdout, stderr = self.shell('bash-completion')
# just check we have some output
required = [
@ -403,6 +418,79 @@ class ShellTest(utils.TestCase):
keyring_saver = mock_client_instance.client.keyring_saver
self.assertIsInstance(keyring_saver, novaclient.shell.SecretsHelper)
@mock.patch('novaclient.client.Client')
def test_microversion_with_latest(self, mock_client):
self.make_env()
novaclient.API_MAX_VERSION = api_versions.APIVersion('2.3')
self.mock_server_version_range.return_value = (
api_versions.APIVersion("2.1"), api_versions.APIVersion("2.3"))
self.shell('--os-compute-api-version 2.latest list')
client_args = mock_client.call_args_list[1][0]
self.assertEqual(api_versions.APIVersion("2.3"), client_args[0])
@mock.patch('novaclient.client.Client')
def test_microversion_with_specified_version(self, mock_client):
self.make_env()
self.mock_server_version_range.return_value = (
api_versions.APIVersion("2.10"), api_versions.APIVersion("2.100"))
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.90")
self.shell('--os-compute-api-version 2.99 list')
client_args = mock_client.call_args_list[1][0]
self.assertEqual(api_versions.APIVersion("2.99"), client_args[0])
@mock.patch('novaclient.client.Client')
def test_microversion_with_specified_version_out_of_range(self,
mock_client):
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.90")
self.assertRaises(exceptions.CommandError,
self.shell, '--os-compute-api-version 2.199 list')
@mock.patch('novaclient.client.Client')
def test_microversion_with_v2_and_v2_1_server(self, mock_client):
self.make_env()
self.mock_server_version_range.return_value = (
api_versions.APIVersion('2.1'), api_versions.APIVersion('2.3'))
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1")
self.shell('--os-compute-api-version 2 list')
client_args = mock_client.call_args_list[1][0]
self.assertEqual(api_versions.APIVersion("2.0"), client_args[0])
@mock.patch('novaclient.client.Client')
def test_microversion_with_v2_and_v2_server(self, mock_client):
self.make_env()
self.mock_server_version_range.return_value = (
api_versions.APIVersion(), api_versions.APIVersion())
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1")
self.shell('--os-compute-api-version 2 list')
client_args = mock_client.call_args_list[1][0]
self.assertEqual(api_versions.APIVersion("2.0"), client_args[0])
@mock.patch('novaclient.client.Client')
def test_microversion_with_v2_without_server_compatible(self, mock_client):
self.make_env()
self.mock_server_version_range.return_value = (
api_versions.APIVersion('2.2'), api_versions.APIVersion('2.3'))
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1")
self.assertRaises(
exceptions.UnsupportedVersion,
self.shell, '--os-compute-api-version 2 list')
def test_microversion_with_specific_version_without_microversions(self):
self.make_env()
self.mock_server_version_range.return_value = (
api_versions.APIVersion(), api_versions.APIVersion())
novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100")
novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1")
self.assertRaises(
exceptions.UnsupportedVersion,
self.shell,
'--os-compute-api-version 2.3 list')
class TestLoadVersionedActions(utils.TestCase):

@ -2325,7 +2325,14 @@ class FakeSessionMockClient(base_client.SessionClient, FakeHTTPClient):
self.callstack = []
self.auth = mock.Mock()
self.session = mock.Mock()
self.session.get_endpoint.return_value = FakeHTTPClient.get_endpoint(
self)
self.service_type = 'service_type'
self.service_name = None
self.endpoint_override = None
self.interface = None
self.region_name = None
self.version = None
self.auth.get_auth_ref.return_value.project_id = 'tenant_id'