Added support for Client creation from KS session

In this changeset, I refactored some of the code to now support
the creation of a watcher client using a Keystone session as it was
already possible in most other OS projects.

Change-Id: I3462daeaac9103138366a0fcaa2fc46729bbd8ce
Related-Bug: #1625643
Closes-Bug: #1623070
This commit is contained in:
Vincent Francoise 2016-09-21 17:47:19 +02:00
parent a48a9213f0
commit 940a7093e6
25 changed files with 1562 additions and 1512 deletions

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# 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
@ -12,112 +10,176 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystoneclient.v2_0 import client as ksclient
import oslo_i18n
from keystoneauth1 import loading as kaloading
from oslo_utils import importutils
from watcherclient._i18n import _
from watcherclient.common import utils
from watcherclient import exceptions as exc
oslo_i18n.install('watcherclient')
from watcherclient.common import api_versioning
from watcherclient import exceptions
def _get_ksclient(**kwargs):
"""Get an endpoint and auth token from Keystone.
:param kwargs: keyword args containing credentials:
* username: name of user
* password: user's password
* auth_url: endpoint to authenticate against
* insecure: allow insecure SSL (no cert verification)
* tenant_{name|id}: name or ID of tenant
"""
return ksclient.Client(username=kwargs.get('username'),
password=kwargs.get('password'),
tenant_id=kwargs.get('tenant_id'),
tenant_name=kwargs.get('tenant_name'),
auth_url=kwargs.get('auth_url'),
insecure=kwargs.get('insecure'))
def _get_endpoint(client, **kwargs):
"""Get an endpoint using the provided keystone client."""
attr = None
filter_value = None
if kwargs.get('region_name'):
attr = 'region'
filter_value = kwargs.get('region_name')
return client.service_catalog.url_for(
service_type=kwargs.get('service_type') or 'infra-optim',
attr=attr,
filter_value=filter_value,
endpoint_type=kwargs.get('endpoint_type') or 'publicURL')
def get_client(api_version, **kwargs):
"""Get an authenticated client, based on the credentials in args.
def get_client(api_version, os_auth_token=None, watcher_url=None,
os_username=None, os_password=None, os_auth_url=None,
os_project_id=None, os_project_name=None, os_tenant_id=None,
os_tenant_name=None, os_region_name=None,
os_user_domain_id=None, os_user_domain_name=None,
os_project_domain_id=None, os_project_domain_name=None,
os_service_type=None, os_endpoint_type=None,
insecure=None, timeout=None, os_cacert=None, ca_file=None,
os_cert=None, cert_file=None, os_key=None, key_file=None,
os_watcher_api_version=None, max_retries=None,
retry_interval=None, session=None, os_endpoint_override=None,
**ignored_kwargs):
"""Get an authenticated client, based on the credentials.
:param api_version: the API version to use. Valid value: '1'.
:param kwargs: keyword args containing credentials, either:
* os_auth_token: pre-existing token to re-use
* watcher_url: watcher API endpoint
or:
* os_username: name of user
* os_password: user's password
* os_auth_url: endpoint to authenticate against
* insecure: allow insecure SSL (no cert verification)
* os_tenant_{name|id}: name or ID of tenant
:param os_auth_token: pre-existing token to re-use
:param watcher_url: watcher API endpoint
:param os_username: name of a user
:param os_password: user's password
:param os_auth_url: endpoint to authenticate against
:param os_project_id: ID of a project
:param os_project_name: name of a project
:param os_tenant_id: ID of a tenant (deprecated in favour of
os_project_id)
:param os_tenant_name: name of a tenant (deprecated in favour of
os_project_name)
:param os_region_name: name of a keystone region
:param os_user_domain_id: ID of a domain the user belongs to
:param os_user_domain_name: name of a domain the user belongs to
:param os_project_domain_id: ID of a domain the project belongs to
:param os_project_domain_name: name of a domain the project belongs to
:param os_service_type: the type of service to lookup the endpoint for
:param os_endpoint_type: the type (exposure) of the endpoint
:param insecure: allow insecure SSL (no cert verification)
:param timeout: allows customization of the timeout for client HTTP
requests
:param os_cacert: path to cacert file
:param ca_file: path to cacert file, deprecated in favour of os_cacert
:param os_cert: path to cert file
:param cert_file: path to cert file, deprecated in favour of os_cert
:param os_key: path to key file
:param key_file: path to key file, deprecated in favour of os_key
:param os_watcher_api_version: watcher API version to use
:param max_retries: Maximum number of retries in case of conflict error
:param retry_interval: Amount of time (in seconds) between retries in case
of conflict error
:param session: Keystone session to use
:param os_endpoint_override: watcher API endpoint
:param ignored_kwargs: all the other params that are passed. Left for
backwards compatibility. They are ignored.
"""
if kwargs.get('os_auth_token') and kwargs.get('watcher_url'):
token = kwargs.get('os_auth_token')
endpoint = kwargs.get('watcher_url')
auth_ref = None
elif (kwargs.get('os_username') and
kwargs.get('os_password') and
kwargs.get('os_auth_url') and
(kwargs.get('os_tenant_id') or kwargs.get('os_tenant_name'))):
ks_kwargs = {
'username': kwargs.get('os_username'),
'password': kwargs.get('os_password'),
'tenant_id': kwargs.get('os_tenant_id'),
'tenant_name': kwargs.get('os_tenant_name'),
'auth_url': kwargs.get('os_auth_url'),
'service_type': kwargs.get('os_service_type'),
'endpoint_type': kwargs.get('os_endpoint_type'),
'insecure': kwargs.get('insecure'),
}
_ksclient = _get_ksclient(**ks_kwargs)
token = (kwargs.get('os_auth_token')
if kwargs.get('os_auth_token')
else _ksclient.auth_token)
ks_kwargs['region_name'] = kwargs.get('os_region_name')
endpoint = (kwargs.get('watcher_url') or
_get_endpoint(_ksclient, **ks_kwargs))
auth_ref = _ksclient.auth_ref
else:
e = (_('Must provide Keystone credentials or user-defined endpoint '
'and token'))
raise exc.AmbiguousAuthSystem(e)
cli_kwargs = {
'token': token,
'insecure': kwargs.get('insecure'),
'timeout': kwargs.get('timeout'),
'ca_file': kwargs.get('ca_file'),
'cert_file': kwargs.get('cert_file'),
'key_file': kwargs.get('key_file'),
'auth_ref': auth_ref,
os_service_type = os_service_type or 'infra-optim'
os_endpoint_type = os_endpoint_type or 'publicURL'
project_id = (os_project_id or os_tenant_id)
project_name = (os_project_name or os_tenant_name)
kwargs = {
'os_watcher_api_version': os_watcher_api_version,
'max_retries': max_retries,
'retry_interval': retry_interval,
}
endpoint = watcher_url or os_endpoint_override
cacert = os_cacert or ca_file
cert = os_cert or cert_file
key = os_key or key_file
if os_auth_token and endpoint:
kwargs.update({
'token': os_auth_token,
'insecure': insecure,
'ca_file': cacert,
'cert_file': cert,
'key_file': key,
'timeout': timeout,
})
elif os_auth_url:
auth_type = 'password'
auth_kwargs = {
'auth_url': os_auth_url,
'project_id': project_id,
'project_name': project_name,
'user_domain_id': os_user_domain_id,
'user_domain_name': os_user_domain_name,
'project_domain_id': os_project_domain_id,
'project_domain_name': os_project_domain_name,
}
if os_username and os_password:
auth_kwargs.update({
'username': os_username,
'password': os_password,
})
elif os_auth_token:
auth_type = 'token'
auth_kwargs.update({
'token': os_auth_token,
})
# Create new session only if it was not passed in
if not session:
loader = kaloading.get_plugin_loader(auth_type)
auth_plugin = loader.load_from_options(**auth_kwargs)
# Let keystoneauth do the necessary parameter conversions
session = kaloading.session.Session().load_from_options(
auth=auth_plugin, insecure=insecure, cacert=cacert,
cert=cert, key=key, timeout=timeout,
)
return Client(api_version, endpoint, **cli_kwargs)
exception_msg = _('Must provide Keystone credentials or user-defined '
'endpoint and token')
if not endpoint:
if session:
try:
# Pass the endpoint, it will be used to get hostname
# and port that will be used for API version caching. It will
# be also set as endpoint_override.
endpoint = session.get_endpoint(
service_type=os_service_type,
interface=os_endpoint_type,
region_name=os_region_name
)
except Exception as e:
raise exceptions.AmbiguousAuthSystem(
exception_msg + _(', error was: %s') % e)
else:
# Neither session, nor valid auth parameters provided
raise exceptions.AmbiguousAuthSystem(exception_msg)
# Always pass the session
kwargs['session'] = session
return Client(api_version, endpoint, **kwargs)
def _get_client_class_and_version(version):
if not isinstance(version, api_versioning.APIVersion):
version = api_versioning.get_api_version(version)
else:
api_versioning.check_major_version(version)
if version.is_latest():
raise exceptions.UnsupportedVersion(
_("The version should be explicit, not latest."))
return version, importutils.import_class(
"watcherclient.v%s.client.Client" % version.ver_major)
def Client(version, *args, **kwargs):
module = utils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
"""Initialize client object based on given version.
HOW-TO:
The simplest way to create a client instance is initialization with your
credentials::
>>> from watcherclient import client
>>> watcher = client.Client(VERSION, USERNAME, PASSWORD,
... PROJECT_ID, AUTH_URL)
Here ``VERSION`` can be a string or
``watcherclient.api_versions.APIVersion`` obj. If you prefer string value,
you can use ``1`` or ``1.X`` (where X is a microversion).
Alternatively, you can create a client instance using the keystoneauth
session API. See "The watcherclient Python API" page at
python-watcherclient's doc.
"""
api_version, client_class = _get_client_class_and_version(version)
return client_class(*args, **kwargs)

@ -0,0 +1,215 @@
#
# 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 watcherclient._i18n import _, _LW
from watcherclient import exceptions
LOG = logging.getLogger(__name__)
if not LOG.handlers:
LOG.addHandler(logging.StreamHandler())
HEADER_NAME = "OpenStack-API-Version"
SERVICE_TYPE = "infra-optim"
# key is a deprecated version and value is an alternative version.
DEPRECATED_VERSIONS = {}
_type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'")
class APIVersion(object):
"""This class represents an API Version Request.
This class provides 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.
:param version_str: String representation of APIVersionRequest.
Correct format is 'X.Y', where 'X' and 'Y'
are int values. None value should be used
to create Null APIVersionRequest, which is
equal to 0.0
"""
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):
"""Matches the version object.
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):
"""Version string representation.
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(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 watcherclient.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

@ -497,7 +497,7 @@ class Resource(object):
def get(self):
"""Support for lazy loading details.
Some clients, such as novaclient have the option to lazy load the
Some clients, such as watcherclient have the option to lazy load the
details, details which can be loaded with this function.
"""
# set_loaded() first ... so if we have to bail, we know we tried.

@ -433,11 +433,7 @@ def from_response(response, method, url):
:param method: HTTP method used for request
:param url: URL used for request
"""
req_id = response.headers.get("x-openstack-request-id")
# NOTE(hdd) true for older versions of nova and cinder
if not req_id:
req_id = response.headers.get("x-compute-request-id")
kwargs = {
"http_status": response.status_code,
"response": response,

@ -1,388 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# 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 copy
import json
import logging
import os
import socket
import ssl
from keystoneclient import adapter
import six
import six.moves.urllib.parse as urlparse
from watcherclient._i18n import _, _LW, _LE
from watcherclient import exceptions as exc
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-watcherclient'
CHUNKSIZE = 1024 * 64 # 64kB
API_VERSION = '/v1'
def _trim_endpoint_api_version(url):
"""Trim API version and trailing slash from endpoint."""
return url.rstrip('/').rstrip(API_VERSION)
def _extract_error_json(body):
"""Return error_message from the HTTP response body."""
error_json = {}
if six.PY3 and not isinstance(body, six.string_types):
body = body.decode("utf-8")
try:
body_json = json.loads(body)
if 'error_message' in body_json:
raw_msg = body_json['error_message']
error_json = json.loads(raw_msg)
except ValueError:
pass
return error_json
class HTTPClient(object):
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
self.endpoint_trimmed = _trim_endpoint_api_version(endpoint)
self.auth_token = kwargs.get('token')
self.auth_ref = kwargs.get('auth_ref')
self.connection_params = self.get_connection_params(endpoint, **kwargs)
@staticmethod
def get_connection_params(endpoint, **kwargs):
parts = urlparse.urlparse(endpoint)
path = _trim_endpoint_api_version(parts.path)
_args = (parts.hostname, parts.port, path)
_kwargs = {'timeout': (float(kwargs.get('timeout'))
if kwargs.get('timeout') else 600)}
if parts.scheme == 'https':
_class = VerifiedHTTPSConnection
_kwargs['ca_file'] = kwargs.get('ca_file', None)
_kwargs['cert_file'] = kwargs.get('cert_file', None)
_kwargs['key_file'] = kwargs.get('key_file', None)
_kwargs['insecure'] = kwargs.get('insecure', False)
elif parts.scheme == 'http':
_class = six.moves.http_client.HTTPConnection
else:
raise exc.EndpointException(
_('Unsupported scheme: %s'), parts.scheme)
return (_class, _args, _kwargs)
def get_connection(self):
_class = self.connection_params[0]
try:
return _class(*self.connection_params[1][0:2],
**self.connection_params[2])
except six.moves.http_client.InvalidURL:
raise exc.EndpointException()
def log_curl_request(self, method, url, kwargs):
curl = ['curl -i -X %s' % method]
for (key, value) in kwargs['headers'].items():
header = '-H \'%s: %s\'' % (key, value)
curl.append(header)
conn_params_fmt = [
('key_file', '--key %s'),
('cert_file', '--cert %s'),
('ca_file', '--cacert %s'),
]
for (key, fmt) in conn_params_fmt:
value = self.connection_params[2].get(key)
if value:
curl.append(fmt % value)
if self.connection_params[2].get('insecure'):
curl.append('-k')
if 'body' in kwargs:
curl.append('-d \'%s\'' % kwargs['body'])
curl.append(urlparse.urljoin(self.endpoint_trimmed, url))
LOG.debug(' '.join(curl))
@staticmethod
def log_http_response(resp, body=None):
status = (resp.version / 10.0, resp.status, resp.reason)
dump = ['\nHTTP/%.1f %s %s' % status]
dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()])
dump.append('')
if body:
dump.extend([body, ''])
LOG.debug('\n'.join(dump))
def _make_connection_url(self, url):
(_class, _args, _kwargs) = self.connection_params
base_url = _args[2]
return '%s/%s' % (base_url, url.lstrip('/'))
def _http_request(self, url, method, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
as setting headers and error handling.
"""
# Copy the kwargs so we can reuse the original in case of redirects
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
if self.auth_token:
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
self.log_curl_request(method, url, kwargs)
conn = self.get_connection()
try:
conn_url = self._make_connection_url(url)
conn.request(method, conn_url, **kwargs)
resp = conn.getresponse()
except socket.gaierror as e:
raise exc.EndpointNotFound(
_("Error finding address for %(url)s: %(e)s"),
url=url, e=e)
except (socket.error, socket.timeout) as e:
endpoint = self.endpoint
raise exc.CommunicationError(
_("Error communicating with %(endpoint)s %(e)s"),
endpoint=endpoint, e=e)
body_iter = ResponseBodyIterator(resp)
# Read body into string if it isn't obviously image data
body_str = None
if resp.getheader('content-type', None) != 'application/octet-stream':
body_str = ''.join([chunk for chunk in body_iter])
self.log_http_response(resp, body_str)
body_iter = six.StringIO(body_str)
else:
self.log_http_response(resp)
if 400 <= resp.status < 600:
LOG.warning(_LW("Request returned failure status."))
error_json = _extract_error_json(body_str)
raise exc.from_response(
resp, error_json.get('faultstring'),
error_json.get('debuginfo'), method, url)
elif resp.status in (301, 302, 305):
# Redirected. Reissue the request to the new location.
return self._http_request(resp['location'], method, **kwargs)
elif resp.status == 300:
raise exc.from_response(resp, method=method, url=url)
return resp, body_iter
def json_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', 'application/json')
kwargs['headers'].setdefault('Accept', 'application/json')
if 'body' in kwargs:
kwargs['body'] = json.dumps(kwargs['body'])
resp, body_iter = self._http_request(url, method, **kwargs)
content_type = resp.getheader('content-type', None)
if resp.status == 204 or resp.status == 205 or content_type is None:
return resp, list()
if 'application/json' in content_type:
body = ''.join([chunk for chunk in body_iter])
try:
body = json.loads(body)
except ValueError:
LOG.error(_LE('Could not decode response body as JSON'))
else:
body = None
return resp, body
def raw_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type',
'application/octet-stream')
return self._http_request(url, method, **kwargs)
class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
"""httplib-compatibile connection using client-side SSL authentication
:see http://code.activestate.com/recipes/
577548-https-httplib-client-connection-with-certificate-v/
"""
def __init__(self, host, port, key_file=None, cert_file=None,
ca_file=None, timeout=None, insecure=False):
six.moves.http_client.HTTPSConnection.__init__(
self, host, port,
key_file=key_file,
cert_file=cert_file)
self.key_file = key_file
self.cert_file = cert_file
if ca_file is not None:
self.ca_file = ca_file
else:
self.ca_file = self.get_system_ca_file()
self.timeout = timeout
self.insecure = insecure
def connect(self):
"""Connect to a host on a given (SSL) port.
If ca_file is pointing somewhere, use it to check Server Certificate.
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
ssl.wrap_socket(), which forces SSL to check server certificate against
our client certificate.
"""
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
self.sock = sock
self._tunnel()
if self.insecure is True:
kwargs = {'cert_reqs': ssl.CERT_NONE}
else:
kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
if self.cert_file:
kwargs['certfile'] = self.cert_file
if self.key_file:
kwargs['keyfile'] = self.key_file
self.sock = ssl.wrap_socket(sock, **kwargs)
@staticmethod
def get_system_ca_file():
"""Return path to system default CA file."""
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/ca-bundle.pem',
'/etc/ssl/cert.pem']
for ca in ca_path:
if os.path.exists(ca):
return ca
return None
class SessionClient(adapter.LegacyJsonAdapter):
"""HTTP client based on Keystone client session."""
def _http_request(self, url, method, **kwargs):
kwargs.setdefault('user_agent', USER_AGENT)
kwargs.setdefault('auth', self.auth)
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
endpoint_filter.setdefault('interface', self.interface)
endpoint_filter.setdefault('service_type', self.service_type)
endpoint_filter.setdefault('region_name', self.region_name)
resp = self.session.request(url, method,
raise_exc=False, **kwargs)
if 400 <= resp.status_code < 600:
error_json = _extract_error_json(resp.content)
raise exc.from_response(resp, error_json.get(
'faultstring'),
error_json.get('debuginfo'), method, url)
elif resp.status_code in (301, 302, 305):
# Redirected. Reissue the request to the new location.
location = resp.headers.get('location')
resp = self._http_request(location, method, **kwargs)
elif resp.status_code == 300:
raise exc.from_response(resp, method=method, url=url)
return resp
def json_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', 'application/json')
kwargs['headers'].setdefault('Accept', 'application/json')
if 'body' in kwargs:
kwargs['data'] = json.dumps(kwargs.pop('body'))
resp = self._http_request(url, method, **kwargs)
body = resp.content
content_type = resp.headers.get('content-type', None)
status = resp.status_code
if status == 204 or status == 205 or content_type is None:
return resp, list()
if 'application/json' in content_type:
try:
body = resp.json()
except ValueError:
LOG.error(_LE('Could not decode response body as JSON'))
else:
body = None
return resp, body
def raw_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type',
'application/octet-stream')
return self._http_request(url, method, **kwargs)
class ResponseBodyIterator(object):
"""A class that acts as an iterator over an HTTP response."""
def __init__(self, resp):
self.resp = resp
def __iter__(self):
while True:
yield self.next()
def next(self):
chunk = self.resp.read(CHUNKSIZE)
if chunk:
if six.PY3 and not isinstance(chunk, six.string_types):
chunk = chunk.decode("utf-8")
return chunk
else:
raise StopIteration()
def _construct_http_client(*args, **kwargs):
session = kwargs.pop('session', None)
auth = kwargs.pop('auth', None)
if session:
service_type = kwargs.pop('service_type', 'infra-optim')
interface = kwargs.pop('endpoint_type', None)
region_name = kwargs.pop('region_name', None)
return SessionClient(session=session,
auth=auth,
interface=interface,
service_type=service_type,
region_name=region_name,
service_name=None,
user_agent='python-watcherclient')
else:
return HTTPClient(*args, **kwargs)

@ -0,0 +1,632 @@
# Copyright 2012 OpenStack LLC.
# 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 copy
from distutils import version
import functools
import hashlib
import json
import logging
import os
import socket
import ssl
import textwrap
import time
from keystoneauth1 import adapter
from keystoneauth1 import exceptions as kexceptions
from oslo_utils import strutils
import requests
import six
from six.moves import http_client
import six.moves.urllib.parse as urlparse
from watcherclient._i18n import _, _LE, _LW
from watcherclient import exceptions
# NOTE(deva): Record the latest version that this client was tested with.
# We still have a lot of work to do in the client to implement
# microversion support in the client properly! See
# http://specs.openstack.org/openstack/watcher-specs/specs/kilo/api-microversions.html # noqa
# for full details.
DEFAULT_VER = '1.0'
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-watcherclient'
CHUNKSIZE = 1024 * 64 # 64kB
API_VERSION = '/v1'
API_VERSION_SELECTED_STATES = ('user', 'negotiated', 'cached', 'default')
DEFAULT_MAX_RETRIES = 5
DEFAULT_RETRY_INTERVAL = 2
SENSITIVE_HEADERS = ('X-Auth-Token',)
SUPPORTED_ENDPOINT_SCHEME = ('http', 'https')
def _trim_endpoint_api_version(url):
"""Trim API version and trailing slash from endpoint."""
return url.rstrip('/').rstrip(API_VERSION)
def _extract_error_json(body):
"""Return error_message from the HTTP response body."""
error_json = {}
try:
body_json = json.loads(body)
if 'error_message' in body_json:
raw_msg = body_json['error_message']
error_json = json.loads(raw_msg)
except ValueError:
pass
return error_json
def get_server(endpoint):
"""Extract and return the server & port that we're connecting to."""
if endpoint is None:
return None, None
parts = urlparse.urlparse(endpoint)
return parts.hostname, str(parts.port)
class VersionNegotiationMixin(object):
def negotiate_version(self, conn, resp):
"""Negotiate the server version
Assumption: Called after receiving a 406 error when doing a request.
param conn: A connection object
param resp: The response object from http request
"""
if self.api_version_select_state not in API_VERSION_SELECTED_STATES:
raise RuntimeError(
_('Error: self.api_version_select_state should be one of the '
'values in: "%(valid)s" but had the value: "%(value)s"') %
{'valid': ', '.join(API_VERSION_SELECTED_STATES),
'value': self.api_version_select_state})
min_ver, max_ver = self._parse_version_headers(resp)
# NOTE: servers before commit 32fb6e99 did not return version headers
# on error, so we need to perform a GET to determine
# the supported version range
if not max_ver:
LOG.debug('No version header in response, requesting from server')
if self.os_watcher_api_version:
base_version = ("/v%s" %
str(self.os_watcher_api_version).split('.')[0])
else:
base_version = API_VERSION
resp = self._make_simple_request(conn, 'GET', base_version)
min_ver, max_ver = self._parse_version_headers(resp)
# If the user requested an explicit version or we have negotiated a
# version and still failing then error now. The server could
# support the version requested but the requested operation may not
# be supported by the requested version.
if self.api_version_select_state == 'user':
raise exceptions.UnsupportedVersion(textwrap.fill(
_("Requested API version %(req)s is not supported by the "
"server or the requested operation is not supported by the "
"requested version. Supported version range is %(min)s to "
"%(max)s")
% {'req': self.os_watcher_api_version,
'min': min_ver, 'max': max_ver}))
if self.api_version_select_state == 'negotiated':
raise exceptions.UnsupportedVersion(textwrap.fill(
_("No API version was specified and the requested operation "
"was not supported by the client's negotiated API version "
"%(req)s. Supported version range is: %(min)s to %(max)s")
% {'req': self.os_watcher_api_version,
'min': min_ver, 'max': max_ver}))
negotiated_ver = str(
min(version.StrictVersion(self.os_watcher_api_version),
version.StrictVersion(max_ver)))
if negotiated_ver < min_ver:
negotiated_ver = min_ver
# server handles microversions, but doesn't support
# the requested version, so try a negotiated version
self.api_version_select_state = 'negotiated'
self.os_watcher_api_version = negotiated_ver
LOG.debug('Negotiated API version is %s', negotiated_ver)
return negotiated_ver
def _generic_parse_version_headers(self, accessor_func):
min_ver = accessor_func('X-OpenStack-Watcher-API-Minimum-Version',
None)
max_ver = accessor_func('X-OpenStack-Watcher-API-Maximum-Version',
None)
return min_ver, max_ver
def _parse_version_headers(self, accessor_func):
# NOTE(jlvillal): Declared for unit testing purposes
raise NotImplementedError()
def _make_simple_request(self, conn, method, url):
# NOTE(jlvillal): Declared for unit testing purposes
raise NotImplementedError()
_RETRY_EXCEPTIONS = (exceptions.Conflict,
exceptions.ServiceUnavailable,
exceptions.ConnectionRefused,
kexceptions.RetriableConnectionFailure)
def with_retries(func):
"""Wrapper for _http_request adding support for retries."""
@functools.wraps(func)
def wrapper(self, url, method, **kwargs):
if self.conflict_max_retries is None:
self.conflict_max_retries = DEFAULT_MAX_RETRIES
if self.conflict_retry_interval is None:
self.conflict_retry_interval = DEFAULT_RETRY_INTERVAL
num_attempts = self.conflict_max_retries + 1
for attempt in range(1, num_attempts + 1):
try:
return func(self, url, method, **kwargs)
except _RETRY_EXCEPTIONS as error:
msg = (_LE("Error contacting Watcher server: %(error)s. "
"Attempt %(attempt)d of %(total)d") %
{'attempt': attempt,
'total': num_attempts,
'error': error})
if attempt == num_attempts:
LOG.error(msg)
raise
else:
LOG.debug(msg)
time.sleep(self.conflict_retry_interval)
return wrapper
class HTTPClient(VersionNegotiationMixin):
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
self.endpoint_trimmed = _trim_endpoint_api_version(endpoint)
self.auth_token = kwargs.get('token')
self.auth_ref = kwargs.get('auth_ref')
self.os_watcher_api_version = kwargs.get('os_watcher_api_version',
DEFAULT_VER)
self.api_version_select_state = kwargs.get(
'api_version_select_state', 'default')
self.conflict_max_retries = kwargs.pop('max_retries',
DEFAULT_MAX_RETRIES)
self.conflict_retry_interval = kwargs.pop('retry_interval',
DEFAULT_RETRY_INTERVAL)
self.session = requests.Session()
parts = urlparse.urlparse(endpoint)
if parts.scheme not in SUPPORTED_ENDPOINT_SCHEME:
msg = _('Unsupported scheme: %s') % parts.scheme
raise exceptions.EndpointException(msg)
if parts.scheme == 'https':
if kwargs.get('insecure') is True:
self.session.verify = False
elif kwargs.get('ca_file'):
self.session.verify = kwargs['ca_file']
self.session.cert = (kwargs.get('cert_file'),
kwargs.get('key_file'))
def _process_header(self, name, value):
"""Redacts any sensitive header
Redact a header that contains sensitive information, by returning an
updated header with the sha1 hash of that value. The redacted value is
prefixed by '{SHA1}' because that's the convention used within
OpenStack.
:returns: A tuple of (name, value)
name: the safe encoding format of name
value: the redacted value if name is x-auth-token,
or the safe encoding format of name
"""
if name in SENSITIVE_HEADERS:
v = value.encode('utf-8')
h = hashlib.sha1(v)
d = h.hexdigest()
return (name, "{SHA1}%s" % d)
else:
return (name, value)
def log_curl_request(self, method, url, kwargs):
curl = ['curl -i -X %s' % method]
for (key, value) in kwargs['headers'].items():
header = '-H \'%s: %s\'' % self._process_header(key, value)
curl.append(header)
if not self.session.verify:
curl.append('-k')
elif isinstance(self.session.verify, six.string_types):
curl.append('--cacert %s' % self.session.verify)
if self.session.cert:
curl.append('--cert %s' % self.session.cert[0])
curl.append('--key %s' % self.session.cert[1])
if 'body' in kwargs:
body = strutils.mask_password(kwargs['body'])
curl.append('-d \'%s\'' % body)
curl.append(urlparse.urljoin(self.endpoint_trimmed, url))
LOG.debug(' '.join(curl))
@staticmethod
def log_http_response(resp, body=None):
# NOTE(aarefiev): resp.raw is urllib3 response object, it's used
# only to get 'version', response from request with 'stream = True'
# should be used for raw reading.
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
dump = ['\nHTTP/%.1f %s %s' % status]
dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()])
dump.append('')
if body:
body = strutils.mask_password(body)
dump.extend([body, ''])
LOG.debug('\n'.join(dump))
def _make_connection_url(self, url):
return urlparse.urljoin(self.endpoint_trimmed, url)
def _parse_version_headers(self, resp):
return self._generic_parse_version_headers(resp.headers.get)
def _make_simple_request(self, conn, method, url):
return conn.request(method, self._make_connection_url(url))
@with_retries
def _http_request(self, url, method, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around request.Session.request to handle tasks such
as setting headers and error handling.
"""
# Copy the kwargs so we can reuse the original in case of redirects
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
if self.os_watcher_api_version:
kwargs['headers'].setdefault('X-OpenStack-Watcher-API-Version',
self.os_watcher_api_version)
if self.auth_token:
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
self.log_curl_request(method, url, kwargs)
# NOTE(aarefiev): This is for backwards compatibility, request
# expected body in 'data' field, previously we used httplib,
# which expected 'body' field.
body = kwargs.pop('body', None)
if body:
kwargs['data'] = body
conn_url = self._make_connection_url(url)
try:
resp = self.session.request(method,
conn_url,
**kwargs)
# TODO(deva): implement graceful client downgrade when connecting
# to servers that did not support microversions. Details here:
# http://specs.openstack.org/openstack/watcher-specs/specs/kilo/api-microversions.html#use-case-3b-new-client-communicating-with-a-old-watcher-user-specified # noqa
if resp.status_code == http_client.NOT_ACCEPTABLE:
negotiated_ver = self.negotiate_version(self.session, resp)
kwargs['headers']['X-OpenStack-Watcher-API-Version'] = (
negotiated_ver)
return self._http_request(url, method, **kwargs)
except requests.exceptions.RequestException as e:
message = (_("Error has occurred while handling "
"request for %(url)s: %(e)s") %
dict(url=conn_url, e=e))
# NOTE(aarefiev): not valid request(invalid url, missing schema,
# and so on), retrying is not needed.
if isinstance(e, ValueError):
raise exceptions.ValidationError(message)
raise exceptions.ConnectionRefused(message)
body_iter = resp.iter_content(chunk_size=CHUNKSIZE)
# Read body into string if it isn't obviously image data
body_str = None
if resp.headers.get('Content-Type') != 'application/octet-stream':
body_str = ''.join([chunk for chunk in body_iter])
self.log_http_response(resp, body_str)
body_iter = six.StringIO(body_str)
else:
self.log_http_response(resp)
if resp.status_code >= http_client.BAD_REQUEST:
error_json = _extract_error_json(body_str)
raise exceptions.from_response(
resp, error_json.get('faultstring'),
error_json.get('debuginfo'), method, url)
elif resp.status_code in (http_client.MOVED_PERMANENTLY,
http_client.FOUND,
http_client.USE_PROXY):
# Redirected. Reissue the request to the new location.
return self._http_request(resp['location'], method, **kwargs)
elif resp.status_code == http_client.MULTIPLE_CHOICES:
raise exceptions.from_response(resp, method=method, url=url)
return resp, body_iter
def json_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', 'application/json')
kwargs['headers'].setdefault('Accept', 'application/json')
if 'body' in kwargs:
kwargs['body'] = json.dumps(kwargs['body'])
resp, body_iter = self._http_request(url, method, **kwargs)
content_type = resp.headers.get('Content-Type')
if (resp.status_code in (http_client.NO_CONTENT,
http_client.RESET_CONTENT)
or content_type is None):
return resp, list()
if 'application/json' in content_type:
body = ''.join([chunk for chunk in body_iter])
try:
body = json.loads(body)
except ValueError:
LOG.error(_LE('Could not decode response body as JSON'))
else:
body = None
return resp, body
def raw_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type',
'application/octet-stream')
return self._http_request(url, method, **kwargs)
class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
"""httplib-compatible connection using client-side SSL authentication
:see http://code.activestate.com/recipes/
577548-https-httplib-client-connection-with-certificate-v/
"""
def __init__(self, host, port, key_file=None, cert_file=None,
ca_file=None, timeout=None, insecure=False):
six.moves.http_client.HTTPSConnection.__init__(self, host, port,
key_file=key_file,
cert_file=cert_file)
self.key_file = key_file
self.cert_file = cert_file
if ca_file is not None:
self.ca_file = ca_file
else:
self.ca_file = self.get_system_ca_file()
self.timeout = timeout
self.insecure = insecure
def connect(self):
"""Connect to a host on a given (SSL) port.
If ca_file is pointing somewhere, use it to check Server Certificate.
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
ssl.wrap_socket(), which forces SSL to check server certificate against
our client certificate.
"""
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
self.sock = sock
self._tunnel()
if self.insecure is True:
kwargs = {'cert_reqs': ssl.CERT_NONE}
else:
kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
if self.cert_file:
kwargs['certfile'] = self.cert_file
if self.key_file:
kwargs['keyfile'] = self.key_file
self.sock = ssl.wrap_socket(sock, **kwargs)
@staticmethod
def get_system_ca_file():
"""Return path to system default CA file."""
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/ca-bundle.pem',
'/etc/ssl/cert.pem']
for ca in ca_path:
if os.path.exists(ca):
return ca
return None
class SessionClient(VersionNegotiationMixin, adapter.LegacyJsonAdapter):
"""HTTP client based on Keystone client session."""
def __init__(self,
os_watcher_api_version,
api_version_select_state,
max_retries,
retry_interval,
endpoint,
**kwargs):
self.os_watcher_api_version = os_watcher_api_version
self.api_version_select_state = api_version_select_state
self.conflict_max_retries = max_retries
self.conflict_retry_interval = retry_interval
self.endpoint = endpoint
super(SessionClient, self).__init__(**kwargs)
def _parse_version_headers(self, resp):
return self._generic_parse_version_headers(resp.headers.get)
def _make_simple_request(self, conn, method, url):
# NOTE: conn is self.session for this class
return conn.request(url, method, raise_exc=False)
@with_retries
def _http_request(self, url, method, **kwargs):
kwargs.setdefault('user_agent', USER_AGENT)
kwargs.setdefault('auth', self.auth)
if isinstance(self.endpoint_override, six.string_types):
kwargs.setdefault(
'endpoint_override',
_trim_endpoint_api_version(self.endpoint_override)
)
if getattr(self, 'os_watcher_api_version', None):
kwargs['headers'].setdefault('X-OpenStack-Watcher-API-Version',
self.os_watcher_api_version)
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
endpoint_filter.setdefault('interface', self.interface)
endpoint_filter.setdefault('service_type', self.service_type)
endpoint_filter.setdefault('region_name', self.region_name)
resp = self.session.request(url, method,
raise_exc=False, **kwargs)
if resp.status_code == http_client.NOT_ACCEPTABLE:
negotiated_ver = self.negotiate_version(self.session, resp)
kwargs['headers']['X-OpenStack-Watcher-API-Version'] = (
negotiated_ver)
return self._http_request(url, method, **kwargs)
if resp.status_code >= http_client.BAD_REQUEST:
error_json = _extract_error_json(resp.content)
raise exceptions.from_response(
resp, error_json.get('faultstring'),
error_json.get('debuginfo'), method, url)
elif resp.status_code in (http_client.MOVED_PERMANENTLY,
http_client.FOUND, http_client.USE_PROXY):
# Redirected. Reissue the request to the new location.
location = resp.headers.get('location')
resp = self._http_request(location, method, **kwargs)
elif resp.status_code == http_client.MULTIPLE_CHOICES:
raise exceptions.from_response(resp, method=method, url=url)
return resp
def json_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', 'application/json')
kwargs['headers'].setdefault('Accept', 'application/json')
if 'body' in kwargs:
kwargs['data'] = json.dumps(kwargs.pop('body'))
resp = self._http_request(url, method, **kwargs)
body = resp.content
content_type = resp.headers.get('content-type', None)
status = resp.status_code
if (status in (http_client.NO_CONTENT, http_client.RESET_CONTENT) or
content_type is None):
return resp, list()
if 'application/json' in content_type:
try:
body = resp.json()
except ValueError:
LOG.error(_LE('Could not decode response body as JSON'))
else:
body = None
return resp, body
def raw_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type',
'application/octet-stream')
return self._http_request(url, method, **kwargs)
def _construct_http_client(endpoint=None,
session=None,
token=None,
auth_ref=None,
os_watcher_api_version=DEFAULT_VER,
api_version_select_state='default',
max_retries=DEFAULT_MAX_RETRIES,
retry_interval=DEFAULT_RETRY_INTERVAL,
timeout=600,
ca_file=None,
cert_file=None,
key_file=None,
insecure=None,
**kwargs):
if session:
kwargs.setdefault('service_type', 'infra-optim')
kwargs.setdefault('user_agent', 'python-watcherclient')
kwargs.setdefault('interface', kwargs.pop('endpoint_type', None))
kwargs.setdefault('endpoint_override', endpoint)
ignored = {'token': token,
'auth_ref': auth_ref,
'timeout': timeout != 600,
'ca_file': ca_file,
'cert_file': cert_file,
'key_file': key_file,
'insecure': insecure}
dvars = [k for k, v in ignored.items() if v]
if dvars:
LOG.warning(_LW('The following arguments are ignored when using '
'the session to construct a client: %s'),
', '.join(dvars))
return SessionClient(session=session,
os_watcher_api_version=os_watcher_api_version,
api_version_select_state=api_version_select_state,
max_retries=max_retries,
retry_interval=retry_interval,
endpoint=endpoint,
**kwargs)
else:
if kwargs:
LOG.warning(_LW('The following arguments are being ignored when '
'constructing the client: %s'), ', '.join(kwargs))
return HTTPClient(endpoint=endpoint,
token=token,
auth_ref=auth_ref,
os_watcher_api_version=os_watcher_api_version,
api_version_select_state=api_version_select_state,
max_retries=max_retries,
retry_interval=retry_interval,
timeout=timeout,
ca_file=ca_file,
cert_file=cert_file,
key_file=key_file,
insecure=insecure)

@ -53,6 +53,20 @@ A generic error message, given when no more specific message is suitable.
An alias of :py:exc:`watcherclient.common.apiclient.ValidationError`
"""
Conflict = exceptions.Conflict
ConnectionRefused = exceptions.ConnectionRefused
EndpointException = exceptions.EndpointException
EndpointNotFound = exceptions.EndpointNotFound
ServiceUnavailable = exceptions.ServiceUnavailable
class UnsupportedVersion(Exception):
"""Unsupported API Version
Indicates that the user is trying to use an unsupported version of the API.
"""
pass
class AmbiguousAuthSystem(exceptions.ClientException):
"""Could not obtain token and endpoint using provided credentials."""
@ -86,11 +100,10 @@ def from_response(response, message=None, traceback=None, method=None,
'Content-Type': response.getheader('content-type', "")}
if hasattr(response, 'status_code'):
# NOTE(jiangfei): These modifications allow SessionClient
# to handle faultstring.
# NOTE(hongbin): This allows SessionClient to handle faultstring.
response.json = lambda: {'error': error_body}
if (response.headers['Content-Type'].startswith('text/') and
if (response.headers.get('Content-Type', '').startswith('text/') and
not hasattr(response, 'text')):
# NOTE(clif_h): There seems to be a case in the
# common.apiclient.exceptions module where if the
@ -100,4 +113,4 @@ def from_response(response, message=None, traceback=None, method=None,
# This is to work around that problem.
response.text = ''
return exceptions.from_response(response, message, url)
return exceptions.from_response(response, method, url)

@ -26,32 +26,16 @@ API_VERSIONS = {
def make_client(instance):
"""Returns an infra-optim service client"""
watcher_client = utils.get_client_class(
"""Returns an infra-optim service client."""
infraoptim_client_class = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
API_VERSIONS)
LOG.debug('Instantiating infra-optim client: %s', watcher_client)
LOG.debug('Instantiating infraoptim client: %s', infraoptim_client_class)
endpoint = instance.get_endpoint_for_service_type(
API_NAME,
region_name=instance._region_name,
interface=instance._interface,
)
auth_url = instance._auth_url \
if hasattr(instance, '_auth_url') else instance.auth.auth_url
username = instance._username \
if hasattr(instance, '_username') else instance.auth._username
password = instance._password \
if hasattr(instance, '_password') else instance.auth._password
client = watcher_client(
endpoint=endpoint,
client = infraoptim_client_class(
os_watcher_api_version=instance._api_version[API_NAME],
session=instance.session,
auth_url=auth_url,
username=username,
password=password,
region_name=instance._region_name,
)

@ -25,17 +25,11 @@ from cliff import command
from cliff import commandmanager
from cliff import complete
from cliff import help as cli_help
from keystoneclient.auth.identity import v2
from keystoneclient.auth.identity import v3
from keystoneclient import discover
from keystoneclient import exceptions as ks_exc
from keystoneclient import session
from osc_lib import logs
from osc_lib import utils
import six.moves.urllib.parse as urlparse
from watcherclient._i18n import _
from watcherclient import exceptions as exc
from watcherclient import client as watcherclient
from watcherclient import version
LOG = logging.getLogger(__name__)
@ -75,145 +69,9 @@ class WatcherShell(app.App):
)
def create_client(self, args):
service_type = 'infra-optim'
project_id = args.os_project_id or args.os_tenant_id
project_name = args.os_project_name or args.os_tenant_name
keystone_session = session.Session.load_from_cli_options(args)
kwargs = {
'username': args.os_username,
'user_domain_id': args.os_user_domain_id,
'user_domain_name': args.os_user_domain_name,
'password': args.os_password,
'auth_token': args.os_auth_token,
'project_id': project_id,
'project_name': project_name,
'project_domain_id': args.os_project_domain_id,
'project_domain_name': args.os_project_domain_name,
}
keystone_auth = self._get_keystone_auth(keystone_session,
args.os_auth_url,
**kwargs)
region_name = args.os_region_name
endpoint = keystone_auth.get_endpoint(keystone_session,
service_type=service_type,
region_name=region_name)
endpoint_type = args.os_endpoint_type or 'publicURL'
kwargs = {
'auth_url': args.os_auth_url,
'session': keystone_session,
'auth': keystone_auth,
'service_type': service_type,
'endpoint_type': endpoint_type,
'region_name': args.os_region_name,
'username': args.os_username,
'password': args.os_password,
}
watcher_client = utils.get_client_class(
API_NAME,
args.watcher_api_version or 1,
API_VERSIONS)
LOG.debug('Instantiating infra-optim client: %s', watcher_client)
client = watcher_client(args.watcher_api_version, endpoint, **kwargs)
client = watcherclient.get_client('1', **args.__dict__)
return client
def _discover_auth_versions(self, session, auth_url):
# discover the API versions the server is supporting base on the
# given URL
v2_auth_url = None
v3_auth_url = None
try:
ks_discover = discover.Discover(session=session, auth_url=auth_url)
v2_auth_url = ks_discover.url_for('2.0')
v3_auth_url = ks_discover.url_for('3.0')
except ks_exc.ClientException:
# Identity service may not support discover API version.
# Let's try to figure out the API version from the original URL.
url_parts = urlparse.urlparse(auth_url)
(scheme, netloc, path, params, query, fragment) = url_parts
path = path.lower()
if path.startswith('/v3'):
v3_auth_url = auth_url
elif path.startswith('/v2'):
v2_auth_url = auth_url
else:
# not enough information to determine the auth version
msg = _('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url. Identity service may not support API '
'version discovery. Please provide a versioned '
'auth_url instead. %s') % auth_url
raise exc.CommandError(msg)
return (v2_auth_url, v3_auth_url)
def _get_keystone_v3_auth(self, v3_auth_url, **kwargs):
auth_token = kwargs.pop('auth_token', None)
if auth_token:
return v3.Token(v3_auth_url, auth_token)
else:
return v3.Password(v3_auth_url, **kwargs)
def _get_keystone_v2_auth(self, v2_auth_url, **kwargs):
auth_token = kwargs.pop('auth_token', None)
if auth_token:
return v2.Token(
v2_auth_url,
auth_token,
tenant_id=kwargs.pop('project_id', None),
tenant_name=kwargs.pop('project_name', None))
else:
return v2.Password(
v2_auth_url,
username=kwargs.pop('username', None),
password=kwargs.pop('password', None),
tenant_id=kwargs.pop('project_id', None),
tenant_name=kwargs.pop('project_name', None))
def _get_keystone_auth(self, session, auth_url, **kwargs):
# FIXME(dhu): this code should come from keystoneclient
# discover the supported keystone versions using the given url
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
session=session,
auth_url=auth_url)
# Determine which authentication plugin to use. First inspect the
# auth_url to see the supported version. If both v3 and v2 are
# supported, then use the highest version if possible.
auth = None
if v3_auth_url and v2_auth_url:
user_domain_name = kwargs.get('user_domain_name', None)
user_domain_id = kwargs.get('user_domain_id', None)
project_domain_name = kwargs.get('project_domain_name', None)
project_domain_id = kwargs.get('project_domain_id', None)
# support both v2 and v3 auth. Use v3 if domain information is
# provided.
if (user_domain_name or user_domain_id or project_domain_name or
project_domain_id):
auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs)
else:
auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs)
elif v3_auth_url:
# support only v3
auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs)
elif v2_auth_url:
# support only v2
auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs)
else:
raise exc.CommandError(
_('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url.'))
return auth
def build_option_parser(self, description, version, argparse_kwargs=None):
"""Introduces global arguments for the application.
@ -291,14 +149,20 @@ class WatcherShell(app.App):
metavar='<auth-token>',
default=utils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN].')
parser.add_argument('--watcher-api-version',
metavar='<watcher-api-version>',
default=utils.env('WATCHER_API_VERSION'),
help='Defaults to env[WATCHER_API_VERSION].')
parser.add_argument('--os-watcher-api-version',
metavar='<os-watcher-api-version>',
default=utils.env('OS_WATCHER_API_VERSION',
default='1'),
help='Defaults to env[OS_WATCHER_API_VERSION].')
parser.add_argument('--os-endpoint-type',
default=utils.env('OS_ENDPOINT_TYPE'),
help='Defaults to env[OS_ENDPOINT_TYPE] or '
'"publicURL"')
parser.add_argument('--os-endpoint-override',
metavar='<endpoint-override>',
default=utils.env('OS_ENDPOINT_OVERRIDE'),
help="Use this API endpoint instead of the "
"Service Catalog.")
parser.epilog = ('See "watcher help COMMAND" for help '
'on a specific command.')
session.Session.register_cli_options(parser)

@ -0,0 +1,150 @@
# Copyright 2016 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 watcherclient.common import api_versioning
from watcherclient import exceptions
from watcherclient.tests import utils
class APIVersionTestCase(utils.BaseTestCase):
def test_valid_version_strings(self):
def _test_string(version, exp_major, exp_minor):
v = api_versioning.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_versioning.APIVersion()
self.assertTrue(v.is_null())
def test_invalid_version_strings(self):
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "2")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "200")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "2.1.4")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "200.23.66.3")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "5 .3")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "5. 3")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "5.03")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "02.1")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "2.001")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, " 2.1")
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.APIVersion, "2.1 ")
def test_version_comparisons(self):
v1 = api_versioning.APIVersion("2.0")
v2 = api_versioning.APIVersion("2.5")
v3 = api_versioning.APIVersion("5.23")
v4 = api_versioning.APIVersion("2.0")
v_null = api_versioning.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_versioning.APIVersion("2.0")
v2 = api_versioning.APIVersion("2.5")
v3 = api_versioning.APIVersion("2.45")
v4 = api_versioning.APIVersion("3.3")
v5 = api_versioning.APIVersion("3.23")
v6 = api_versioning.APIVersion("2.0")
v7 = api_versioning.APIVersion("3.3")
v8 = api_versioning.APIVersion("4.0")
v_null = api_versioning.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_versioning.APIVersion(v1_string)
self.assertEqual(v1_string, v1.get_string())
self.assertRaises(ValueError,
api_versioning.APIVersion().get_string)
class GetAPIVersionTestCase(utils.BaseTestCase):
def test_get_available_client_versions(self):
output = api_versioning.get_available_major_versions()
self.assertNotEqual([], output)
def test_wrong_format(self):
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.get_api_version, "something_wrong")
def test_wrong_major_version(self):
self.assertRaises(exceptions.UnsupportedVersion,
api_versioning.get_api_version, "2")
@mock.patch("watcherclient.common.api_versioning.APIVersion")
def test_only_major_part_is_presented(self, mock_apiversion):
version = 7
self.assertEqual(mock_apiversion.return_value,
api_versioning.get_api_version(version))
mock_apiversion.assert_called_once_with("%s.0" % str(version))
@mock.patch("watcherclient.common.api_versioning.APIVersion")
def test_major_and_minor_parts_is_presented(self, mock_apiversion):
version = "2.7"
self.assertEqual(mock_apiversion.return_value,
api_versioning.get_api_version(version))
mock_apiversion.assert_called_once_with(version)

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# 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
@ -12,35 +10,50 @@
# License for the specific language governing permissions and limitations
# under the License.
import fixtures
import mock
from watcherclient.client import get_client
from watcherclient import exceptions as exc
from keystoneauth1 import loading as kaloading
from watcherclient import client as watcherclient
from watcherclient.common import httpclient
from watcherclient import exceptions
from watcherclient.tests import utils
def fake_get_ksclient(**kwargs):
return utils.FakeKeystone('KSCLIENT_AUTH_TOKEN')
class ClientTest(utils.BaseTestCase):
def test_get_client_with_auth_token_watcher_url(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'watcher_url': 'http://watcher.example.org:6385/',
'watcher_url': 'http://watcher.example.org:9322/',
'os_auth_token': 'USER_AUTH_TOKEN',
}
client = get_client('1', **kwargs)
client = watcherclient.get_client('1', **kwargs)
self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://watcher.example.org:6385/',
self.assertEqual('http://watcher.example.org:9322/',
client.http_client.endpoint)
@mock.patch.object(kaloading.session, 'Session', autospec=True)
@mock.patch.object(kaloading, 'get_plugin_loader', autospec=True)
def _test_get_client(self, mock_ks_loader, mock_ks_session,
version=None, auth='password', **kwargs):
session = mock_ks_session.return_value.load_from_options.return_value
session.get_endpoint.return_value = 'http://localhost:9322/v1/f14b4123'
mock_ks_loader.return_value.load_from_options.return_value = 'auth'
watcherclient.get_client('1', **kwargs)
mock_ks_loader.assert_called_once_with(auth)
mock_ks_session.return_value.load_from_options.assert_called_once_with(
auth='auth', timeout=kwargs.get('timeout'),
insecure=kwargs.get('insecure'), cert=kwargs.get('cert'),
cacert=kwargs.get('cacert'), key=kwargs.get('key'))
session.get_endpoint.assert_called_once_with(
service_type=kwargs.get('os_service_type') or 'infra-optim',
interface=kwargs.get('os_endpoint_type') or 'publicURL',
region_name=kwargs.get('os_region_name'))
def test_get_client_no_auth_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
@ -48,17 +61,21 @@ class ClientTest(utils.BaseTestCase):
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': '',
}
client = get_client('1', **kwargs)
self._test_get_client(**kwargs)
self.assertEqual('KSCLIENT_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://localhost:6385/v1/f14b41234',
client.http_client.endpoint)
self.assertEqual(fake_get_ksclient().auth_ref,
client.http_client.auth_ref)
def test_get_client_service_and_endpoint_type_defaults(self):
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': '',
'os_service_type': '',
'os_endpoint_type': ''
}
self._test_get_client(**kwargs)
def test_get_client_with_region_no_auth_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
@ -67,76 +84,269 @@ class ClientTest(utils.BaseTestCase):
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': '',
}
client = get_client('1', **kwargs)
self._test_get_client(**kwargs)
self.assertEqual('KSCLIENT_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://regionhost:6385/v1/f14b41234',
client.http_client.endpoint)
self.assertEqual(fake_get_ksclient().auth_ref,
client.http_client.auth_ref)
def test_get_client_with_auth_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': 'USER_AUTH_TOKEN',
}
client = get_client('1', **kwargs)
self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://localhost:6385/v1/f14b41234',
client.http_client.endpoint)
self.assertEqual(fake_get_ksclient().auth_ref,
client.http_client.auth_ref)
def test_get_client_with_region_name_auth_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_region_name': 'REGIONONE',
'os_auth_token': 'USER_AUTH_TOKEN',
}
client = get_client('1', **kwargs)
self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://regionhost:6385/v1/f14b41234',
client.http_client.endpoint)
self.assertEqual(fake_get_ksclient().auth_ref,
client.http_client.auth_ref)
def test_get_client_no_url_and_no_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
def test_get_client_no_url(self):
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': '',
'os_auth_token': '',
}
self.assertRaises(exc.AmbiguousAuthSystem, get_client, '1', **kwargs)
self.assertRaises(
exceptions.AmbiguousAuthSystem, watcherclient.get_client,
'1', **kwargs)
# test the alias as well to ensure backwards compatibility
self.assertRaises(exc.AmbigiousAuthSystem, get_client, '1', **kwargs)
self.assertRaises(
exceptions.AmbigiousAuthSystem, watcherclient.get_client,
'1', **kwargs)
def test_ensure_auth_ref_propagated(self):
ksclient = fake_get_ksclient
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', ksclient))
def test_get_client_incorrect_auth_params(self):
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_auth_url': 'http://localhost:35357/v2.0',
}
self.assertRaises(
exceptions.AmbiguousAuthSystem, watcherclient.get_client,
'1', **kwargs)
def test_get_client_with_api_version_latest(self):
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': '',
'os_watcher_api_version': "latest",
}
client = get_client('1', **kwargs)
self._test_get_client(**kwargs)
self.assertEqual(ksclient().auth_ref, client.http_client.auth_ref)
def test_get_client_with_api_version_numeric(self):
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': '',
'os_watcher_api_version': "1.4",
}
self._test_get_client(**kwargs)
def test_get_client_with_auth_token(self):
kwargs = {
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': 'USER_AUTH_TOKEN',
}
self._test_get_client(auth='token', **kwargs)
def test_get_client_with_region_name_auth_token(self):
kwargs = {
'os_auth_url': 'http://localhost:35357/v2.0',
'os_region_name': 'REGIONONE',
'os_auth_token': 'USER_AUTH_TOKEN',
}
self._test_get_client(auth='token', **kwargs)
def test_get_client_only_session_passed(self):
session = mock.Mock()
session.get_endpoint.return_value = 'http://localhost:35357/v2.0'
kwargs = {
'session': session,
}
watcherclient.get_client('1', **kwargs)
session.get_endpoint.assert_called_once_with(
service_type='infra-optim',
interface='publicURL',
region_name=None)
def test_get_client_incorrect_session_passed(self):
session = mock.Mock()
session.get_endpoint.side_effect = Exception('boo')
kwargs = {
'session': session,
}
self.assertRaises(
exceptions.AmbiguousAuthSystem, watcherclient.get_client,
'1', **kwargs)
@mock.patch.object(kaloading.session, 'Session', autospec=True)
@mock.patch.object(kaloading, 'get_plugin_loader', autospec=True)
def _test_loader_arguments_passed_correctly(
self, mock_ks_loader, mock_ks_session,
passed_kwargs, expected_kwargs):
session = mock_ks_session.return_value.load_from_options.return_value
session.get_endpoint.return_value = 'http://localhost:9322/v1/f14b4123'
mock_ks_loader.return_value.load_from_options.return_value = 'auth'
watcherclient.get_client('1', **passed_kwargs)
mock_ks_loader.return_value.load_from_options.assert_called_once_with(
**expected_kwargs)
mock_ks_session.return_value.load_from_options.assert_called_once_with(
auth='auth', timeout=passed_kwargs.get('timeout'),
insecure=passed_kwargs.get('insecure'),
cert=passed_kwargs.get('cert'),
cacert=passed_kwargs.get('cacert'), key=passed_kwargs.get('key'))
session.get_endpoint.assert_called_once_with(
service_type=passed_kwargs.get('os_service_type') or 'infra-optim',
interface=passed_kwargs.get('os_endpoint_type') or 'publicURL',
region_name=passed_kwargs.get('os_region_name'))
def test_loader_arguments_token(self):
passed_kwargs = {
'os_auth_url': 'http://localhost:35357/v3',
'os_region_name': 'REGIONONE',
'os_auth_token': 'USER_AUTH_TOKEN',
}
expected_kwargs = {
'auth_url': 'http://localhost:35357/v3',
'project_id': None,
'project_name': None,
'user_domain_id': None,
'user_domain_name': None,
'project_domain_id': None,
'project_domain_name': None,
'token': 'USER_AUTH_TOKEN'
}
self._test_loader_arguments_passed_correctly(
passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs)
def test_loader_arguments_password_tenant_name(self):
passed_kwargs = {
'os_auth_url': 'http://localhost:35357/v3',
'os_region_name': 'REGIONONE',
'os_tenant_name': 'TENANT',
'os_username': 'user',
'os_password': '1234',
'os_project_domain_id': 'DEFAULT',
'os_user_domain_id': 'DEFAULT'
}
expected_kwargs = {
'auth_url': 'http://localhost:35357/v3',
'project_id': None,
'project_name': 'TENANT',
'user_domain_id': 'DEFAULT',
'user_domain_name': None,
'project_domain_id': 'DEFAULT',
'project_domain_name': None,
'username': 'user',
'password': '1234'
}
self._test_loader_arguments_passed_correctly(
passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs)
def test_loader_arguments_password_project_id(self):
passed_kwargs = {
'os_auth_url': 'http://localhost:35357/v3',
'os_region_name': 'REGIONONE',
'os_project_id': '1000',
'os_username': 'user',
'os_password': '1234',
'os_project_domain_name': 'domain1',
'os_user_domain_name': 'domain1'
}
expected_kwargs = {
'auth_url': 'http://localhost:35357/v3',
'project_id': '1000',
'project_name': None,
'user_domain_id': None,
'user_domain_name': 'domain1',
'project_domain_id': None,
'project_domain_name': 'domain1',
'username': 'user',
'password': '1234'
}
self._test_loader_arguments_passed_correctly(
passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs)
@mock.patch.object(watcherclient, 'Client')
@mock.patch.object(kaloading.session, 'Session', autospec=True)
def test_correct_arguments_passed_to_client_constructor_noauth_mode(
self, mock_ks_session, mock_client):
kwargs = {
'watcher_url': 'http://watcher.example.org:9322/',
'os_auth_token': 'USER_AUTH_TOKEN',
'os_watcher_api_version': 'latest',
'insecure': True,
'max_retries': 10,
'retry_interval': 10,
'os_cacert': 'data'
}
watcherclient.get_client('1', **kwargs)
mock_client.assert_called_once_with(
'1', 'http://watcher.example.org:9322/',
**{
'os_watcher_api_version': 'latest',
'max_retries': 10,
'retry_interval': 10,
'token': 'USER_AUTH_TOKEN',
'insecure': True,
'ca_file': 'data',
'cert_file': None,
'key_file': None,
'timeout': None,
'session': None
}
)
self.assertFalse(mock_ks_session.called)
@mock.patch.object(watcherclient, 'Client')
@mock.patch.object(kaloading.session, 'Session', autospec=True)
def test_correct_arguments_passed_to_client_constructor_session_created(
self, mock_ks_session, mock_client):
session = mock_ks_session.return_value.load_from_options.return_value
kwargs = {
'os_auth_url': 'http://localhost:35357/v3',
'os_region_name': 'REGIONONE',
'os_project_id': '1000',
'os_username': 'user',
'os_password': '1234',
'os_project_domain_name': 'domain1',
'os_user_domain_name': 'domain1'
}
watcherclient.get_client('1', **kwargs)
mock_client.assert_called_once_with(
'1', session.get_endpoint.return_value,
**{
'os_watcher_api_version': None,
'max_retries': None,
'retry_interval': None,
'session': session,
}
)
@mock.patch.object(watcherclient, 'Client')
@mock.patch.object(kaloading.session, 'Session', autospec=True)
def test_correct_arguments_passed_to_client_constructor_session_passed(
self, mock_ks_session, mock_client):
session = mock.Mock()
kwargs = {
'session': session,
}
watcherclient.get_client('1', **kwargs)
mock_client.assert_called_once_with(
'1', session.get_endpoint.return_value,
**{
'os_watcher_api_version': None,
'max_retries': None,
'retry_interval': None,
'session': session,
}
)
self.assertFalse(mock_ks_session.called)
def test_safe_header_with_auth_token(self):
(name, value) = ('X-Auth-Token', u'3b640e2e64d946ac8f55615aff221dc1')
expected_header = (u'X-Auth-Token',
'{SHA1}6de9fb3b0b89099030a54abfeb468e7b1b1f0f2b')
client = httpclient.HTTPClient('http://localhost/')
header_redact = client._process_header(name, value)
self.assertEqual(expected_header, header_redact)
def test_safe_header_with_no_auth_token(self):
name, value = ('Accept', 'application/json')
header = ('Accept', 'application/json')
client = httpclient.HTTPClient('http://localhost/')
header_redact = client._process_header(name, value)
self.assertEqual(header, header_redact)

@ -1,283 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# 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 json
import six
from watcherclient.common import http
from watcherclient import exceptions as exc
from watcherclient.tests import utils
HTTP_CLASS = six.moves.http_client.HTTPConnection
HTTPS_CLASS = http.VerifiedHTTPSConnection
DEFAULT_TIMEOUT = 600
def _get_error_body(faultstring=None, debuginfo=None):
error_body = {
'faultstring': faultstring,
'debuginfo': debuginfo
}
raw_error_body = json.dumps(error_body)
body = {'error_message': raw_error_body}
raw_body = json.dumps(body)
return raw_body
class HttpClientTest(utils.BaseTestCase):
def test_url_generation_trailing_slash_in_base(self):
client = http.HTTPClient('http://localhost/')
url = client._make_connection_url('/v1/resources')
self.assertEqual('/v1/resources', url)
def test_url_generation_without_trailing_slash_in_base(self):
client = http.HTTPClient('http://localhost')
url = client._make_connection_url('/v1/resources')
self.assertEqual('/v1/resources', url)
def test_url_generation_prefix_slash_in_path(self):
client = http.HTTPClient('http://localhost/')
url = client._make_connection_url('/v1/resources')
self.assertEqual('/v1/resources', url)
def test_url_generation_without_prefix_slash_in_path(self):
client = http.HTTPClient('http://localhost')
url = client._make_connection_url('v1/resources')
self.assertEqual('/v1/resources', url)
def test_server_exception_empty_body(self):
error_body = _get_error_body()
fake_resp = utils.FakeResponse({'content-type': 'application/json'},
six.StringIO(error_body),
version=1,
status=500)
client = http.HTTPClient('http://localhost/')
client.get_connection = (
lambda *a, **kw: utils.FakeConnection(fake_resp))
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual('Internal Server Error (HTTP 500)', str(error))
def test_server_exception_msg_only(self):
error_msg = 'test error msg'
error_body = _get_error_body(error_msg)
fake_resp = utils.FakeResponse({'content-type': 'application/json'},
six.StringIO(error_body),
version=1,
status=500)
client = http.HTTPClient('http://localhost/')
client.get_connection = (
lambda *a, **kw: utils.FakeConnection(fake_resp))
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual(error_msg + ' (HTTP 500)', str(error))
def test_server_exception_msg_and_traceback(self):
error_msg = 'another test error'
error_trace = ("\"Traceback (most recent call last):\\n\\n "
"File \\\"/usr/local/lib/python2.7/...")
error_body = _get_error_body(error_msg, error_trace)
fake_resp = utils.FakeResponse({'content-type': 'application/json'},
six.StringIO(error_body),
version=1,
status=500)
client = http.HTTPClient('http://localhost/')
client.get_connection = (
lambda *a, **kw: utils.FakeConnection(fake_resp))
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual(
'%(error)s (HTTP 500)\n%(trace)s' % {'error': error_msg,
'trace': error_trace},
"%(error)s\n%(details)s" % {'error': str(error),
'details': str(error.details)})
def test_get_connection_params(self):
endpoint = 'http://watcher-host:6385'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_trailing_slash(self):
endpoint = 'http://watcher-host:6385/'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_ssl(self):
endpoint = 'https://watcher-host:6385'
expected = (HTTPS_CLASS,
('watcher-host', 6385, ''),
{
'timeout': DEFAULT_TIMEOUT,
'ca_file': None,
'cert_file': None,
'key_file': None,
'insecure': False,
})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_ssl_params(self):
endpoint = 'https://watcher-host:6385'
ssl_args = {
'ca_file': '/path/to/ca_file',
'cert_file': '/path/to/cert_file',
'key_file': '/path/to/key_file',
'insecure': True,
}
expected_kwargs = {'timeout': DEFAULT_TIMEOUT}
expected_kwargs.update(ssl_args)
expected = (HTTPS_CLASS,
('watcher-host', 6385, ''),
expected_kwargs)
params = http.HTTPClient.get_connection_params(endpoint, **ssl_args)
self.assertEqual(expected, params)
def test_get_connection_params_with_timeout(self):
endpoint = 'http://watcher-host:6385'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': 300.0})
params = http.HTTPClient.get_connection_params(endpoint, timeout=300)
self.assertEqual(expected, params)
def test_get_connection_params_with_version(self):
endpoint = 'http://watcher-host:6385/v1'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_version_trailing_slash(self):
endpoint = 'http://watcher-host:6385/v1/'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_subpath(self):
endpoint = 'http://watcher-host:6385/watcher'
expected = (HTTP_CLASS,
('watcher-host', 6385, '/watcher'),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_subpath_trailing_slash(self):
endpoint = 'http://watcher-host:6385/watcher/'
expected = (HTTP_CLASS,
('watcher-host', 6385, '/watcher'),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_subpath_version(self):
endpoint = 'http://watcher-host:6385/watcher/v1'
expected = (HTTP_CLASS,
('watcher-host', 6385, '/watcher'),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_subpath_version_trailing_slash(self):
endpoint = 'http://watcher-host:6385/watcher/v1/'
expected = (HTTP_CLASS,
('watcher-host', 6385, '/watcher'),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_401_unauthorized_exception(self):
error_body = _get_error_body()
fake_resp = utils.FakeResponse({'content-type': 'text/plain'},
six.StringIO(error_body),
version=1,
status=401)
client = http.HTTPClient('http://localhost/')
client.get_connection = (
lambda *a, **kw: utils.FakeConnection(fake_resp))
self.assertRaises(exc.Unauthorized, client.json_request,
'GET', '/v1/resources')
class SessionClientTest(utils.BaseTestCase):
def test_server_exception_msg_and_traceback(self):
error_msg = 'another test error'
error_trace = ("\"Traceback (most recent call last):\\n\\n "
"File \\\"/usr/local/lib/python2.7/...")
error_body = _get_error_body(error_msg, error_trace)
fake_session = utils.FakeSession({'Content-Type': 'application/json'},
error_body,
500)
client = http.SessionClient(session=fake_session,
auth=None,
interface=None,
service_type='publicURL',
region_name='',
service_name=None)
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual(
'%(error)s (HTTP 500)\n%(trace)s' % {'error': error_msg,
'trace': error_trace},
"%(error)s\n%(details)s" % {'error': str(error),
'details': str(error.details)})
def test_server_exception_empty_body(self):
error_body = _get_error_body()
fake_session = utils.FakeSession({'Content-Type': 'application/json'},
error_body,
500)
client = http.SessionClient(session=fake_session,
auth=None,
interface=None,
service_type='publicURL',
region_name='',
service_name=None)
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual('Internal Server Error (HTTP 500)', str(error))

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
@ -16,18 +14,16 @@
# under the License.
import copy
import datetime
import os
import fixtures
import mock
from oslo_utils import strutils
from oslotest import base
import six
from watcherclient.common import http
import testtools
class BaseTestCase(base.BaseTestCase):
class BaseTestCase(testtools.TestCase):
def setUp(self):
super(BaseTestCase, self).setUp()
@ -55,7 +51,7 @@ class FakeAPI(object):
def raw_request(self, *args, **kwargs):
response = self._request(*args, **kwargs)
body_iter = http.ResponseBodyIterator(six.StringIO(response[1]))
body_iter = iter(six.StringIO(response[1]))
return FakeResponse(response[0]), body_iter
def json_request(self, *args, **kwargs):
@ -77,6 +73,9 @@ class FakeConnection(object):
def getresponse(self):
return self._response
def __repr__(self):
return ("FakeConnection(response=%s)" % (self._response))
class FakeResponse(object):
def __init__(self, headers, body=None, version=None, status=None,
@ -88,8 +87,9 @@ class FakeResponse(object):
"""
self.headers = headers
self.body = body
self.version = version
self.status = status
self.raw = mock.Mock()
self.raw.version = version
self.status_code = status
self.reason = reason
def getheaders(self):
@ -101,44 +101,37 @@ class FakeResponse(object):
def read(self, amt):
return self.body.read(amt)
class FakeServiceCatalog(object):
def url_for(self, endpoint_type, service_type, attr=None,
filter_value=None):
if attr == 'region' and filter_value:
return 'http://regionhost:6385/v1/f14b41234'
else:
return 'http://localhost:6385/v1/f14b41234'
class FakeKeystone(object):
service_catalog = FakeServiceCatalog()
timestamp = datetime.datetime.utcnow() + datetime.timedelta(days=5)
def __init__(self, auth_token):
self.auth_token = auth_token
self.auth_ref = {
'token': {'expires': FakeKeystone.timestamp.strftime(
'%Y-%m-%dT%H:%M:%S.%f'),
'id': 'd1a541311782870742235'}
}
def __repr__(self):
return ("FakeResponse(%s, body=%s, version=%s, status=%s, reason=%s)" %
(self.headers, self.body, self.version, self.status,
self.reason))
class FakeSessionResponse(object):
def __init__(self, headers, content=None, status_code=None):
def __init__(self, headers, content=None, status_code=None, version=None):
self.headers = headers
self.content = content
self.status_code = status_code
self.raw = mock.Mock()
self.raw.version = version
self.reason = ''
def iter_content(self, chunk_size):
return iter(self.content)
class FakeSession(object):
def __init__(self, headers, content=None, status_code=None):
def __init__(self, headers, content=None, status_code=None, version=None):
self.headers = headers
self.content = content
self.status_code = status_code
self.version = version
self.verify = False
self.cert = ('test_cert', 'test_key')
def request(self, url, method, **kwargs):
return FakeSessionResponse(self.headers, self.content,
self.status_code)
request = FakeSessionResponse(
self.headers, self.content, self.status_code, self.version)
return request

@ -18,10 +18,10 @@ import json
import shlex
import mock
from osc_lib import utils as oscutils
from watcherclient import shell
from watcherclient.common import httpclient
from watcherclient.tests import utils
from watcherclient.v1 import client
class CommandTestCase(utils.BaseTestCase):
@ -29,17 +29,29 @@ class CommandTestCase(utils.BaseTestCase):
def setUp(self):
super(CommandTestCase, self).setUp()
self.p_build_http_client = mock.patch.object(
client.Client, 'build_http_client')
self.m_build_http_client = self.p_build_http_client.start()
self.fake_env = {
'debug': False,
'insecure': False,
'no_auth': False,
'os_auth_token': '',
'os_auth_url': 'http://127.0.0.1:5000/v2.0',
'os_endpoint_override': 'http://watcher-endpoint:9322',
'os_username': 'test',
'os_password': 'test',
'timeout': 600,
'os_watcher_api_version': '1'}
self.m_env = mock.Mock(
name='m_env',
side_effect=lambda x, *args, **kwargs: self.fake_env.get(
x.lower(), kwargs.get('default', '')))
self.p_env = mock.patch.object(oscutils, 'env', self.m_env)
self.p_env.start()
self.addCleanup(self.p_env.stop)
self.m_watcher_client = mock.Mock(side_effect=client.Client)
self.p_create_client = mock.patch.object(
shell.WatcherShell, 'create_client', self.m_watcher_client)
self.p_create_client.start()
self.addCleanup(self.p_build_http_client.stop)
self.addCleanup(self.p_create_client.stop)
self.p_construct_http_client = mock.patch.object(
httpclient, '_construct_http_client')
self.m_construct_http_client = self.p_construct_http_client.start()
self.addCleanup(self.p_construct_http_client.stop)
def run_cmd(self, cmd, formatting='json'):
if formatting:

@ -127,7 +127,7 @@ fake_responses_pagination = {
'GET': (
{},
{"actions": [ACTION1],
"next": "http://127.0.0.1:6385/v1/actions/?limit=1"}
"next": "http://127.0.0.1:9322/v1/actions/?limit=1"}
),
},
'/v1/actions/?limit=1':

@ -86,7 +86,7 @@ fake_responses_pagination = {
'GET': (
{},
{"action_plans": [ACTION_PLAN1],
"next": "http://127.0.0.1:6385/v1/action_plans/?limit=1"}
"next": "http://127.0.0.1:9322/v1/action_plans/?limit=1"}
),
},
'/v1/action_plans/?limit=1':

@ -100,7 +100,7 @@ fake_responses_pagination = {
'GET': (
{},
{"audits": [AUDIT1],
"next": "http://127.0.0.1:6385/v1/audits/?limit=1"}
"next": "http://127.0.0.1:9322/v1/audits/?limit=1"}
),
},
'/v1/audits/?limit=1':

@ -157,7 +157,7 @@ fake_responses_pagination = {
'GET': (
{},
{"audit_templates": [AUDIT_TMPL1],
"next": "http://127.0.0.1:6385/v1/audit_templates/?limit=1"}
"next": "http://127.0.0.1:9322/v1/audit_templates/?limit=1"}
),
},
'/v1/audit_templates/?limit=1':

@ -71,7 +71,7 @@ fake_responses_pagination = {
'GET': (
{},
{"goals": [GOAL1],
"next": "http://127.0.0.1:6385/v1/goals/?limit=1"}
"next": "http://127.0.0.1:9322/v1/goals/?limit=1"}
),
},
'/v1/goals/?limit=1':

@ -1,321 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Red Hat, Inc.
# 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 copy
import testtools
from testtools import matchers
from watcherclient.tests import utils
import watcherclient.v1.metric_collector
METRIC_COLLECTOR1 = {
'id': 1,
'uuid': '770ef053-ecb3-48b0-85b5-d55a2dbc6588',
'category': 'cat1',
'endpoint': 'http://metric_collector_1:6446',
}
METRIC_COLLECTOR2 = {
'id': 2,
'uuid': '67653274-eb24-c7ba-70f6-a84e73d80843',
'category': 'cat2',
}
METRIC_COLLECTOR3 = {
'id': 3,
'uuid': 'f8e47706-efcf-49a4-a5c4-af604eb492f2',
'category': 'cat2',
'endpoint': 'http://metric_collector_3:6446',
}
CREATE_METRIC_COLLECTOR = copy.deepcopy(METRIC_COLLECTOR1)
del CREATE_METRIC_COLLECTOR['id']
del CREATE_METRIC_COLLECTOR['uuid']
UPDATED_METRIC_COLLECTOR1 = copy.deepcopy(METRIC_COLLECTOR1)
NEW_ENDPOINT = 'http://metric_collector_1:6447'
UPDATED_METRIC_COLLECTOR1['endpoint'] = NEW_ENDPOINT
fake_responses = {
'/v1/metric-collectors':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1]},
),
'POST': (
{},
CREATE_METRIC_COLLECTOR,
),
},
'/v1/metric-collectors/?category=%s' % METRIC_COLLECTOR1['category']:
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1]},
),
},
'/v1/metric-collectors/?category=%s' % METRIC_COLLECTOR2['category']:
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR3]},
),
},
'/v1/metric-collectors/detail':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1]},
),
},
'/v1/metric-collectors/%s' % METRIC_COLLECTOR1['uuid']:
{
'GET': (
{},
METRIC_COLLECTOR1,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_METRIC_COLLECTOR1,
),
},
'/v1/metric-collectors/detail?category=%s' % METRIC_COLLECTOR1['category']:
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1]},
),
},
'/v1/metric-collectors/detail?category=%s' % METRIC_COLLECTOR2['category']:
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR3]},
),
},
}
fake_responses_pagination = {
'/v1/metric-collectors':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1],
"next": "http://127.0.0.1:6385/v1/metric-collectors/?limit=1"}
),
},
'/v1/metric-collectors/?limit=1':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2]}
),
},
}
fake_responses_sorting = {
'/v1/metric-collectors/?sort_key=updated_at':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR1]}
),
},
'/v1/metric-collectors/?sort_dir=desc':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR1]}
),
},
}
class MetricCollectorManagerTest(testtools.TestCase):
def setUp(self):
super(MetricCollectorManagerTest, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
def test_metric_collectors_list(self):
metric_collectors = self.mgr.list()
expect = [
('GET', '/v1/metric-collectors', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(metric_collectors))
def test_metric_collectors_list_by_category(self):
metric_collectors = self.mgr.list(
category=METRIC_COLLECTOR1['category']
)
expect = [
('GET',
'/v1/metric-collectors/?category=%s' %
METRIC_COLLECTOR1['category'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(metric_collectors))
def test_metric_collectors_list_by_category_bis(self):
metric_collectors = self.mgr.list(
category=METRIC_COLLECTOR2['category']
)
expect = [
('GET',
'/v1/metric-collectors/?category=%s' %
METRIC_COLLECTOR2['category'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(metric_collectors))
def test_metric_collectors_list_detail(self):
metric_collectors = self.mgr.list(detail=True)
expect = [
('GET', '/v1/metric-collectors/detail', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(metric_collectors))
def test_metric_collectors_list_by_category_detail(self):
metric_collectors = self.mgr.list(
category=METRIC_COLLECTOR1['category'],
detail=True)
expect = [
('GET',
'/v1/metric-collectors/detail?category=%s' %
METRIC_COLLECTOR1['category'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(metric_collectors))
def test_metric_collectors_list_by_category_detail_bis(self):
metric_collectors = self.mgr.list(
category=METRIC_COLLECTOR2['category'],
detail=True)
expect = [
('GET',
'/v1/metric-collectors/detail?category=%s' %
METRIC_COLLECTOR2['category'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(metric_collectors))
def test_metric_collectors_list_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
metric_collectors = self.mgr.list(limit=1)
expect = [
('GET', '/v1/metric-collectors/?limit=1', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(metric_collectors, matchers.HasLength(1))
def test_metric_collectors_list_pagination_no_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
metric_collectors = self.mgr.list(limit=0)
expect = [
('GET', '/v1/metric-collectors', {}, None),
('GET', '/v1/metric-collectors/?limit=1', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertThat(metric_collectors, matchers.HasLength(2))
def test_metric_collectors_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
metric_collectors = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/metric-collectors/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(metric_collectors))
def test_metric_collectors_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
metric_collectors = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/metric-collectors/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(metric_collectors))
def test_metric_collectors_show(self):
metric_collector = self.mgr.get(METRIC_COLLECTOR1['uuid'])
expect = [
('GET', '/v1/metric-collectors/%s' %
METRIC_COLLECTOR1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(METRIC_COLLECTOR1['uuid'], metric_collector.uuid)
self.assertEqual(METRIC_COLLECTOR1['category'],
metric_collector.category)
self.assertEqual(METRIC_COLLECTOR1['endpoint'],
metric_collector.endpoint)
def test_create(self):
metric_collector = self.mgr.create(**CREATE_METRIC_COLLECTOR)
expect = [
('POST', '/v1/metric-collectors', {}, CREATE_METRIC_COLLECTOR),
]
self.assertEqual(expect, self.api.calls)
self.assertTrue(metric_collector)
def test_delete(self):
metric_collector = self.mgr.delete(
metric_collector_id=METRIC_COLLECTOR1['uuid'])
expect = [
('DELETE', '/v1/metric-collectors/%s' %
METRIC_COLLECTOR1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertIsNone(metric_collector)
def test_update(self):
patch = {'op': 'replace',
'value': NEW_ENDPOINT,
'path': '/endpoint'}
metric_collector = self.mgr.update(
metric_collector_id=METRIC_COLLECTOR1['uuid'], patch=patch)
expect = [
('PATCH', '/v1/metric-collectors/%s' %
METRIC_COLLECTOR1['uuid'], {}, patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(NEW_ENDPOINT, metric_collector.endpoint)

@ -70,7 +70,7 @@ fake_responses_pagination = {
'GET': (
{},
{"scoring_engines": [SE1],
"next": "http://127.0.0.1:6385/v1/scoring_engines/?limit=1"}
"next": "http://127.0.0.1:9322/v1/scoring_engines/?limit=1"}
),
},
'/v1/scoring_engines/?limit=1':

@ -73,7 +73,7 @@ fake_responses_pagination = {
'GET': (
{},
{"strategies": [STRATEGY1],
"next": "http://127.0.0.1:6385/v1/strategies/?limit=1"}
"next": "http://127.0.0.1:9322/v1/strategies/?limit=1"}
),
},
'/v1/strategies/?limit=1':

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
@ -15,7 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
from watcherclient.common import http
from watcherclient._i18n import _
from watcherclient.common import httpclient
from watcherclient import exceptions
from watcherclient import v1
@ -29,9 +29,25 @@ class Client(object):
http requests. (optional)
"""
def __init__(self, *args, **kwargs):
def __init__(self, endpoint=None, *args, **kwargs):
"""Initialize a new client for the Watcher v1 API."""
self.http_client = self.build_http_client(*args, **kwargs)
if kwargs.get('os_watcher_api_version'):
kwargs['api_version_select_state'] = "user"
else:
if not endpoint:
raise exceptions.EndpointException(
_("Must provide 'endpoint' if os_watcher_api_version "
"isn't specified"))
# If the user didn't specify a version, use a cached version if
# one has been stored
host, netport = httpclient.get_server(endpoint)
kwargs['api_version_select_state'] = "default"
kwargs['os_watcher_api_version'] = httpclient.DEFAULT_VER
self.http_client = httpclient._construct_http_client(
endpoint, *args, **kwargs)
self.audit = v1.AuditManager(self.http_client)
self.audit_template = v1.AuditTemplateManager(self.http_client)
self.action = v1.ActionManager(self.http_client)
@ -39,7 +55,3 @@ class Client(object):
self.goal = v1.GoalManager(self.http_client)
self.scoring_engine = v1.ScoringEngineManager(self.http_client)
self.strategy = v1.StrategyManager(self.http_client)
# self.metric_collector = v1.MetricCollectorManager(self.http_client)
def build_http_client(self, *args, **kwargs):
return http._construct_http_client(*args, **kwargs)

@ -1,101 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
#
# 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.
from watcherclient.common import base
from watcherclient.common import utils
from watcherclient import exceptions as exc
CREATION_ATTRIBUTES = ['endpoint', 'category']
class MetricCollector(base.Resource):
def __repr__(self):
return "<MetricCollector %s>" % self._info
class MetricCollectorManager(base.Manager):
resource_class = MetricCollector
@staticmethod
def _path(id=None):
return \
'/v1/metric-collectors/%s' % id if id else '/v1/metric-collectors'
def list(self, category=None, limit=None, sort_key=None,
sort_dir=None, detail=False):
"""Retrieve a list of metric collector.
:param category: Optional, Metric category, to get all metric
collectors mapped with this category.
:param limit: The maximum number of results to return per
request, if:
1) limit > 0, the maximum number of metric collectors to return.
2) limit == 0, return the entire list of metriccollectors.
3) limit param is NOT specified (None), the number of items
returned respect the maximum imposed by the Watcher API
(see Watcher's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:param detail: Optional, boolean whether to return detailed information
about metric collectors.
:returns: A list of metric collectors.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(limit, sort_key, sort_dir)
if category is not None:
filters.append('category=%s' % category)
path = ''
if detail:
path += 'detail'
if filters:
path += '?' + '&'.join(filters)
if limit is None:
return self._list(self._path(path), "metric-collectors")
else:
return self._list_pagination(self._path(path), "metric-collectors",
limit=limit)
def get(self, metric_collector_id):
try:
return self._list(self._path(metric_collector_id))[0]
except IndexError:
return None
def create(self, **kwargs):
new = {}
for (key, value) in kwargs.items():
if key in CREATION_ATTRIBUTES:
new[key] = value
else:
raise exc.InvalidAttribute()
return self._create(self._path(), new)
def delete(self, metric_collector_id):
return self._delete(self._path(metric_collector_id))
def update(self, metric_collector_id, patch):
return self._update(self._path(metric_collector_id), patch)