monasca-ceilometer/ceilosca/ceilometer/monasca_client.py

260 lines
10 KiB
Python

# Copyright 2015 Hewlett-Packard Company
#
# 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 monascaclient import client
from monascaclient import exc
from monascaclient import ksclient
from oslo_config import cfg
from oslo_log import log
import retrying
from ceilometer.i18n import _, _LW
from ceilometer import keystone_client
monclient_opts = [
cfg.StrOpt('clientapi_version',
default='2_0',
help='Version of Monasca client to use while publishing.'),
cfg.BoolOpt('enable_api_pagination',
default=False,
help='Enable paging through monasca api resultset.'),
]
cfg.CONF.register_opts(monclient_opts, group='monasca')
keystone_client.register_keystoneauth_opts(cfg.CONF)
cfg.CONF.import_group('service_credentials', 'ceilometer.service')
LOG = log.getLogger(__name__)
class MonascaException(Exception):
def __init__(self, message=''):
msg = 'An exception is raised from Monasca: ' + message
super(MonascaException, self).__init__(msg)
class MonascaServiceException(Exception):
def __init__(self, message=''):
msg = 'Monasca service is unavailable: ' + message
super(MonascaServiceException, self).__init__(msg)
class MonascaInvalidServiceCredentialsException(Exception):
pass
class MonascaInvalidParametersException(Exception):
code = 400
def __init__(self, message=''):
msg = 'Request cannot be handled by Monasca: ' + message
super(MonascaInvalidParametersException, self).__init__(msg)
class Client(object):
"""A client which gets information via python-monascaclient."""
_ksclient = None
def __init__(self, parsed_url):
self._retry_interval = cfg.CONF.database.retry_interval * 1000
self._max_retries = cfg.CONF.database.max_retries or 1
# enable monasca api pagination
self._enable_api_pagination = cfg.CONF.monasca.enable_api_pagination
# NOTE(zqfan): There are many concurrency requests while using
# Ceilosca, to save system resource, we don't retry too many times.
if self._max_retries < 0 or self._max_retries > 10:
LOG.warning(_LW('Reduce max retries from %s to 10'),
self._max_retries)
self._max_retries = 10
conf = cfg.CONF.service_credentials
# because our ansible script are in another repo, the old setting
# of auth_type is password-ceilometer-legacy which doesn't register
# os_xxx options, so here we need to provide a compatible way to
# avoid circle dependency
if conf.auth_type == 'password-ceilometer-legacy':
username = conf.os_username
password = conf.os_password
auth_url = conf.os_auth_url
project_id = conf.os_tenant_id
project_name = conf.os_tenant_name
else:
username = conf.username
password = conf.password
auth_url = conf.auth_url
project_id = conf.project_id
project_name = conf.project_name
if not username or not password or not auth_url:
err_msg = _("No user name or password or auth_url "
"found in service_credentials")
LOG.error(err_msg)
raise MonascaInvalidServiceCredentialsException(err_msg)
kwargs = {
'username': username,
'password': password,
'auth_url': auth_url.replace("v2.0", "v3"),
'project_id': project_id,
'project_name': project_name,
'region_name': conf.region_name,
'read_timeout': cfg.CONF.http_timeout,
'write_timeout': cfg.CONF.http_timeout,
}
self._kwargs = kwargs
self._endpoint = parsed_url.netloc + parsed_url.path
LOG.info(_("monasca_client: using %s as monasca end point") %
self._endpoint)
self._refresh_client()
def _refresh_client(self):
if not Client._ksclient:
Client._ksclient = ksclient.KSClient(**self._kwargs)
self._kwargs['token'] = Client._ksclient.token
self._mon_client = client.Client(cfg.CONF.monasca.clientapi_version,
self._endpoint, **self._kwargs)
@staticmethod
def _retry_on_exception(e):
return not isinstance(e, MonascaInvalidParametersException)
def call_func(self, func, **kwargs):
@retrying.retry(wait_fixed=self._retry_interval,
stop_max_attempt_number=self._max_retries,
retry_on_exception=self._retry_on_exception)
def _inner():
try:
return func(**kwargs)
except (exc.HTTPInternalServerError,
exc.HTTPServiceUnavailable,
exc.HTTPBadGateway,
exc.CommunicationError) as e:
LOG.exception(e)
msg = '%s: %s' % (e.__class__.__name__, e)
raise MonascaServiceException(msg)
except exc.HTTPException as e:
LOG.exception(e)
msg = '%s: %s' % (e.__class__.__name__, e)
status_code = e.code
# exc.HTTPException has string code 'N/A'
if not isinstance(status_code, int):
status_code = 500
if 400 <= status_code < 500:
raise MonascaInvalidParametersException(msg)
else:
raise MonascaException(msg)
except Exception as e:
LOG.exception(e)
msg = '%s: %s' % (e.__class__.__name__, e)
raise MonascaException(msg)
return _inner()
def metrics_create(self, **kwargs):
return self.call_func(self._mon_client.metrics.create,
**kwargs)
def metrics_list(self, **kwargs):
"""Using monasca pagination to get all metrics.
We yield endless metrics till caller doesn't want more or
no more is left.
"""
search_args = copy.deepcopy(kwargs)
metrics = self.call_func(self._mon_client.metrics.list, **search_args)
# check if api pagination is enabled
if self._enable_api_pagination:
# page through monasca results
while metrics:
for metric in metrics:
yield metric
# offset for metrics is the last metric's id
search_args['offset'] = metric['id']
metrics = self.call_func(self._mon_client.metrics.list,
**search_args)
else:
for metric in metrics:
yield metric
def metric_names_list(self, **kwargs):
return self.call_func(self._mon_client.metrics.list_names,
**kwargs)
def measurements_list(self, **kwargs):
"""Using monasca pagination to get all measurements.
We yield endless measurements till caller doesn't want more or
no more is left.
"""
search_args = copy.deepcopy(kwargs)
measurements = self.call_func(
self._mon_client.metrics.list_measurements,
**search_args)
# check if api pagination is enabled
if self._enable_api_pagination:
while measurements:
for measurement in measurements:
yield measurement
# offset for measurements is measurement id composited with
# the last measurement's timestamp
search_args['offset'] = '%s_%s' % (
measurement['id'], measurement['measurements'][-1][0])
measurements = self.call_func(
self._mon_client.metrics.list_measurements,
**search_args)
else:
for measurement in measurements:
yield measurement
def statistics_list(self, **kwargs):
"""Using monasca pagination to get all statistics.
We yield endless statistics till caller doesn't want more or
no more is left.
"""
search_args = copy.deepcopy(kwargs)
statistics = self.call_func(self._mon_client.metrics.list_statistics,
**search_args)
# check if api pagination is enabled
if self._enable_api_pagination:
while statistics:
for statistic in statistics:
yield statistic
# with groupby, the offset is unpredictable to me, we don't
# support pagination for it now.
if kwargs.get('group_by'):
break
# offset for statistics is statistic id composited with
# the last statistic's timestamp
search_args['offset'] = '%s_%s' % (
statistic['id'], statistic['statistics'][-1][0])
statistics = self.call_func(
self._mon_client.metrics.list_statistics,
**search_args)
# unlike metrics.list and metrics.list_measurements
# return whole new data, metrics.list_statistics
# next page will use last page's final statistic
# data as the first one, so we need to pop it here.
# I think Monasca should treat this as a bug and fix it.
if statistics:
statistics[0]['statistics'].pop(0)
if len(statistics[0]['statistics']) == 0:
statistics.pop(0)
else:
for statistic in statistics:
yield statistic