Merge "Implements 'microversions' api type - Part 1"
This commit is contained in:
commit
3199e6bd34
208
novaclient/api_versions.py
Normal file
208
novaclient/api_versions.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
import re
|
||||||
|
|
||||||
|
from oslo_utils import strutils
|
||||||
|
|
||||||
|
from novaclient import exceptions
|
||||||
|
from novaclient.i18n import _, _LW
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
if not LOG.handlers:
|
||||||
|
LOG.addHandler(logging.StreamHandler())
|
||||||
|
|
||||||
|
|
||||||
|
# key is a deprecated version and value is an alternative version.
|
||||||
|
DEPRECATED_VERSIONS = {"1.1": "2"}
|
||||||
|
|
||||||
|
|
||||||
|
_type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'")
|
||||||
|
|
||||||
|
|
||||||
|
class APIVersion(object):
|
||||||
|
"""This class represents an API Version with convenience
|
||||||
|
methods for manipulation and comparison of version
|
||||||
|
numbers that we need to do to implement microversions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, version_str=None):
|
||||||
|
"""Create an API version object."""
|
||||||
|
self.ver_major = 0
|
||||||
|
self.ver_minor = 0
|
||||||
|
|
||||||
|
if version_str is not None:
|
||||||
|
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str)
|
||||||
|
if match:
|
||||||
|
self.ver_major = int(match.group(1))
|
||||||
|
if match.group(2) == "latest":
|
||||||
|
# NOTE(andreykurilin): Infinity allows to easily determine
|
||||||
|
# latest version and doesn't require any additional checks
|
||||||
|
# in comparison methods.
|
||||||
|
self.ver_minor = float("inf")
|
||||||
|
else:
|
||||||
|
self.ver_minor = int(match.group(2))
|
||||||
|
else:
|
||||||
|
msg = _("Invalid format of client version '%s'. "
|
||||||
|
"Expected format 'X.Y', where X is a major part and Y "
|
||||||
|
"is a minor part of version.") % version_str
|
||||||
|
raise exceptions.UnsupportedVersion(msg)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Debug/Logging representation of object."""
|
||||||
|
if self.is_latest():
|
||||||
|
return "Latest API Version Major: %s" % self.ver_major
|
||||||
|
return ("API Version Major: %s, Minor: %s"
|
||||||
|
% (self.ver_major, self.ver_minor))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.is_null():
|
||||||
|
return "<APIVersion: null>"
|
||||||
|
else:
|
||||||
|
return "<APIVersion: %s>" % self.get_string()
|
||||||
|
|
||||||
|
def is_null(self):
|
||||||
|
return self.ver_major == 0 and self.ver_minor == 0
|
||||||
|
|
||||||
|
def is_latest(self):
|
||||||
|
return self.ver_minor == float("inf")
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if not isinstance(other, APIVersion):
|
||||||
|
raise TypeError(_type_error_msg % {"other": other,
|
||||||
|
"cls": self.__class__})
|
||||||
|
|
||||||
|
return ((self.ver_major, self.ver_minor) <
|
||||||
|
(other.ver_major, other.ver_minor))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, APIVersion):
|
||||||
|
raise TypeError(_type_error_msg % {"other": other,
|
||||||
|
"cls": self.__class__})
|
||||||
|
|
||||||
|
return ((self.ver_major, self.ver_minor) ==
|
||||||
|
(other.ver_major, other.ver_minor))
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
if not isinstance(other, APIVersion):
|
||||||
|
raise TypeError(_type_error_msg % {"other": other,
|
||||||
|
"cls": self.__class__})
|
||||||
|
|
||||||
|
return ((self.ver_major, self.ver_minor) >
|
||||||
|
(other.ver_major, other.ver_minor))
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self < other or self == other
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self > other or self == other
|
||||||
|
|
||||||
|
def matches(self, min_version, max_version):
|
||||||
|
"""Returns whether the version object represents a version
|
||||||
|
greater than or equal to the minimum version and less than
|
||||||
|
or equal to the maximum version.
|
||||||
|
|
||||||
|
:param min_version: Minimum acceptable version.
|
||||||
|
:param max_version: Maximum acceptable version.
|
||||||
|
:returns: boolean
|
||||||
|
|
||||||
|
If min_version is null then there is no minimum limit.
|
||||||
|
If max_version is null then there is no maximum limit.
|
||||||
|
If self is null then raise ValueError
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.is_null():
|
||||||
|
raise ValueError(_("Null APIVersion doesn't support 'matches'."))
|
||||||
|
if max_version.is_null() and min_version.is_null():
|
||||||
|
return True
|
||||||
|
elif max_version.is_null():
|
||||||
|
return min_version <= self
|
||||||
|
elif min_version.is_null():
|
||||||
|
return self <= max_version
|
||||||
|
else:
|
||||||
|
return min_version <= self <= max_version
|
||||||
|
|
||||||
|
def get_string(self):
|
||||||
|
"""Converts object to string representation which if used to create
|
||||||
|
an APIVersion object results in the same version.
|
||||||
|
"""
|
||||||
|
if self.is_null():
|
||||||
|
raise ValueError(
|
||||||
|
_("Null APIVersion cannot be converted to string."))
|
||||||
|
elif self.is_latest():
|
||||||
|
return "%s.%s" % (self.ver_major, "latest")
|
||||||
|
return "%s.%s" % (self.ver_major, self.ver_minor)
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_major_versions():
|
||||||
|
# NOTE(andreykurilin): available clients version should not be
|
||||||
|
# hardcoded, so let's discover them.
|
||||||
|
matcher = re.compile(r"v[0-9]*$")
|
||||||
|
submodules = pkgutil.iter_modules([os.path.dirname(__file__)])
|
||||||
|
available_versions = [name[1:] for loader, name, ispkg in submodules
|
||||||
|
if matcher.search(name)]
|
||||||
|
|
||||||
|
return available_versions
|
||||||
|
|
||||||
|
|
||||||
|
def check_major_version(api_version):
|
||||||
|
"""Checks major part of ``APIVersion`` obj is supported.
|
||||||
|
|
||||||
|
:raises novaclient.exceptions.UnsupportedVersion: if major part is not
|
||||||
|
supported
|
||||||
|
"""
|
||||||
|
available_versions = get_available_major_versions()
|
||||||
|
if (not api_version.is_null() and
|
||||||
|
str(api_version.ver_major) not in available_versions):
|
||||||
|
if len(available_versions) == 1:
|
||||||
|
msg = _("Invalid client version '%(version)s'. "
|
||||||
|
"Major part should be '%(major)s'") % {
|
||||||
|
"version": api_version.get_string(),
|
||||||
|
"major": available_versions[0]}
|
||||||
|
else:
|
||||||
|
msg = _("Invalid client version '%(version)s'. "
|
||||||
|
"Major part must be one of: '%(major)s'") % {
|
||||||
|
"version": api_version.get_string(),
|
||||||
|
"major": ", ".join(available_versions)}
|
||||||
|
raise exceptions.UnsupportedVersion(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_version(version_string):
|
||||||
|
"""Returns checked APIVersion object"""
|
||||||
|
version_string = str(version_string)
|
||||||
|
if version_string in DEPRECATED_VERSIONS:
|
||||||
|
LOG.warning(
|
||||||
|
_LW("Version %(deprecated_version)s is deprecated, using "
|
||||||
|
"alternative version %(alternative)s instead.") %
|
||||||
|
{"deprecated_version": version_string,
|
||||||
|
"alternative": DEPRECATED_VERSIONS[version_string]})
|
||||||
|
version_string = DEPRECATED_VERSIONS[version_string]
|
||||||
|
if strutils.is_int_like(version_string):
|
||||||
|
version_string = "%s.0" % version_string
|
||||||
|
|
||||||
|
api_version = APIVersion(version_string)
|
||||||
|
check_major_version(api_version)
|
||||||
|
return api_version
|
||||||
|
|
||||||
|
|
||||||
|
def update_headers(headers, api_version):
|
||||||
|
"""Set 'X-OpenStack-Nova-API-Version' header if api_version is not null"""
|
||||||
|
|
||||||
|
if not api_version.is_null() and api_version.ver_minor != 0:
|
||||||
|
headers["X-OpenStack-Nova-API-Version"] = api_version.get_string()
|
@ -31,7 +31,6 @@ import os
|
|||||||
import pkgutil
|
import pkgutil
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import warnings
|
|
||||||
|
|
||||||
from keystoneclient import adapter
|
from keystoneclient import adapter
|
||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
@ -47,17 +46,14 @@ except ImportError:
|
|||||||
|
|
||||||
from six.moves.urllib import parse
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
|
from novaclient import api_versions
|
||||||
from novaclient import exceptions
|
from novaclient import exceptions
|
||||||
from novaclient import extension as ext
|
from novaclient import extension as ext
|
||||||
from novaclient.i18n import _, _LW
|
from novaclient.i18n import _
|
||||||
from novaclient import service_catalog
|
from novaclient import service_catalog
|
||||||
from novaclient import utils
|
from novaclient import utils
|
||||||
|
|
||||||
|
|
||||||
# key is a deprecated version and value is an alternative version.
|
|
||||||
DEPRECATED_VERSIONS = {"1.1": "2"}
|
|
||||||
|
|
||||||
|
|
||||||
class TCPKeepAliveAdapter(adapters.HTTPAdapter):
|
class TCPKeepAliveAdapter(adapters.HTTPAdapter):
|
||||||
"""The custom adapter used to set TCP Keep-Alive on all connections."""
|
"""The custom adapter used to set TCP Keep-Alive on all connections."""
|
||||||
def init_poolmanager(self, *args, **kwargs):
|
def init_poolmanager(self, *args, **kwargs):
|
||||||
@ -88,9 +84,13 @@ class SessionClient(adapter.LegacyJsonAdapter):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.times = []
|
self.times = []
|
||||||
self.timings = kwargs.pop('timings', False)
|
self.timings = kwargs.pop('timings', False)
|
||||||
|
self.api_version = kwargs.pop('api_version', None)
|
||||||
|
self.api_version = self.api_version or api_versions.APIVersion()
|
||||||
super(SessionClient, self).__init__(*args, **kwargs)
|
super(SessionClient, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def request(self, url, method, **kwargs):
|
def request(self, url, method, **kwargs):
|
||||||
|
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
||||||
|
api_versions.update_headers(kwargs["headers"], self.api_version)
|
||||||
# NOTE(jamielennox): The standard call raises errors from
|
# NOTE(jamielennox): The standard call raises errors from
|
||||||
# keystoneclient, where we need to raise the novaclient errors.
|
# keystoneclient, where we need to raise the novaclient errors.
|
||||||
raise_exc = kwargs.pop('raise_exc', True)
|
raise_exc = kwargs.pop('raise_exc', True)
|
||||||
@ -144,12 +144,13 @@ class HTTPClient(object):
|
|||||||
http_log_debug=False, auth_system='keystone',
|
http_log_debug=False, auth_system='keystone',
|
||||||
auth_plugin=None, auth_token=None,
|
auth_plugin=None, auth_token=None,
|
||||||
cacert=None, tenant_id=None, user_id=None,
|
cacert=None, tenant_id=None, user_id=None,
|
||||||
connection_pool=False):
|
connection_pool=False, api_version=None):
|
||||||
self.user = user
|
self.user = user
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.password = password
|
self.password = password
|
||||||
self.projectid = projectid
|
self.projectid = projectid
|
||||||
self.tenant_id = tenant_id
|
self.tenant_id = tenant_id
|
||||||
|
self.api_version = api_version or api_versions.APIVersion()
|
||||||
|
|
||||||
self._connection_pool = (_ClientConnectionPool()
|
self._connection_pool = (_ClientConnectionPool()
|
||||||
if connection_pool else None)
|
if connection_pool else None)
|
||||||
@ -357,6 +358,7 @@ class HTTPClient(object):
|
|||||||
kwargs['headers']['Content-Type'] = 'application/json'
|
kwargs['headers']['Content-Type'] = 'application/json'
|
||||||
kwargs['data'] = json.dumps(kwargs['body'])
|
kwargs['data'] = json.dumps(kwargs['body'])
|
||||||
del kwargs['body']
|
del kwargs['body']
|
||||||
|
api_versions.update_headers(kwargs["headers"], self.api_version)
|
||||||
if self.timeout is not None:
|
if self.timeout is not None:
|
||||||
kwargs.setdefault('timeout', self.timeout)
|
kwargs.setdefault('timeout', self.timeout)
|
||||||
kwargs['verify'] = self.verify_cert
|
kwargs['verify'] = self.verify_cert
|
||||||
@ -681,7 +683,7 @@ def _construct_http_client(username=None, password=None, project_id=None,
|
|||||||
auth_token=None, cacert=None, tenant_id=None,
|
auth_token=None, cacert=None, tenant_id=None,
|
||||||
user_id=None, connection_pool=False, session=None,
|
user_id=None, connection_pool=False, session=None,
|
||||||
auth=None, user_agent='python-novaclient',
|
auth=None, user_agent='python-novaclient',
|
||||||
interface=None, **kwargs):
|
interface=None, api_version=None, **kwargs):
|
||||||
if session:
|
if session:
|
||||||
return SessionClient(session=session,
|
return SessionClient(session=session,
|
||||||
auth=auth,
|
auth=auth,
|
||||||
@ -691,6 +693,7 @@ def _construct_http_client(username=None, password=None, project_id=None,
|
|||||||
service_name=service_name,
|
service_name=service_name,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
timings=timings,
|
timings=timings,
|
||||||
|
api_version=api_version,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
else:
|
else:
|
||||||
# FIXME(jamielennox): username and password are now optional. Need
|
# FIXME(jamielennox): username and password are now optional. Need
|
||||||
@ -718,10 +721,13 @@ def _construct_http_client(username=None, password=None, project_id=None,
|
|||||||
os_cache=os_cache,
|
os_cache=os_cache,
|
||||||
http_log_debug=http_log_debug,
|
http_log_debug=http_log_debug,
|
||||||
cacert=cacert,
|
cacert=cacert,
|
||||||
connection_pool=connection_pool)
|
connection_pool=connection_pool,
|
||||||
|
api_version=api_version)
|
||||||
|
|
||||||
|
|
||||||
def discover_extensions(version):
|
def discover_extensions(version):
|
||||||
|
if not isinstance(version, api_versions.APIVersion):
|
||||||
|
version = api_versions.get_api_version(version)
|
||||||
extensions = []
|
extensions = []
|
||||||
for name, module in itertools.chain(
|
for name, module in itertools.chain(
|
||||||
_discover_via_python_path(),
|
_discover_via_python_path(),
|
||||||
@ -750,12 +756,7 @@ def _discover_via_python_path():
|
|||||||
|
|
||||||
def _discover_via_contrib_path(version):
|
def _discover_via_contrib_path(version):
|
||||||
module_path = os.path.dirname(os.path.abspath(__file__))
|
module_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
version_str = "v%s" % version.replace('.', '_')
|
ext_path = os.path.join(module_path, "v%s" % version.ver_major, 'contrib')
|
||||||
# NOTE(andreykurilin): v1.1 uses implementation of v2, so we should
|
|
||||||
# discover contrib modules in novaclient.v2 dir.
|
|
||||||
if version_str == "v1_1":
|
|
||||||
version_str = "v2"
|
|
||||||
ext_path = os.path.join(module_path, version_str, 'contrib')
|
|
||||||
ext_glob = os.path.join(ext_path, "*.py")
|
ext_glob = os.path.join(ext_path, "*.py")
|
||||||
|
|
||||||
for ext_path in glob.iglob(ext_glob):
|
for ext_path in glob.iglob(ext_glob):
|
||||||
@ -776,38 +777,25 @@ def _discover_via_entry_points():
|
|||||||
yield name, module
|
yield name, module
|
||||||
|
|
||||||
|
|
||||||
def _get_available_client_versions():
|
def _get_client_class_and_version(version):
|
||||||
# NOTE(andreykurilin): available clients version should not be
|
if not isinstance(version, api_versions.APIVersion):
|
||||||
# hardcoded, so let's discover them.
|
version = api_versions.get_api_version(version)
|
||||||
matcher = re.compile(r"v[0-9_]*$")
|
else:
|
||||||
submodules = pkgutil.iter_modules([os.path.dirname(__file__)])
|
api_versions.check_major_version(version)
|
||||||
available_versions = [
|
if version.is_latest():
|
||||||
name[1:].replace("_", ".") for loader, name, ispkg in submodules
|
raise exceptions.UnsupportedVersion(
|
||||||
if matcher.search(name)]
|
_("The version should be explicit, not latest."))
|
||||||
|
return version, importutils.import_class(
|
||||||
return available_versions
|
"novaclient.v%s.client.Client" % version.ver_major)
|
||||||
|
|
||||||
|
|
||||||
def get_client_class(version):
|
def get_client_class(version):
|
||||||
version = str(version)
|
"""Returns Client class based on given version."""
|
||||||
if version in DEPRECATED_VERSIONS:
|
_api_version, client_class = _get_client_class_and_version(version)
|
||||||
warnings.warn(_LW(
|
return client_class
|
||||||
"Version %(deprecated_version)s is deprecated, using "
|
|
||||||
"alternative version %(alternative)s instead.") %
|
|
||||||
{"deprecated_version": version,
|
|
||||||
"alternative": DEPRECATED_VERSIONS[version]})
|
|
||||||
version = DEPRECATED_VERSIONS[version]
|
|
||||||
try:
|
|
||||||
return importutils.import_class(
|
|
||||||
"novaclient.v%s.client.Client" % version)
|
|
||||||
except ImportError:
|
|
||||||
available_versions = _get_available_client_versions()
|
|
||||||
msg = _("Invalid client version '%(version)s'. must be one of: "
|
|
||||||
"%(keys)s") % {'version': version,
|
|
||||||
'keys': ', '.join(available_versions)}
|
|
||||||
raise exceptions.UnsupportedVersion(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def Client(version, *args, **kwargs):
|
def Client(version, *args, **kwargs):
|
||||||
client_class = get_client_class(version)
|
"""Initialize client object based on given version."""
|
||||||
return client_class(*args, **kwargs)
|
api_version, client_class = _get_client_class_and_version(version)
|
||||||
|
return client_class(api_version=api_version, *args, **kwargs)
|
||||||
|
@ -159,6 +159,14 @@ class MethodNotAllowed(ClientException):
|
|||||||
message = "Method Not Allowed"
|
message = "Method Not Allowed"
|
||||||
|
|
||||||
|
|
||||||
|
class NotAcceptable(ClientException):
|
||||||
|
"""
|
||||||
|
HTTP 406 - Not Acceptable
|
||||||
|
"""
|
||||||
|
http_status = 406
|
||||||
|
message = "Not Acceptable"
|
||||||
|
|
||||||
|
|
||||||
class Conflict(ClientException):
|
class Conflict(ClientException):
|
||||||
"""
|
"""
|
||||||
HTTP 409 - Conflict
|
HTTP 409 - Conflict
|
||||||
@ -199,8 +207,8 @@ class HTTPNotImplemented(ClientException):
|
|||||||
#
|
#
|
||||||
# Instead, we have to hardcode it:
|
# Instead, we have to hardcode it:
|
||||||
_error_classes = [BadRequest, Unauthorized, Forbidden, NotFound,
|
_error_classes = [BadRequest, Unauthorized, Forbidden, NotFound,
|
||||||
MethodNotAllowed, Conflict, OverLimit, RateLimit,
|
MethodNotAllowed, NotAcceptable, Conflict, OverLimit,
|
||||||
HTTPNotImplemented]
|
RateLimit, HTTPNotImplemented]
|
||||||
_code_map = dict((c.http_status, c) for c in _error_classes)
|
_code_map = dict((c.http_status, c) for c in _error_classes)
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ from keystoneclient.auth.identity.generic import token
|
|||||||
from keystoneclient.auth.identity import v3 as identity
|
from keystoneclient.auth.identity import v3 as identity
|
||||||
from keystoneclient import session as ksession
|
from keystoneclient import session as ksession
|
||||||
from oslo_utils import encodeutils
|
from oslo_utils import encodeutils
|
||||||
|
from oslo_utils import importutils
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ except ImportError:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
import novaclient
|
import novaclient
|
||||||
|
from novaclient import api_versions
|
||||||
import novaclient.auth_plugin
|
import novaclient.auth_plugin
|
||||||
from novaclient import client
|
from novaclient import client
|
||||||
from novaclient import exceptions as exc
|
from novaclient import exceptions as exc
|
||||||
@ -48,7 +50,6 @@ import novaclient.extension
|
|||||||
from novaclient.i18n import _
|
from novaclient.i18n import _
|
||||||
from novaclient.openstack.common import cliutils
|
from novaclient.openstack.common import cliutils
|
||||||
from novaclient import utils
|
from novaclient import utils
|
||||||
from novaclient.v2 import shell as shell_v2
|
|
||||||
|
|
||||||
DEFAULT_OS_COMPUTE_API_VERSION = "2"
|
DEFAULT_OS_COMPUTE_API_VERSION = "2"
|
||||||
DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL'
|
DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL'
|
||||||
@ -402,7 +403,7 @@ class OpenStackComputeShell(object):
|
|||||||
metavar='<compute-api-ver>',
|
metavar='<compute-api-ver>',
|
||||||
default=cliutils.env('OS_COMPUTE_API_VERSION',
|
default=cliutils.env('OS_COMPUTE_API_VERSION',
|
||||||
default=DEFAULT_OS_COMPUTE_API_VERSION),
|
default=DEFAULT_OS_COMPUTE_API_VERSION),
|
||||||
help=_('Accepts number of API version, '
|
help=_('Accepts X, X.Y (where X is major and Y is minor part), '
|
||||||
'defaults to env[OS_COMPUTE_API_VERSION].'))
|
'defaults to env[OS_COMPUTE_API_VERSION].'))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os_compute_api_version',
|
'--os_compute_api_version',
|
||||||
@ -431,15 +432,10 @@ class OpenStackComputeShell(object):
|
|||||||
self.subcommands = {}
|
self.subcommands = {}
|
||||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||||
|
|
||||||
try:
|
actions_module = importutils.import_module(
|
||||||
actions_module = {
|
"novaclient.v%s.shell" % version.ver_major)
|
||||||
'1.1': shell_v2,
|
|
||||||
'2': shell_v2,
|
|
||||||
'3': shell_v2,
|
|
||||||
}[version]
|
|
||||||
except KeyError:
|
|
||||||
actions_module = shell_v2
|
|
||||||
|
|
||||||
|
# TODO(andreykurilin): discover actions based on microversions
|
||||||
self._find_actions(subparsers, actions_module)
|
self._find_actions(subparsers, actions_module)
|
||||||
self._find_actions(subparsers, self)
|
self._find_actions(subparsers, self)
|
||||||
|
|
||||||
@ -523,9 +519,11 @@ class OpenStackComputeShell(object):
|
|||||||
# Discover available auth plugins
|
# Discover available auth plugins
|
||||||
novaclient.auth_plugin.discover_auth_systems()
|
novaclient.auth_plugin.discover_auth_systems()
|
||||||
|
|
||||||
# build available subcommands based on version
|
api_version = api_versions.get_api_version(
|
||||||
self.extensions = self._discover_extensions(
|
|
||||||
options.os_compute_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__')
|
self._run_extension_hooks('__pre_parse_args__')
|
||||||
|
|
||||||
# NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse
|
# NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse
|
||||||
@ -536,8 +534,7 @@ class OpenStackComputeShell(object):
|
|||||||
spot = argv.index('--endpoint_type')
|
spot = argv.index('--endpoint_type')
|
||||||
argv[spot] = '--endpoint-type'
|
argv[spot] = '--endpoint-type'
|
||||||
|
|
||||||
subcommand_parser = self.get_subcommand_parser(
|
subcommand_parser = self.get_subcommand_parser(api_version)
|
||||||
options.os_compute_api_version)
|
|
||||||
self.parser = subcommand_parser
|
self.parser = subcommand_parser
|
||||||
|
|
||||||
if options.help or not argv:
|
if options.help or not argv:
|
||||||
@ -670,23 +667,22 @@ class OpenStackComputeShell(object):
|
|||||||
project_domain_id=args.os_project_domain_id,
|
project_domain_id=args.os_project_domain_id,
|
||||||
project_domain_name=args.os_project_domain_name)
|
project_domain_name=args.os_project_domain_name)
|
||||||
|
|
||||||
if options.os_compute_api_version:
|
if not any([args.os_tenant_id, args.os_tenant_name,
|
||||||
if not any([args.os_tenant_id, args.os_tenant_name,
|
args.os_project_id, args.os_project_name]):
|
||||||
args.os_project_id, args.os_project_name]):
|
raise exc.CommandError(_("You must provide a project name or"
|
||||||
raise exc.CommandError(_("You must provide a project name or"
|
" project id via --os-project-name,"
|
||||||
" project id via --os-project-name,"
|
" --os-project-id, env[OS_PROJECT_ID]"
|
||||||
" --os-project-id, env[OS_PROJECT_ID]"
|
" or env[OS_PROJECT_NAME]. You may"
|
||||||
" or env[OS_PROJECT_NAME]. You may"
|
" use os-project and os-tenant"
|
||||||
" use os-project and os-tenant"
|
" interchangeably."))
|
||||||
" interchangeably."))
|
|
||||||
|
|
||||||
if not os_auth_url:
|
if not os_auth_url:
|
||||||
raise exc.CommandError(
|
raise exc.CommandError(
|
||||||
_("You must provide an auth url "
|
_("You must provide an auth url "
|
||||||
"via either --os-auth-url or env[OS_AUTH_URL]"))
|
"via either --os-auth-url or env[OS_AUTH_URL]"))
|
||||||
|
|
||||||
self.cs = client.Client(
|
self.cs = client.Client(
|
||||||
options.os_compute_api_version,
|
api_version,
|
||||||
os_username, os_password, os_tenant_name,
|
os_username, os_password, os_tenant_name,
|
||||||
tenant_id=os_tenant_id, user_id=os_user_id,
|
tenant_id=os_tenant_id, user_id=os_user_id,
|
||||||
auth_url=os_auth_url, insecure=insecure,
|
auth_url=os_auth_url, insecure=insecure,
|
||||||
|
170
novaclient/tests/unit/test_api_versions.py
Normal file
170
novaclient/tests/unit/test_api_versions.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
# Copyright 2015 Mirantis
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from novaclient import api_versions
|
||||||
|
from novaclient import exceptions
|
||||||
|
from novaclient.tests.unit import utils
|
||||||
|
|
||||||
|
|
||||||
|
class APIVersionTestCase(utils.TestCase):
|
||||||
|
def test_valid_version_strings(self):
|
||||||
|
def _test_string(version, exp_major, exp_minor):
|
||||||
|
v = api_versions.APIVersion(version)
|
||||||
|
self.assertEqual(v.ver_major, exp_major)
|
||||||
|
self.assertEqual(v.ver_minor, exp_minor)
|
||||||
|
|
||||||
|
_test_string("1.1", 1, 1)
|
||||||
|
_test_string("2.10", 2, 10)
|
||||||
|
_test_string("5.234", 5, 234)
|
||||||
|
_test_string("12.5", 12, 5)
|
||||||
|
_test_string("2.0", 2, 0)
|
||||||
|
_test_string("2.200", 2, 200)
|
||||||
|
|
||||||
|
def test_null_version(self):
|
||||||
|
v = api_versions.APIVersion()
|
||||||
|
self.assertTrue(v.is_null())
|
||||||
|
|
||||||
|
def test_invalid_version_strings(self):
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "2")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "200")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "2.1.4")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "200.23.66.3")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "5 .3")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "5. 3")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "5.03")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "02.1")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "2.001")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, " 2.1")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.APIVersion, "2.1 ")
|
||||||
|
|
||||||
|
def test_version_comparisons(self):
|
||||||
|
v1 = api_versions.APIVersion("2.0")
|
||||||
|
v2 = api_versions.APIVersion("2.5")
|
||||||
|
v3 = api_versions.APIVersion("5.23")
|
||||||
|
v4 = api_versions.APIVersion("2.0")
|
||||||
|
v_null = api_versions.APIVersion()
|
||||||
|
|
||||||
|
self.assertTrue(v1 < v2)
|
||||||
|
self.assertTrue(v3 > v2)
|
||||||
|
self.assertTrue(v1 != v2)
|
||||||
|
self.assertTrue(v1 == v4)
|
||||||
|
self.assertTrue(v1 != v_null)
|
||||||
|
self.assertTrue(v_null == v_null)
|
||||||
|
self.assertRaises(TypeError, v1.__le__, "2.1")
|
||||||
|
|
||||||
|
def test_version_matches(self):
|
||||||
|
v1 = api_versions.APIVersion("2.0")
|
||||||
|
v2 = api_versions.APIVersion("2.5")
|
||||||
|
v3 = api_versions.APIVersion("2.45")
|
||||||
|
v4 = api_versions.APIVersion("3.3")
|
||||||
|
v5 = api_versions.APIVersion("3.23")
|
||||||
|
v6 = api_versions.APIVersion("2.0")
|
||||||
|
v7 = api_versions.APIVersion("3.3")
|
||||||
|
v8 = api_versions.APIVersion("4.0")
|
||||||
|
v_null = api_versions.APIVersion()
|
||||||
|
|
||||||
|
self.assertTrue(v2.matches(v1, v3))
|
||||||
|
self.assertTrue(v2.matches(v1, v_null))
|
||||||
|
self.assertTrue(v1.matches(v6, v2))
|
||||||
|
self.assertTrue(v4.matches(v2, v7))
|
||||||
|
self.assertTrue(v4.matches(v_null, v7))
|
||||||
|
self.assertTrue(v4.matches(v_null, v8))
|
||||||
|
self.assertFalse(v1.matches(v2, v3))
|
||||||
|
self.assertFalse(v5.matches(v2, v4))
|
||||||
|
self.assertFalse(v2.matches(v3, v1))
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, v_null.matches, v1, v3)
|
||||||
|
|
||||||
|
def test_get_string(self):
|
||||||
|
v1_string = "3.23"
|
||||||
|
v1 = api_versions.APIVersion(v1_string)
|
||||||
|
self.assertEqual(v1_string, v1.get_string())
|
||||||
|
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
api_versions.APIVersion().get_string)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateHeadersTestCase(utils.TestCase):
|
||||||
|
def test_api_version_is_null(self):
|
||||||
|
headers = {}
|
||||||
|
api_versions.update_headers(headers, api_versions.APIVersion())
|
||||||
|
self.assertEqual({}, headers)
|
||||||
|
|
||||||
|
def test_api_version_is_major(self):
|
||||||
|
headers = {}
|
||||||
|
api_versions.update_headers(headers, api_versions.APIVersion("7.0"))
|
||||||
|
self.assertEqual({}, headers)
|
||||||
|
|
||||||
|
def test_api_version_is_not_null(self):
|
||||||
|
api_version = api_versions.APIVersion("2.3")
|
||||||
|
headers = {}
|
||||||
|
api_versions.update_headers(headers, api_version)
|
||||||
|
self.assertEqual(
|
||||||
|
{"X-OpenStack-Nova-API-Version": api_version.get_string()},
|
||||||
|
headers)
|
||||||
|
|
||||||
|
|
||||||
|
class GetAPIVersionTestCase(utils.TestCase):
|
||||||
|
def test_get_available_client_versions(self):
|
||||||
|
output = api_versions.get_available_major_versions()
|
||||||
|
self.assertNotEqual([], output)
|
||||||
|
|
||||||
|
def test_wrong_format(self):
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.get_api_version, "something_wrong")
|
||||||
|
|
||||||
|
def test_wrong_major_version(self):
|
||||||
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
api_versions.get_api_version, "1")
|
||||||
|
|
||||||
|
@mock.patch("novaclient.api_versions.APIVersion")
|
||||||
|
def test_only_major_part_is_presented(self, mock_apiversion):
|
||||||
|
version = 7
|
||||||
|
self.assertEqual(mock_apiversion.return_value,
|
||||||
|
api_versions.get_api_version(version))
|
||||||
|
mock_apiversion.assert_called_once_with("%s.0" % str(version))
|
||||||
|
|
||||||
|
@mock.patch("novaclient.api_versions.APIVersion")
|
||||||
|
def test_major_and_minor_parts_is_presented(self, mock_apiversion):
|
||||||
|
version = "2.7"
|
||||||
|
self.assertEqual(mock_apiversion.return_value,
|
||||||
|
api_versions.get_api_version(version))
|
||||||
|
mock_apiversion.assert_called_once_with(version)
|
@ -161,10 +161,6 @@ class ClientTest(utils.TestCase):
|
|||||||
self._check_version_url('http://foo.com/nova/v2/%s',
|
self._check_version_url('http://foo.com/nova/v2/%s',
|
||||||
'http://foo.com/nova/')
|
'http://foo.com/nova/')
|
||||||
|
|
||||||
def test_get_available_client_versions(self):
|
|
||||||
output = novaclient.client._get_available_client_versions()
|
|
||||||
self.assertNotEqual([], output)
|
|
||||||
|
|
||||||
def test_get_client_class_v2(self):
|
def test_get_client_class_v2(self):
|
||||||
output = novaclient.client.get_client_class('2')
|
output = novaclient.client.get_client_class('2')
|
||||||
self.assertEqual(output, novaclient.v2.client.Client)
|
self.assertEqual(output, novaclient.v2.client.Client)
|
||||||
@ -181,6 +177,12 @@ class ClientTest(utils.TestCase):
|
|||||||
self.assertRaises(novaclient.exceptions.UnsupportedVersion,
|
self.assertRaises(novaclient.exceptions.UnsupportedVersion,
|
||||||
novaclient.client.get_client_class, '0')
|
novaclient.client.get_client_class, '0')
|
||||||
|
|
||||||
|
def test_get_client_class_latest(self):
|
||||||
|
self.assertRaises(novaclient.exceptions.UnsupportedVersion,
|
||||||
|
novaclient.client.get_client_class, 'latest')
|
||||||
|
self.assertRaises(novaclient.exceptions.UnsupportedVersion,
|
||||||
|
novaclient.client.get_client_class, '2.latest')
|
||||||
|
|
||||||
def test_client_with_os_cache_enabled(self):
|
def test_client_with_os_cache_enabled(self):
|
||||||
cs = novaclient.v2.client.Client("user", "password", "project_id",
|
cs = novaclient.v2.client.Client("user", "password", "project_id",
|
||||||
auth_url="foo/v2", os_cache=True)
|
auth_url="foo/v2", os_cache=True)
|
||||||
|
@ -97,8 +97,8 @@ class ShellTest(utils.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ShellTest, self).setUp()
|
super(ShellTest, self).setUp()
|
||||||
self.useFixture(fixtures.MonkeyPatch(
|
self.useFixture(fixtures.MonkeyPatch(
|
||||||
'novaclient.client.get_client_class',
|
'novaclient.client.Client',
|
||||||
mock.MagicMock))
|
mock.MagicMock()))
|
||||||
self.nc_util = mock.patch(
|
self.nc_util = mock.patch(
|
||||||
'novaclient.openstack.common.cliutils.isunauthenticated').start()
|
'novaclient.openstack.common.cliutils.isunauthenticated').start()
|
||||||
self.nc_util.return_value = False
|
self.nc_util.return_value = False
|
||||||
@ -344,7 +344,9 @@ class ShellTest(utils.TestCase):
|
|||||||
|
|
||||||
@mock.patch('novaclient.client.Client')
|
@mock.patch('novaclient.client.Client')
|
||||||
def test_v_unknown_service_type(self, mock_client):
|
def test_v_unknown_service_type(self, mock_client):
|
||||||
self._test_service_type('unknown', 'compute', mock_client)
|
self.assertRaises(exceptions.UnsupportedVersion,
|
||||||
|
self._test_service_type,
|
||||||
|
'unknown', 'compute', mock_client)
|
||||||
|
|
||||||
@mock.patch('sys.argv', ['nova'])
|
@mock.patch('sys.argv', ['nova'])
|
||||||
@mock.patch('sys.stdout', six.StringIO())
|
@mock.patch('sys.stdout', six.StringIO())
|
||||||
|
@ -30,10 +30,11 @@ from novaclient.v2 import client
|
|||||||
|
|
||||||
class FakeClient(fakes.FakeClient, client.Client):
|
class FakeClient(fakes.FakeClient, client.Client):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, api_version=None, *args, **kwargs):
|
||||||
client.Client.__init__(self, 'username', 'password',
|
client.Client.__init__(self, 'username', 'password',
|
||||||
'project_id', 'auth_url',
|
'project_id', 'auth_url',
|
||||||
extensions=kwargs.get('extensions'))
|
extensions=kwargs.get('extensions'))
|
||||||
|
self.api_version = api_version
|
||||||
self.client = FakeHTTPClient(**kwargs)
|
self.client = FakeHTTPClient(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,8 +70,8 @@ class ShellTest(utils.TestCase):
|
|||||||
self.shell = self.useFixture(ShellFixture()).shell
|
self.shell = self.useFixture(ShellFixture()).shell
|
||||||
|
|
||||||
self.useFixture(fixtures.MonkeyPatch(
|
self.useFixture(fixtures.MonkeyPatch(
|
||||||
'novaclient.client.get_client_class',
|
'novaclient.client.Client',
|
||||||
lambda *_: fakes.FakeClient))
|
lambda *args, **kwargs: fakes.FakeClient(*args, **kwargs)))
|
||||||
|
|
||||||
@mock.patch('sys.stdout', new_callable=six.StringIO)
|
@mock.patch('sys.stdout', new_callable=six.StringIO)
|
||||||
@mock.patch('sys.stderr', new_callable=six.StringIO)
|
@mock.patch('sys.stderr', new_callable=six.StringIO)
|
||||||
@ -2433,8 +2433,8 @@ class ShellWithSessionClientTest(ShellTest):
|
|||||||
"""Run before each test."""
|
"""Run before each test."""
|
||||||
super(ShellWithSessionClientTest, self).setUp()
|
super(ShellWithSessionClientTest, self).setUp()
|
||||||
self.useFixture(fixtures.MonkeyPatch(
|
self.useFixture(fixtures.MonkeyPatch(
|
||||||
'novaclient.client.get_client_class',
|
'novaclient.client.Client',
|
||||||
lambda *_: fakes.FakeSessionClient))
|
lambda *args, **kwargs: fakes.FakeSessionClient(*args, **kwargs)))
|
||||||
|
|
||||||
|
|
||||||
class GetSecgroupTest(utils.TestCase):
|
class GetSecgroupTest(utils.TestCase):
|
||||||
|
@ -103,7 +103,7 @@ class Client(object):
|
|||||||
auth_system='keystone', auth_plugin=None, auth_token=None,
|
auth_system='keystone', auth_plugin=None, auth_token=None,
|
||||||
cacert=None, tenant_id=None, user_id=None,
|
cacert=None, tenant_id=None, user_id=None,
|
||||||
connection_pool=False, session=None, auth=None,
|
connection_pool=False, session=None, auth=None,
|
||||||
**kwargs):
|
api_version=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param str username: Username
|
:param str username: Username
|
||||||
:param str api_key: API Key
|
:param str api_key: API Key
|
||||||
@ -133,6 +133,8 @@ class Client(object):
|
|||||||
:param bool connection_pool: Use a connection pool
|
:param bool connection_pool: Use a connection pool
|
||||||
:param str session: Session
|
:param str session: Session
|
||||||
:param str auth: Auth
|
:param str auth: Auth
|
||||||
|
:param api_version: Compute API version
|
||||||
|
:type api_version: novaclient.api_versions.APIVersion
|
||||||
"""
|
"""
|
||||||
# FIXME(comstud): Rename the api_key argument above when we
|
# FIXME(comstud): Rename the api_key argument above when we
|
||||||
# know it's not being used as keyword argument
|
# know it's not being used as keyword argument
|
||||||
@ -153,6 +155,7 @@ class Client(object):
|
|||||||
self.limits = limits.LimitsManager(self)
|
self.limits = limits.LimitsManager(self)
|
||||||
self.servers = servers.ServerManager(self)
|
self.servers = servers.ServerManager(self)
|
||||||
self.versions = versions.VersionManager(self)
|
self.versions = versions.VersionManager(self)
|
||||||
|
self.api_version = api_version
|
||||||
|
|
||||||
# extensions
|
# extensions
|
||||||
self.agents = agents.AgentsManager(self)
|
self.agents = agents.AgentsManager(self)
|
||||||
@ -224,6 +227,7 @@ class Client(object):
|
|||||||
connection_pool=connection_pool,
|
connection_pool=connection_pool,
|
||||||
session=session,
|
session=session,
|
||||||
auth=auth,
|
auth=auth,
|
||||||
|
api_version=api_version,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
@client._original_only
|
@client._original_only
|
||||||
|
Loading…
x
Reference in New Issue
Block a user