Delete v2 gnocchi storage
This is part of a global effort to clean up CloudKitty's unmaintained codebase. This storage backend was only present for development purposes, and not production ready. A second v2 backend will be implemented in the future, with support for HA/clustering. Change-Id: Iab9d152d2851ca385e607d338c0a09b74ba7e3b3 Story: 2004400 Task: 28568
This commit is contained in:
@@ -28,7 +28,6 @@ import cloudkitty.orchestrator
|
|||||||
import cloudkitty.service
|
import cloudkitty.service
|
||||||
import cloudkitty.storage
|
import cloudkitty.storage
|
||||||
import cloudkitty.storage.v1.hybrid.backends.gnocchi
|
import cloudkitty.storage.v1.hybrid.backends.gnocchi
|
||||||
import cloudkitty.storage.v2.gnocchi
|
|
||||||
import cloudkitty.storage.v2.influx
|
import cloudkitty.storage.v2.influx
|
||||||
import cloudkitty.utils
|
import cloudkitty.utils
|
||||||
|
|
||||||
@@ -66,8 +65,6 @@ _opts = [
|
|||||||
cloudkitty.storage.v2.influx.influx_storage_opts))),
|
cloudkitty.storage.v2.influx.influx_storage_opts))),
|
||||||
('storage_gnocchi', list(itertools.chain(
|
('storage_gnocchi', list(itertools.chain(
|
||||||
cloudkitty.storage.v1.hybrid.backends.gnocchi.gnocchi_storage_opts))),
|
cloudkitty.storage.v1.hybrid.backends.gnocchi.gnocchi_storage_opts))),
|
||||||
('storage_gnocchi', list(itertools.chain(
|
|
||||||
cloudkitty.storage.v2.gnocchi.gnocchi_storage_opts))),
|
|
||||||
(None, list(itertools.chain(
|
(None, list(itertools.chain(
|
||||||
cloudkitty.api.app.auth_opts,
|
cloudkitty.api.app.auth_opts,
|
||||||
cloudkitty.service.service_opts))),
|
cloudkitty.service.service_opts))),
|
||||||
|
|||||||
@@ -1,763 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2018 Objectif Libre
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# @author: Luka Peschke
|
|
||||||
#
|
|
||||||
from collections import deque
|
|
||||||
from collections import Iterable
|
|
||||||
import copy
|
|
||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
import time
|
|
||||||
|
|
||||||
from gnocchiclient import auth as gauth
|
|
||||||
from gnocchiclient import client as gclient
|
|
||||||
from gnocchiclient import exceptions as gexceptions
|
|
||||||
from keystoneauth1 import loading as ks_loading
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cloudkitty.storage.v2 import BaseStorage
|
|
||||||
from cloudkitty import utils as ck_utils
|
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
|
|
||||||
|
|
||||||
gnocchi_storage_opts = [
|
|
||||||
cfg.StrOpt(
|
|
||||||
'gnocchi_auth_type',
|
|
||||||
default='keystone',
|
|
||||||
choices=['keystone', 'basic'],
|
|
||||||
help='(v2) Gnocchi auth type (keystone or basic). Keystone '
|
|
||||||
'credentials can be specified through the "auth_section" parameter',
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
|
||||||
'gnocchi_user',
|
|
||||||
default='',
|
|
||||||
help='(v2) Gnocchi user (for basic auth only)',
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
|
||||||
'gnocchi_endpoint',
|
|
||||||
default='',
|
|
||||||
help='(v2) Gnocchi endpoint (for basic auth only)',
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
|
||||||
'api_interface',
|
|
||||||
default='internalURL',
|
|
||||||
help='(v2) Endpoint URL type (for keystone auth only)',
|
|
||||||
),
|
|
||||||
cfg.IntOpt(
|
|
||||||
'measure_chunk_size',
|
|
||||||
min=10, max=1000000,
|
|
||||||
default=500,
|
|
||||||
help='(v2) Maximum amount of measures to send to gnocchi at once '
|
|
||||||
'(defaults to 500).',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
CONF.register_opts(gnocchi_storage_opts, 'storage_gnocchi')
|
|
||||||
ks_loading.register_session_conf_options(CONF, 'storage_gnocchi')
|
|
||||||
ks_loading.register_auth_conf_options(CONF, 'storage_gnocchi')
|
|
||||||
|
|
||||||
|
|
||||||
RESOURCE_TYPE_NAME_ROOT = 'cloudkitty_metric_'
|
|
||||||
ARCHIVE_POLICY_NAME = 'cloudkitty_archive_policy'
|
|
||||||
|
|
||||||
GROUPBY_NAME_ROOT = 'groupby_attr_'
|
|
||||||
META_NAME_ROOT = 'meta_attr_'
|
|
||||||
|
|
||||||
|
|
||||||
class GnocchiResource(object):
|
|
||||||
"""Class representing a gnocchi resource
|
|
||||||
|
|
||||||
It provides utils for resource_type/resource creation and identifying.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, metric, conn):
|
|
||||||
"""Resource_type name, metric, gnocchiclient"""
|
|
||||||
|
|
||||||
self.name = name
|
|
||||||
self.resource_type = RESOURCE_TYPE_NAME_ROOT + name
|
|
||||||
self.unit = metric['vol']['unit']
|
|
||||||
self.groupby = {
|
|
||||||
k: v if v else '' for k, v in metric['groupby'].items()}
|
|
||||||
self.metadata = {
|
|
||||||
k: v if v else '' for k, v in metric['metadata'].items()}
|
|
||||||
self._trans_groupby = {
|
|
||||||
GROUPBY_NAME_ROOT + key: val for key, val in self.groupby.items()
|
|
||||||
}
|
|
||||||
self._trans_metadata = {
|
|
||||||
META_NAME_ROOT + key: val for key, val in self.metadata.items()
|
|
||||||
}
|
|
||||||
self._conn = conn
|
|
||||||
self._resource = None
|
|
||||||
self.attributes = self.metadata.copy()
|
|
||||||
self.attributes.update(self.groupby)
|
|
||||||
self._trans_attributes = self._trans_metadata.copy()
|
|
||||||
self._trans_attributes.update(self._trans_groupby)
|
|
||||||
self.needs_update = False
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
output = self._trans_attributes.get(GROUPBY_NAME_ROOT + key, None)
|
|
||||||
if output is None:
|
|
||||||
output = self._trans_attributes.get(META_NAME_ROOT + key, None)
|
|
||||||
return output
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if self.resource_type != other.resource_type or \
|
|
||||||
self['id'] != other['id']:
|
|
||||||
return False
|
|
||||||
own_keys = list(self.groupby.keys())
|
|
||||||
own_keys.sort()
|
|
||||||
other_keys = list(other.groupby.keys())
|
|
||||||
other_keys.sort()
|
|
||||||
if own_keys != other_keys:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for key in own_keys:
|
|
||||||
if other[key] != self[key]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def qty(self):
|
|
||||||
if self._resource:
|
|
||||||
return self._resource['metrics']['qty']
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cost(self):
|
|
||||||
if self._resource:
|
|
||||||
return self._resource['metrics']['cost']
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_res_type_dict(self):
|
|
||||||
attributes = {}
|
|
||||||
for key in self._trans_groupby.keys():
|
|
||||||
attributes[key] = {'required': True, 'type': 'string'}
|
|
||||||
attributes['unit'] = {'required': True, 'type': 'string'}
|
|
||||||
for key in self._trans_metadata.keys():
|
|
||||||
attributes[key] = {'required': False, 'type': 'string'}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'name': self.resource_type,
|
|
||||||
'attributes': attributes,
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_resource_type(self):
|
|
||||||
"""Allows to create the type corresponding to this resource."""
|
|
||||||
try:
|
|
||||||
self._conn.resource_type.get(self.resource_type)
|
|
||||||
except gexceptions.ResourceTypeNotFound:
|
|
||||||
res_type = self._get_res_type_dict()
|
|
||||||
LOG.debug('Creating resource_type {} in gnocchi'.format(
|
|
||||||
self.resource_type))
|
|
||||||
self._conn.resource_type.create(res_type)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_rfc6902_attributes_add_op(new_attributes):
|
|
||||||
return [{
|
|
||||||
'op': 'add',
|
|
||||||
'path': '/attributes/{}'.format(attr),
|
|
||||||
'value': {
|
|
||||||
'required': attr.startswith(GROUPBY_NAME_ROOT),
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
} for attr in new_attributes]
|
|
||||||
|
|
||||||
def update_resource_type(self):
|
|
||||||
needed_res_type = self._get_res_type_dict()
|
|
||||||
current_res_type = self._conn.resource_type.get(
|
|
||||||
needed_res_type['name'])
|
|
||||||
|
|
||||||
new_attributes = [attr for attr in needed_res_type['attributes'].keys()
|
|
||||||
if attr not in current_res_type['attributes'].keys()]
|
|
||||||
if not new_attributes:
|
|
||||||
return
|
|
||||||
LOG.info('Adding {} to resource_type {}'.format(
|
|
||||||
[attr.replace(GROUPBY_NAME_ROOT, '').replace(META_NAME_ROOT, '')
|
|
||||||
for attr in new_attributes],
|
|
||||||
current_res_type['name'].replace(RESOURCE_TYPE_NAME_ROOT, ''),
|
|
||||||
))
|
|
||||||
new_attributes_op = self._get_rfc6902_attributes_add_op(new_attributes)
|
|
||||||
self._conn.resource_type.update(
|
|
||||||
needed_res_type['name'], new_attributes_op)
|
|
||||||
|
|
||||||
def _create_metrics(self):
|
|
||||||
qty = self._conn.metric.create(
|
|
||||||
name='qty',
|
|
||||||
unit=self.unit,
|
|
||||||
archive_policy_name=ARCHIVE_POLICY_NAME,
|
|
||||||
)
|
|
||||||
cost = self._conn.metric.create(
|
|
||||||
name='cost',
|
|
||||||
archive_policy_name=ARCHIVE_POLICY_NAME,
|
|
||||||
)
|
|
||||||
return qty, cost
|
|
||||||
|
|
||||||
def exists_in_gnocchi(self):
|
|
||||||
"""Check if the resource exists in gnocchi.
|
|
||||||
|
|
||||||
Returns true if the resource exists.
|
|
||||||
"""
|
|
||||||
query = {
|
|
||||||
'and': [
|
|
||||||
{'=': {key: value}}
|
|
||||||
for key, value in self._trans_groupby.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
res = self._conn.resource.search(resource_type=self.resource_type,
|
|
||||||
query=query)
|
|
||||||
if len(res) > 1:
|
|
||||||
LOG.warning(
|
|
||||||
"Found more than one metric matching groupby. This may not "
|
|
||||||
"have the behavior you're expecting. You should probably add "
|
|
||||||
"some items to groupby")
|
|
||||||
if len(res) > 0:
|
|
||||||
self._resource = res[0]
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def create(self):
|
|
||||||
"""Creates the resource in gnocchi."""
|
|
||||||
if self._resource:
|
|
||||||
return
|
|
||||||
self.create_resource_type()
|
|
||||||
qty_metric, cost_metric = self._create_metrics()
|
|
||||||
resource = self._trans_attributes.copy()
|
|
||||||
resource['metrics'] = {
|
|
||||||
'qty': qty_metric['id'],
|
|
||||||
'cost': cost_metric['id'],
|
|
||||||
}
|
|
||||||
resource['id'] = uuidutils.generate_uuid()
|
|
||||||
resource['unit'] = self.unit
|
|
||||||
if not self.exists_in_gnocchi():
|
|
||||||
try:
|
|
||||||
self._resource = self._conn.resource.create(
|
|
||||||
self.resource_type, resource)
|
|
||||||
# Attributes have changed
|
|
||||||
except gexceptions.BadRequest:
|
|
||||||
self.update_resource_type()
|
|
||||||
self._resource = self._conn.resource.create(
|
|
||||||
self.resource_type, resource)
|
|
||||||
|
|
||||||
def update(self, metric):
|
|
||||||
for key, val in metric['metadata'].items():
|
|
||||||
self._resource[META_NAME_ROOT + key] = val
|
|
||||||
self._resource = self._conn.update(
|
|
||||||
self.resource_type, self._resource['id'], self._resource)
|
|
||||||
self.needs_update = False
|
|
||||||
return self._resource
|
|
||||||
|
|
||||||
|
|
||||||
class GnocchiResourceCacher(object):
|
|
||||||
"""Class allowing to keep created resource in memory to improve perfs.
|
|
||||||
|
|
||||||
It keeps the last max_size resources in cache.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, max_size=500):
|
|
||||||
self._resources = deque(maxlen=max_size)
|
|
||||||
|
|
||||||
def __contains__(self, resource):
|
|
||||||
for r in self._resources:
|
|
||||||
if r == resource:
|
|
||||||
for key, val in resource.metadata.items():
|
|
||||||
if val != r[key]:
|
|
||||||
r.needs_update = True
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_resource(self, resource):
|
|
||||||
"""Add a resource to the cacher.
|
|
||||||
|
|
||||||
:param resource: resource to add
|
|
||||||
:type resource: GnocchiResource
|
|
||||||
"""
|
|
||||||
for r in self._resources:
|
|
||||||
if r == resource:
|
|
||||||
return
|
|
||||||
self._resources.append(resource)
|
|
||||||
|
|
||||||
def get(self, resource):
|
|
||||||
"""Returns the resource matching to the parameter.
|
|
||||||
|
|
||||||
:param resource: resource to get
|
|
||||||
:type resource: GnocchiResource
|
|
||||||
"""
|
|
||||||
for r in self._resources:
|
|
||||||
if r == resource:
|
|
||||||
return r
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_by_id(self, resource_id):
|
|
||||||
"""Returns the resource matching the given id.
|
|
||||||
|
|
||||||
:param resource_id: ID of the resource to get
|
|
||||||
:type resource: str
|
|
||||||
"""
|
|
||||||
for r in self._resources:
|
|
||||||
if r['id'] == resource_id:
|
|
||||||
return r
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class GnocchiStorage(BaseStorage):
|
|
||||||
|
|
||||||
default_op = ['aggregate', 'sum', ['metric', 'cost', 'sum'], ]
|
|
||||||
|
|
||||||
def _check_archive_policy(self):
|
|
||||||
try:
|
|
||||||
self._conn.archive_policy.get(ARCHIVE_POLICY_NAME)
|
|
||||||
except gexceptions.ArchivePolicyNotFound:
|
|
||||||
definition = [
|
|
||||||
{'granularity': str(CONF.collect.period) + 's',
|
|
||||||
'timespan': '{d} days'.format(d=self.get_retention().days)},
|
|
||||||
]
|
|
||||||
archive_policy = {
|
|
||||||
'name': ARCHIVE_POLICY_NAME,
|
|
||||||
'back_window': 0,
|
|
||||||
'aggregation_methods': [
|
|
||||||
'std', 'count', 'min', 'max', 'sum', 'mean'],
|
|
||||||
'definition': definition,
|
|
||||||
}
|
|
||||||
self._conn.archive_policy.create(archive_policy)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(GnocchiStorage, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
adapter_options = {'connect_retries': 3}
|
|
||||||
if CONF.storage_gnocchi.gnocchi_auth_type == 'keystone':
|
|
||||||
auth_plugin = ks_loading.load_auth_from_conf_options(
|
|
||||||
CONF,
|
|
||||||
'storage_gnocchi',
|
|
||||||
)
|
|
||||||
adapter_options['interface'] = CONF.storage_gnocchi.api_interface
|
|
||||||
else:
|
|
||||||
auth_plugin = gauth.GnocchiBasicPlugin(
|
|
||||||
user=CONF.storage_gnocchi.gnocchi_user,
|
|
||||||
endpoint=CONF.storage_gnocchi.gnocchi_endpoint,
|
|
||||||
)
|
|
||||||
self._conn = gclient.Client(
|
|
||||||
'1',
|
|
||||||
session_options={'auth': auth_plugin},
|
|
||||||
adapter_options=adapter_options,
|
|
||||||
)
|
|
||||||
self._cacher = GnocchiResourceCacher()
|
|
||||||
|
|
||||||
def init(self):
|
|
||||||
self._check_archive_policy()
|
|
||||||
|
|
||||||
def _check_resource(self, metric_name, metric):
|
|
||||||
resource = GnocchiResource(metric_name, metric, self._conn)
|
|
||||||
if resource in self._cacher:
|
|
||||||
return self._cacher.get(resource)
|
|
||||||
resource.create()
|
|
||||||
self._cacher.add_resource(resource)
|
|
||||||
return resource
|
|
||||||
|
|
||||||
def _push_measures_to_gnocchi(self, measures):
|
|
||||||
if measures:
|
|
||||||
try:
|
|
||||||
self._conn.metric.batch_metrics_measures(measures)
|
|
||||||
except gexceptions.BadRequest:
|
|
||||||
LOG.warning(
|
|
||||||
'An exception occured while trying to push measures to '
|
|
||||||
'gnocchi. Retrying in 1 second. If this happens again, '
|
|
||||||
'set measure_chunk_size to a lower value.')
|
|
||||||
time.sleep(1)
|
|
||||||
self._conn.metric.batch_metrics_measures(measures)
|
|
||||||
|
|
||||||
# Do not use scope_id, as it is deprecated and will be
|
|
||||||
# removed together with the v1 storage
|
|
||||||
def push(self, dataframes, scope_id=None):
|
|
||||||
if not isinstance(dataframes, list):
|
|
||||||
dataframes = [dataframes]
|
|
||||||
measures = {}
|
|
||||||
nb_measures = 0
|
|
||||||
for dataframe in dataframes:
|
|
||||||
timestamp = dataframe['period']['begin']
|
|
||||||
for metric_name, metrics in dataframe['usage'].items():
|
|
||||||
for metric in metrics:
|
|
||||||
resource = self._check_resource(metric_name, metric)
|
|
||||||
if resource.needs_update:
|
|
||||||
resource.update(metric)
|
|
||||||
if not resource.qty or not resource.cost:
|
|
||||||
LOG.warning('Unexpected continue')
|
|
||||||
continue
|
|
||||||
|
|
||||||
# resource.qty is the uuid of the qty metric
|
|
||||||
if not measures.get(resource.qty):
|
|
||||||
measures[resource.qty] = []
|
|
||||||
measures[resource.qty].append({
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'value': metric['vol']['qty'],
|
|
||||||
})
|
|
||||||
|
|
||||||
if not measures.get(resource.cost):
|
|
||||||
measures[resource.cost] = []
|
|
||||||
measures[resource.cost].append({
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'value': metric['rating']['price'],
|
|
||||||
})
|
|
||||||
nb_measures += 2
|
|
||||||
if nb_measures >= CONF.storage_gnocchi.measure_chunk_size:
|
|
||||||
LOG.debug('Pushing {} measures to gnocchi.'.format(
|
|
||||||
nb_measures))
|
|
||||||
self._push_measures_to_gnocchi(measures)
|
|
||||||
measures = {}
|
|
||||||
nb_measures = 0
|
|
||||||
|
|
||||||
LOG.debug('Pushing {} measures to gnocchi.'.format(nb_measures))
|
|
||||||
self._push_measures_to_gnocchi(measures)
|
|
||||||
|
|
||||||
def _get_ck_resource_types(self):
|
|
||||||
types = self._conn.resource_type.list()
|
|
||||||
return [gtype['name'] for gtype in types
|
|
||||||
if gtype['name'].startswith(RESOURCE_TYPE_NAME_ROOT)]
|
|
||||||
|
|
||||||
def _check_res_types(self, res_type=None):
|
|
||||||
if res_type is None:
|
|
||||||
output = self._get_ck_resource_types()
|
|
||||||
elif isinstance(res_type, Iterable):
|
|
||||||
output = res_type
|
|
||||||
else:
|
|
||||||
output = [res_type]
|
|
||||||
return sorted(output)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _check_begin_end(begin, end):
|
|
||||||
if not begin:
|
|
||||||
begin = ck_utils.get_month_start()
|
|
||||||
if not end:
|
|
||||||
end = ck_utils.get_next_month()
|
|
||||||
if isinstance(begin, six.text_type):
|
|
||||||
begin = ck_utils.iso2dt(begin)
|
|
||||||
if isinstance(begin, int):
|
|
||||||
begin = ck_utils.ts2dt(begin)
|
|
||||||
if isinstance(end, six.text_type):
|
|
||||||
end = ck_utils.iso2dt(end)
|
|
||||||
if isinstance(end, int):
|
|
||||||
end = ck_utils.ts2dt(end)
|
|
||||||
|
|
||||||
return begin, end
|
|
||||||
|
|
||||||
def _get_resource_frame(self,
|
|
||||||
cost_measure,
|
|
||||||
qty_measure,
|
|
||||||
resource):
|
|
||||||
# Getting price
|
|
||||||
price = decimal.Decimal(cost_measure[2])
|
|
||||||
price_dict = {'price': float(price)}
|
|
||||||
|
|
||||||
# Getting vol
|
|
||||||
vol_dict = {
|
|
||||||
'qty': decimal.Decimal(qty_measure[2]),
|
|
||||||
'unit': resource.get('unit'),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Formatting
|
|
||||||
groupby = {
|
|
||||||
k.replace(GROUPBY_NAME_ROOT, ''): v
|
|
||||||
for k, v in resource.items() if k.startswith(GROUPBY_NAME_ROOT)
|
|
||||||
}
|
|
||||||
metadata = {
|
|
||||||
k.replace(META_NAME_ROOT, ''): v
|
|
||||||
for k, v in resource.items() if k.startswith(META_NAME_ROOT)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
'groupby': groupby,
|
|
||||||
'metadata': metadata,
|
|
||||||
'vol': vol_dict,
|
|
||||||
'rating': price_dict,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _to_cloudkitty(self,
|
|
||||||
res_type,
|
|
||||||
resource,
|
|
||||||
cost_measure,
|
|
||||||
qty_measure):
|
|
||||||
|
|
||||||
start = cost_measure[0]
|
|
||||||
stop = start + datetime.timedelta(seconds=cost_measure[1])
|
|
||||||
|
|
||||||
# Period
|
|
||||||
period_dict = {
|
|
||||||
'begin': ck_utils.dt2iso(start),
|
|
||||||
'end': ck_utils.dt2iso(stop),
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'usage': {res_type: [
|
|
||||||
self._get_resource_frame(cost_measure, qty_measure, resource)],
|
|
||||||
},
|
|
||||||
'period': period_dict,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_resource_info(self, resource_ids, start, stop):
|
|
||||||
search = {
|
|
||||||
'and': [
|
|
||||||
{
|
|
||||||
'or': [
|
|
||||||
{
|
|
||||||
'=': {'id': resource_id},
|
|
||||||
}
|
|
||||||
for resource_id in resource_ids
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
resources = []
|
|
||||||
marker = None
|
|
||||||
while True:
|
|
||||||
resource_chunk = self._conn.resource.search(query=search,
|
|
||||||
details=True,
|
|
||||||
marker=marker,
|
|
||||||
sorts=['id:asc'])
|
|
||||||
if len(resource_chunk) < 1:
|
|
||||||
break
|
|
||||||
marker = resource_chunk[-1]['id']
|
|
||||||
resources += resource_chunk
|
|
||||||
return {resource['id']: resource for resource in resources}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _dataframes_to_list(dataframes):
|
|
||||||
keys = sorted(dataframes.keys())
|
|
||||||
return [dataframes[key] for key in keys]
|
|
||||||
|
|
||||||
def _get_dataframes(self, measures, resource_info):
|
|
||||||
dataframes = {}
|
|
||||||
|
|
||||||
for measure in measures:
|
|
||||||
resource_type = measure['group']['type']
|
|
||||||
resource_id = measure['group']['id']
|
|
||||||
|
|
||||||
# Raw metrics do not contain all required attributes
|
|
||||||
resource = resource_info[resource_id]
|
|
||||||
|
|
||||||
dataframe = dataframes.get(measure['cost'][0])
|
|
||||||
ck_resource_type_name = resource_type.replace(
|
|
||||||
RESOURCE_TYPE_NAME_ROOT, '')
|
|
||||||
if dataframe is None:
|
|
||||||
dataframes[measure['cost'][0]] = self._to_cloudkitty(
|
|
||||||
ck_resource_type_name,
|
|
||||||
resource,
|
|
||||||
measure['cost'],
|
|
||||||
measure['qty'])
|
|
||||||
elif dataframe['usage'].get(ck_resource_type_name) is None:
|
|
||||||
dataframe['usage'][ck_resource_type_name] = [
|
|
||||||
self._get_resource_frame(
|
|
||||||
measure['cost'], measure['qty'], resource)]
|
|
||||||
else:
|
|
||||||
dataframe['usage'][ck_resource_type_name].append(
|
|
||||||
self._get_resource_frame(
|
|
||||||
measure['cost'], measure['qty'], resource))
|
|
||||||
return self._dataframes_to_list(dataframes)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_filters(filters, group_filters):
|
|
||||||
output = {}
|
|
||||||
|
|
||||||
if filters:
|
|
||||||
for k, v in filters.items():
|
|
||||||
output[META_NAME_ROOT + k] = v
|
|
||||||
if group_filters:
|
|
||||||
for k, v in group_filters.items():
|
|
||||||
output[GROUPBY_NAME_ROOT + k] = v
|
|
||||||
return output
|
|
||||||
|
|
||||||
def _raw_metrics_to_distinct_measures(self,
|
|
||||||
raw_cost_metrics,
|
|
||||||
raw_qty_metrics):
|
|
||||||
output = []
|
|
||||||
for cost, qty in zip(raw_cost_metrics, raw_qty_metrics):
|
|
||||||
output += [{
|
|
||||||
'cost': cost_measure,
|
|
||||||
'qty': qty['measures']['measures']['aggregated'][idx],
|
|
||||||
'group': cost['group'],
|
|
||||||
} for idx, cost_measure in enumerate(
|
|
||||||
cost['measures']['measures']['aggregated'])
|
|
||||||
]
|
|
||||||
# Sorting by timestamp, metric type and resource ID
|
|
||||||
output.sort(key=lambda x: (
|
|
||||||
x['cost'][0], x['group']['type'], x['group']['id']))
|
|
||||||
return output
|
|
||||||
|
|
||||||
def retrieve(self, begin=None, end=None,
|
|
||||||
filters=None, group_filters=None,
|
|
||||||
metric_types=None,
|
|
||||||
offset=0, limit=100, paginate=True):
|
|
||||||
|
|
||||||
begin, end = self._check_begin_end(begin, end)
|
|
||||||
|
|
||||||
metric_types = self._check_res_types(metric_types)
|
|
||||||
|
|
||||||
# Getting a list of active gnocchi resources with measures
|
|
||||||
filters = self._create_filters(filters, group_filters)
|
|
||||||
|
|
||||||
# FIXME(lukapeschke): We query all resource types in order to get the
|
|
||||||
# total amount of dataframes, but this could be done in a better way;
|
|
||||||
# ie. by not doing addtional queries once the limit is reached
|
|
||||||
raw_cost_metrics = []
|
|
||||||
raw_qty_metrics = []
|
|
||||||
for mtype in metric_types:
|
|
||||||
cost_metrics, qty_metrics = self._single_resource_type_aggregates(
|
|
||||||
begin, end, mtype, ['type', 'id'], filters, fetch_qty=True)
|
|
||||||
raw_cost_metrics += cost_metrics
|
|
||||||
raw_qty_metrics += qty_metrics
|
|
||||||
measures = self._raw_metrics_to_distinct_measures(
|
|
||||||
raw_cost_metrics, raw_qty_metrics)
|
|
||||||
|
|
||||||
result = {'total': len(measures)}
|
|
||||||
|
|
||||||
if paginate:
|
|
||||||
measures = measures[offset:limit]
|
|
||||||
if len(measures) < 1:
|
|
||||||
return {
|
|
||||||
'total': 0,
|
|
||||||
'dataframes': [],
|
|
||||||
}
|
|
||||||
resource_ids = [measure['group']['id'] for measure in measures]
|
|
||||||
|
|
||||||
resource_info = self._get_resource_info(resource_ids, begin, end)
|
|
||||||
|
|
||||||
result['dataframes'] = self._get_dataframes(measures, resource_info)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _single_resource_type_aggregates(self,
|
|
||||||
start, stop,
|
|
||||||
metric_type,
|
|
||||||
groupby,
|
|
||||||
filters,
|
|
||||||
fetch_qty=False):
|
|
||||||
search = {
|
|
||||||
'and': [
|
|
||||||
{'=': {'type': metric_type}}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
search['and'] += [{'=': {k: v}} for k, v in filters.items()]
|
|
||||||
|
|
||||||
cost_op = self.default_op
|
|
||||||
output = (
|
|
||||||
self._conn.aggregates.fetch(
|
|
||||||
cost_op,
|
|
||||||
search=search,
|
|
||||||
groupby=groupby,
|
|
||||||
resource_type=metric_type,
|
|
||||||
start=start, stop=stop),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
if fetch_qty:
|
|
||||||
qty_op = copy.deepcopy(self.default_op)
|
|
||||||
qty_op[2][1] = 'qty'
|
|
||||||
output = (
|
|
||||||
output[0],
|
|
||||||
self._conn.aggregates.fetch(
|
|
||||||
qty_op,
|
|
||||||
search=search,
|
|
||||||
groupby=groupby,
|
|
||||||
resource_type=metric_type,
|
|
||||||
start=start, stop=stop)
|
|
||||||
)
|
|
||||||
return output
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _ungroup_type(rated_resources):
|
|
||||||
output = []
|
|
||||||
for rated_resource in rated_resources:
|
|
||||||
rated_resource['group'].pop('type', None)
|
|
||||||
new_item = True
|
|
||||||
for elem in output:
|
|
||||||
if rated_resource['group'] == elem['group']:
|
|
||||||
elem['measures']['measures']['aggregated'] \
|
|
||||||
+= rated_resource['measures']['measures']['aggregated']
|
|
||||||
new_item = False
|
|
||||||
break
|
|
||||||
if new_item:
|
|
||||||
output.append(rated_resource)
|
|
||||||
return output
|
|
||||||
|
|
||||||
def total(self, groupby=None,
|
|
||||||
begin=None, end=None,
|
|
||||||
metric_types=None,
|
|
||||||
filters=None, group_filters=None,
|
|
||||||
offset=0, limit=1000, paginate=True):
|
|
||||||
begin, end = self._check_begin_end(begin, end)
|
|
||||||
|
|
||||||
if groupby is None:
|
|
||||||
groupby = []
|
|
||||||
request_groupby = [
|
|
||||||
GROUPBY_NAME_ROOT + elem for elem in groupby if elem != 'type']
|
|
||||||
# We need to have a least one attribute on which to group
|
|
||||||
request_groupby.append('type')
|
|
||||||
|
|
||||||
# NOTE(lukapeschke): For now, it isn't possible to group aggregates
|
|
||||||
# from different resource types using custom attributes, so we need
|
|
||||||
# to do one request per resource type.
|
|
||||||
rated_resources = []
|
|
||||||
metric_types = self._check_res_types(metric_types)
|
|
||||||
filters = self._create_filters(filters, group_filters)
|
|
||||||
for mtype in metric_types:
|
|
||||||
resources, _ = self._single_resource_type_aggregates(
|
|
||||||
begin, end, mtype, request_groupby, filters)
|
|
||||||
|
|
||||||
for resource in resources:
|
|
||||||
# If we have found something
|
|
||||||
if len(resource['measures']['measures']['aggregated']):
|
|
||||||
rated_resources.append(resource)
|
|
||||||
|
|
||||||
result = {'total': len(rated_resources)}
|
|
||||||
if paginate:
|
|
||||||
rated_resources = rated_resources[offset:limit]
|
|
||||||
if len(rated_resources) < 1:
|
|
||||||
return {
|
|
||||||
'total': 0,
|
|
||||||
'results': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
# NOTE(lukapeschke): We undo what has been done previously (grouping
|
|
||||||
# per type). This is not performant. Should be fixed as soon as
|
|
||||||
# previous note is supported in gnocchi
|
|
||||||
if 'type' not in groupby:
|
|
||||||
rated_resources = self._ungroup_type(rated_resources)
|
|
||||||
|
|
||||||
output = []
|
|
||||||
for rated_resource in rated_resources:
|
|
||||||
rate = sum(measure[2] for measure in
|
|
||||||
rated_resource['measures']['measures']['aggregated'])
|
|
||||||
output_elem = {
|
|
||||||
'begin': begin,
|
|
||||||
'end': end,
|
|
||||||
'rate': rate,
|
|
||||||
}
|
|
||||||
for group in groupby:
|
|
||||||
output_elem[group] = rated_resource['group'].get(
|
|
||||||
GROUPBY_NAME_ROOT + group, '')
|
|
||||||
# If we want to group per type
|
|
||||||
if 'type' in groupby:
|
|
||||||
output_elem['type'] = rated_resource['group'].get(
|
|
||||||
'type', '').replace(RESOURCE_TYPE_NAME_ROOT, '') or ''
|
|
||||||
output.append(output_elem)
|
|
||||||
result['results'] = output
|
|
||||||
return result
|
|
||||||
@@ -14,15 +14,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
"""Test SummaryModel objects."""
|
"""Test SummaryModel objects."""
|
||||||
import testtools
|
|
||||||
|
|
||||||
from oslotest import base
|
from oslotest import base
|
||||||
|
|
||||||
from cloudkitty.api.v1.datamodels import report
|
from cloudkitty.api.v1.datamodels import report
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class TestSummary(base.BaseTestCase):
|
class TestSummary(base.BaseTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -14,17 +14,12 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
"""Test cloudkitty/api/v1/types."""
|
"""Test cloudkitty/api/v1/types."""
|
||||||
|
|
||||||
import testtools
|
|
||||||
|
|
||||||
from oslotest import base
|
from oslotest import base
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
from cloudkitty.api.v1 import types
|
from cloudkitty.api.v1 import types
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class TestTypes(base.BaseTestCase):
|
class TestTypes(base.BaseTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -14,15 +14,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
import testtools
|
|
||||||
|
|
||||||
from cloudkitty.collector import gnocchi
|
from cloudkitty.collector import gnocchi
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests import samples
|
from cloudkitty.tests import samples
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class GnocchiCollectorTest(tests.TestCase):
|
class GnocchiCollectorTest(tests.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(GnocchiCollectorTest, self).setUp()
|
super(GnocchiCollectorTest, self).setUp()
|
||||||
|
|||||||
@@ -17,17 +17,14 @@
|
|||||||
#
|
#
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import mock
|
import mock
|
||||||
import testtools
|
|
||||||
|
|
||||||
from cloudkitty import collector
|
from cloudkitty import collector
|
||||||
from cloudkitty.collector import prometheus
|
from cloudkitty.collector import prometheus
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests import samples
|
from cloudkitty.tests import samples
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
from cloudkitty import transformer
|
from cloudkitty import transformer
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class PrometheusCollectorTest(tests.TestCase):
|
class PrometheusCollectorTest(tests.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(PrometheusCollectorTest, self).setUp()
|
super(PrometheusCollectorTest, self).setUp()
|
||||||
@@ -132,7 +129,6 @@ class PrometheusCollectorTest(tests.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class PrometheusClientTest(tests.TestCase):
|
class PrometheusClientTest(tests.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(PrometheusClientTest, self).setUp()
|
super(PrometheusClientTest, self).setUp()
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
import abc
|
import abc
|
||||||
import decimal
|
import decimal
|
||||||
import os
|
import os
|
||||||
from unittest.case import SkipTest
|
|
||||||
|
|
||||||
from gabbi import fixture
|
from gabbi import fixture
|
||||||
import mock
|
import mock
|
||||||
@@ -45,7 +44,6 @@ from cloudkitty import storage
|
|||||||
from cloudkitty.storage.v1.sqlalchemy import models
|
from cloudkitty.storage.v1.sqlalchemy import models
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests import utils as test_utils
|
from cloudkitty.tests import utils as test_utils
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
from cloudkitty import utils as ck_utils
|
from cloudkitty import utils as ck_utils
|
||||||
|
|
||||||
|
|
||||||
@@ -86,10 +84,9 @@ class BaseExtensionFixture(fixture.GabbiFixture):
|
|||||||
self.patch.return_value = fake_mgr
|
self.patch.return_value = fake_mgr
|
||||||
|
|
||||||
def stop_fixture(self):
|
def stop_fixture(self):
|
||||||
if not is_functional_test():
|
self.patch.assert_called_with(
|
||||||
self.patch.assert_called_with(
|
self.namespace,
|
||||||
self.namespace,
|
**self.assert_args)
|
||||||
**self.assert_args)
|
|
||||||
self.mock.stop()
|
self.mock.stop()
|
||||||
|
|
||||||
|
|
||||||
@@ -399,13 +396,6 @@ class MetricsConfFixture(fixture.GabbiFixture):
|
|||||||
ck_utils.load_conf = self._original_function
|
ck_utils.load_conf = self._original_function
|
||||||
|
|
||||||
|
|
||||||
class SkipIfFunctional(fixture.GabbiFixture):
|
|
||||||
|
|
||||||
def start_fixture(self):
|
|
||||||
if is_functional_test():
|
|
||||||
raise SkipTest
|
|
||||||
|
|
||||||
|
|
||||||
def setup_app():
|
def setup_app():
|
||||||
messaging.setup()
|
messaging.setup()
|
||||||
# FIXME(sheeprine): Extension fixtures are interacting with transformers
|
# FIXME(sheeprine): Extension fixtures are interacting with transformers
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ fixtures:
|
|||||||
- ConfigFixtureKeystoneAuth
|
- ConfigFixtureKeystoneAuth
|
||||||
- StorageDataFixture
|
- StorageDataFixture
|
||||||
- NowStorageDataFixture
|
- NowStorageDataFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
- name: Can't query api without token
|
- name: Can't query api without token
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- ConfigFixture
|
- ConfigFixture
|
||||||
- CORSConfigFixture
|
- CORSConfigFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ fixtures:
|
|||||||
- ConfigFixture
|
- ConfigFixture
|
||||||
- StorageDataFixture
|
- StorageDataFixture
|
||||||
- NowStorageDataFixture
|
- NowStorageDataFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
- name: Can query api without auth
|
- name: Can query api without auth
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- ConfigFixture
|
- ConfigFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
- name: test if / is publicly available
|
- name: test if / is publicly available
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- ConfigFixture
|
- ConfigFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- ConfigFixture
|
- ConfigFixture
|
||||||
- MetricsConfFixture
|
- MetricsConfFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
- name: get config
|
- name: get config
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ fixtures:
|
|||||||
- ConfigFixture
|
- ConfigFixture
|
||||||
- RatingModulesFixture
|
- RatingModulesFixture
|
||||||
- QuoteFakeRPC
|
- QuoteFakeRPC
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
- name: reload list of modules available
|
- name: reload list of modules available
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ fixtures:
|
|||||||
- ConfigFixture
|
- ConfigFixture
|
||||||
- StorageDataFixture
|
- StorageDataFixture
|
||||||
- NowStorageDataFixture
|
- NowStorageDataFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
- name: get period with two tenants
|
- name: get period with two tenants
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ fixtures:
|
|||||||
- ConfigFixture
|
- ConfigFixture
|
||||||
- StorageDataFixture
|
- StorageDataFixture
|
||||||
- NowStorageDataFixture
|
- NowStorageDataFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
- name: fetch period with no data
|
- name: fetch period with no data
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- HashMapConfigFixture
|
- HashMapConfigFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- HashMapConfigFixture
|
- HashMapConfigFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- HashMapConfigFixture
|
- HashMapConfigFixture
|
||||||
- UUIDFixture
|
- UUIDFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- HashMapConfigFixture
|
- HashMapConfigFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- PyScriptsConfigFixture
|
- PyScriptsConfigFixture
|
||||||
- UUIDFixture
|
- UUIDFixture
|
||||||
- SkipIfFunctional
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# @author: Luka Peschke
|
# @author: Luka Peschke
|
||||||
#
|
#
|
||||||
import mock
|
import mock
|
||||||
import testtools
|
|
||||||
|
|
||||||
from gnocchiclient import exceptions as gexc
|
from gnocchiclient import exceptions as gexc
|
||||||
|
|
||||||
@@ -55,7 +54,6 @@ class PermissiveDict(object):
|
|||||||
return self.value == other.get(self.key)
|
return self.value == other.get(self.key)
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(test_utils.is_functional_test(), 'Not a functional test')
|
|
||||||
class HybridStorageTestGnocchi(BaseHybridStorageTest):
|
class HybridStorageTestGnocchi(BaseHybridStorageTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# @author: Stéphane Albert
|
# @author: Stéphane Albert
|
||||||
#
|
#
|
||||||
import copy
|
import copy
|
||||||
import testtools
|
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import testscenarios
|
import testscenarios
|
||||||
@@ -65,7 +64,6 @@ class StorageTest(tests.TestCase):
|
|||||||
self.storage.push(working_data, self._other_tenant_id)
|
self.storage.push(working_data, self._other_tenant_id)
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(test_utils.is_functional_test(), 'Not a functional test')
|
|
||||||
class StorageDataframeTest(StorageTest):
|
class StorageDataframeTest(StorageTest):
|
||||||
|
|
||||||
storage_scenarios = [
|
storage_scenarios = [
|
||||||
@@ -129,7 +127,6 @@ class StorageDataframeTest(StorageTest):
|
|||||||
self.assertEqual(3, len(data))
|
self.assertEqual(3, len(data))
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(test_utils.is_functional_test(), 'Not a functional test')
|
|
||||||
class StorageTotalTest(StorageTest):
|
class StorageTotalTest(StorageTest):
|
||||||
|
|
||||||
storage_scenarios = [
|
storage_scenarios = [
|
||||||
@@ -269,7 +266,6 @@ class StorageTotalTest(StorageTest):
|
|||||||
self.assertEqual(end, total[3]["end"])
|
self.assertEqual(end, total[3]["end"])
|
||||||
|
|
||||||
|
|
||||||
if not test_utils.is_functional_test():
|
StorageTest.generate_scenarios()
|
||||||
StorageTest.generate_scenarios()
|
StorageTotalTest.generate_scenarios()
|
||||||
StorageTotalTest.generate_scenarios()
|
StorageDataframeTest.generate_scenarios()
|
||||||
StorageDataframeTest.generate_scenarios()
|
|
||||||
|
|||||||
@@ -1,351 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2018 Objectif Libre
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# @author: Luka Peschke
|
|
||||||
#
|
|
||||||
import copy
|
|
||||||
from datetime import datetime
|
|
||||||
import decimal
|
|
||||||
import fixtures
|
|
||||||
import testtools
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_config import fixture as config_fixture
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
|
|
||||||
from cloudkitty import storage
|
|
||||||
from cloudkitty.tests import utils as test_utils
|
|
||||||
from cloudkitty import utils as ck_utils
|
|
||||||
|
|
||||||
|
|
||||||
CONF = None
|
|
||||||
|
|
||||||
|
|
||||||
def _init_conf():
|
|
||||||
global CONF
|
|
||||||
if not CONF:
|
|
||||||
CONF = cfg.CONF
|
|
||||||
CONF(args=[], project='cloudkitty',
|
|
||||||
validate_default_values=True,
|
|
||||||
default_config_files=['/etc/cloudkitty/cloudkitty.conf'])
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFunctionalStorageTest(testtools.TestCase):
|
|
||||||
|
|
||||||
# Name of the storage backend to test
|
|
||||||
storage_backend = None
|
|
||||||
storage_version = 0
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
_init_conf()
|
|
||||||
cls._conf_fixture = config_fixture.Config(conf=CONF)
|
|
||||||
cls._conf_fixture.set_config_files(
|
|
||||||
['/etc.cloudkitty/cloudkitty.conf'])
|
|
||||||
cls.conf = cls._conf_fixture.conf
|
|
||||||
cls.conf.set_override('version', cls.storage_version, 'storage')
|
|
||||||
cls.conf.set_override('backend', cls.storage_backend, 'storage')
|
|
||||||
cls.storage = storage.get_storage()
|
|
||||||
cls.storage.init()
|
|
||||||
cls.project_ids, cls.data = cls.gen_data_separate_projects(3)
|
|
||||||
for i, project_data in enumerate(cls.data):
|
|
||||||
cls.storage.push(project_data, cls.project_ids[i])
|
|
||||||
|
|
||||||
# Appending data for the second tenant
|
|
||||||
data_next_period = copy.deepcopy(cls.data[0])
|
|
||||||
data_next_period['period']['begin'] += 3600
|
|
||||||
data_next_period['period']['end'] += 3600
|
|
||||||
cls.storage.push(data_next_period, cls.project_ids[0])
|
|
||||||
cls.project_ids.append(cls.project_ids[0])
|
|
||||||
cls.data.append(data_next_period)
|
|
||||||
|
|
||||||
cls.wait_for_backend()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls):
|
|
||||||
cls.cleanup_backend()
|
|
||||||
# cls._conf_fixture.cleanUp()
|
|
||||||
# pass
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(BaseFunctionalStorageTest, self).setUp()
|
|
||||||
self.useFixture(fixtures.FakeLogger())
|
|
||||||
self.useFixture(self._conf_fixture)
|
|
||||||
|
|
||||||
def cleanUp(self):
|
|
||||||
super(BaseFunctionalStorageTest, self).cleanUp()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def wait_for_backend(cls):
|
|
||||||
"""Function waiting for the storage backend to be ready.
|
|
||||||
|
|
||||||
Ex: wait for gnocchi to have processed all metrics
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def cleanup_backend(cls):
|
|
||||||
"""Function deleting everything from the storage backend"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def gen_data_separate_projects(nb_projects):
|
|
||||||
project_ids = [uuidutils.generate_uuid() for i in range(nb_projects)]
|
|
||||||
data = [
|
|
||||||
test_utils.generate_v2_storage_data(
|
|
||||||
project_ids=project_ids[i], nb_projects=1)
|
|
||||||
for i in range(nb_projects)]
|
|
||||||
return project_ids, data
|
|
||||||
|
|
||||||
def test_get_retention(self):
|
|
||||||
retention = self.storage.get_retention().days * 24
|
|
||||||
self.assertEqual(retention, self.conf.storage.retention_period)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _validate_filters(comp, filters=None, group_filters=None):
|
|
||||||
if group_filters:
|
|
||||||
for k, v in group_filters.items():
|
|
||||||
if comp['groupby'].get(k) != v:
|
|
||||||
return False
|
|
||||||
if filters:
|
|
||||||
for k, v in filters.items():
|
|
||||||
if comp['metadata'].get(k) != v:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_expected_total(self, begin=None, end=None,
|
|
||||||
filters=None, group_filters=None):
|
|
||||||
total = decimal.Decimal(0)
|
|
||||||
for dataframes in self.data:
|
|
||||||
if (ck_utils.ts2dt(dataframes['period']['begin']) >= end
|
|
||||||
or ck_utils.ts2dt(dataframes['period']['end']) <= begin):
|
|
||||||
continue
|
|
||||||
for df in dataframes['usage'].values():
|
|
||||||
for elem in df:
|
|
||||||
if self._validate_filters(elem, filters, group_filters):
|
|
||||||
total += elem['rating']['price']
|
|
||||||
return total
|
|
||||||
|
|
||||||
def _compare_totals(self, expected_total, total):
|
|
||||||
self.assertEqual(len(total), len(expected_total))
|
|
||||||
for i in range(len(total)):
|
|
||||||
self.assertEqual(
|
|
||||||
round(expected_total[i], 5),
|
|
||||||
round(decimal.Decimal(total[i]['rate']), 5),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_total_all_projects_on_time_window_with_data_no_grouping(self):
|
|
||||||
expected_total = self._get_expected_total(begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 1, 1, 1))
|
|
||||||
total = self.storage.total(begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 1, 1, 1))
|
|
||||||
self.assertEqual(len(total), 1)
|
|
||||||
self.assertEqual(
|
|
||||||
round(expected_total, 5),
|
|
||||||
round(decimal.Decimal(total[0]['rate']), 5),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_total_one_project_on_time_window_with_data_no_grouping(self):
|
|
||||||
group_filters = {'project_id': self.project_ids[0]}
|
|
||||||
expected_total = self._get_expected_total(
|
|
||||||
begin=datetime(2018, 1, 1), end=datetime(2018, 1, 1, 1),
|
|
||||||
group_filters=group_filters)
|
|
||||||
total = self.storage.total(begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 1, 1, 1),
|
|
||||||
group_filters=group_filters)
|
|
||||||
self.assertEqual(len(total), 1)
|
|
||||||
self.assertEqual(
|
|
||||||
round(expected_total, 5),
|
|
||||||
round(decimal.Decimal(total[0]['rate']), 5),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_total_all_projects_window_with_data_group_by_project_id(self):
|
|
||||||
expected_total = []
|
|
||||||
for project_id in sorted(self.project_ids[:-1]):
|
|
||||||
group_filters = {'project_id': project_id}
|
|
||||||
expected_total.append(self._get_expected_total(
|
|
||||||
begin=datetime(2018, 1, 1), end=datetime(2018, 1, 1, 1),
|
|
||||||
group_filters=group_filters))
|
|
||||||
|
|
||||||
total = self.storage.total(begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 1, 1, 1),
|
|
||||||
groupby=['project_id'])
|
|
||||||
total = sorted(total, key=lambda k: k['project_id'])
|
|
||||||
|
|
||||||
self._compare_totals(expected_total, total)
|
|
||||||
|
|
||||||
def test_get_total_one_project_window_with_data_group_by_resource_id(self):
|
|
||||||
expected_total = []
|
|
||||||
for df in self.data[0]['usage'].values():
|
|
||||||
expected_total += copy.deepcopy(df)
|
|
||||||
for df in self.data[-1]['usage'].values():
|
|
||||||
for df_elem in df:
|
|
||||||
for elem in expected_total:
|
|
||||||
if elem['groupby'] == df_elem['groupby']:
|
|
||||||
elem['rating']['price'] += df_elem['rating']['price']
|
|
||||||
expected_total = sorted(
|
|
||||||
expected_total, key=lambda k: k['groupby']['id'])
|
|
||||||
expected_total = [i['rating']['price'] for i in expected_total]
|
|
||||||
|
|
||||||
total = self.storage.total(
|
|
||||||
begin=datetime(2018, 1, 1), end=datetime(2018, 1, 1, 2),
|
|
||||||
group_filters={'project_id': self.project_ids[0]},
|
|
||||||
groupby=['id'])
|
|
||||||
total = sorted(total, key=lambda k: k['id'])
|
|
||||||
|
|
||||||
self._compare_totals(expected_total, total)
|
|
||||||
|
|
||||||
def test_get_total_all_projects_group_by_resource_id_project_id(self):
|
|
||||||
expected_total = []
|
|
||||||
for data in self.data[:-1]:
|
|
||||||
for df in data['usage'].values():
|
|
||||||
expected_total += copy.deepcopy(df)
|
|
||||||
for df in self.data[-1]['usage'].values():
|
|
||||||
for elem in df:
|
|
||||||
for total_elem in expected_total:
|
|
||||||
if total_elem['groupby'] == elem['groupby']:
|
|
||||||
total_elem['rating']['price'] \
|
|
||||||
+= elem['rating']['price']
|
|
||||||
expected_total = sorted(
|
|
||||||
expected_total, key=lambda k: k['groupby']['id'])
|
|
||||||
expected_total = [i['rating']['price'] for i in expected_total]
|
|
||||||
|
|
||||||
total = self.storage.total(
|
|
||||||
begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 2, 1),
|
|
||||||
groupby=['id', 'project_id'])
|
|
||||||
total = sorted(total, key=lambda k: k['id'])
|
|
||||||
|
|
||||||
self._compare_totals(expected_total, total)
|
|
||||||
|
|
||||||
def test_get_total_all_projects_group_by_resource_type(self):
|
|
||||||
expected_total = {}
|
|
||||||
for data in self.data:
|
|
||||||
for res_type, df in data['usage'].items():
|
|
||||||
if expected_total.get(res_type):
|
|
||||||
expected_total[res_type] += sum(
|
|
||||||
elem['rating']['price'] for elem in df)
|
|
||||||
else:
|
|
||||||
expected_total[res_type] = sum(
|
|
||||||
elem['rating']['price'] for elem in df)
|
|
||||||
expected_total = [
|
|
||||||
expected_total[key] for key in sorted(expected_total.keys())]
|
|
||||||
total = self.storage.total(
|
|
||||||
begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 2, 1),
|
|
||||||
groupby=['type'])
|
|
||||||
total = sorted(total, key=lambda k: k['type'])
|
|
||||||
|
|
||||||
self._compare_totals(expected_total, total)
|
|
||||||
|
|
||||||
def test_get_total_one_project_group_by_resource_type(self):
|
|
||||||
expected_total = {}
|
|
||||||
for res_type, df in self.data[0]['usage'].items():
|
|
||||||
expected_total[res_type] = sum(
|
|
||||||
elem['rating']['price'] for elem in df)
|
|
||||||
expected_total = [
|
|
||||||
expected_total[key] for key in sorted(expected_total.keys())]
|
|
||||||
|
|
||||||
group_filters = {'project_id': self.project_ids[0]}
|
|
||||||
total = self.storage.total(
|
|
||||||
begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 1, 1, 1),
|
|
||||||
group_filters=group_filters,
|
|
||||||
groupby=['type'])
|
|
||||||
total = sorted(total, key=lambda k: k['type'])
|
|
||||||
|
|
||||||
self._compare_totals(expected_total, total)
|
|
||||||
|
|
||||||
def test_get_total_no_data_period(self):
|
|
||||||
total = self.storage.total(
|
|
||||||
begin=datetime(2018, 2, 1), end=datetime(2018, 2, 1, 1))
|
|
||||||
self.assertEqual(0, len(total))
|
|
||||||
|
|
||||||
def test_retrieve_all_projects_with_data(self):
|
|
||||||
expected_length = sum(
|
|
||||||
len(data['usage'].values()) for data in self.data)
|
|
||||||
|
|
||||||
frames = self.storage.retrieve(
|
|
||||||
begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 2, 1),
|
|
||||||
limit=1000)
|
|
||||||
|
|
||||||
self.assertEqual(expected_length, frames['total'])
|
|
||||||
self.assertEqual(2, len(frames['dataframes']))
|
|
||||||
|
|
||||||
def test_retrieve_one_project_with_data(self):
|
|
||||||
expected_length = len(self.data[0]['usage'].values()) \
|
|
||||||
+ len(self.data[-1]['usage'].values())
|
|
||||||
|
|
||||||
group_filters = {'project_id': self.project_ids[0]}
|
|
||||||
frames = self.storage.retrieve(
|
|
||||||
begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 2, 1),
|
|
||||||
group_filters=group_filters,
|
|
||||||
limit=1000)
|
|
||||||
|
|
||||||
self.assertEqual(expected_length, frames['total'])
|
|
||||||
self.assertEqual(2, len(frames['dataframes']))
|
|
||||||
for metric_type in self.data[0]['usage'].keys():
|
|
||||||
self.assertEqual(
|
|
||||||
len(frames['dataframes'][0]['usage'][metric_type]),
|
|
||||||
len(self.data[0]['usage'][metric_type]))
|
|
||||||
for metric_type in self.data[-1]['usage'].keys():
|
|
||||||
self.assertEqual(
|
|
||||||
len(frames['dataframes'][1]['usage'][metric_type]),
|
|
||||||
len(self.data[-1]['usage'][metric_type]))
|
|
||||||
|
|
||||||
def test_retrieve_pagination_one_project(self):
|
|
||||||
expected_length = len(self.data[0]['usage'].values()) \
|
|
||||||
+ len(self.data[-1]['usage'].values())
|
|
||||||
|
|
||||||
group_filters = {'project_id': self.project_ids[0]}
|
|
||||||
first_frames = self.storage.retrieve(
|
|
||||||
begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 2, 1),
|
|
||||||
group_filters=group_filters,
|
|
||||||
limit=5)
|
|
||||||
last_frames = self.storage.retrieve(
|
|
||||||
begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 2, 1),
|
|
||||||
group_filters=group_filters,
|
|
||||||
offset=5,
|
|
||||||
limit=1000)
|
|
||||||
all_frames = self.storage.retrieve(
|
|
||||||
begin=datetime(2018, 1, 1),
|
|
||||||
end=datetime(2018, 2, 1),
|
|
||||||
group_filters=group_filters,
|
|
||||||
paginate=False)
|
|
||||||
|
|
||||||
self.assertEqual(expected_length, first_frames['total'])
|
|
||||||
self.assertEqual(expected_length, last_frames['total'])
|
|
||||||
|
|
||||||
real_length = 0
|
|
||||||
paginated_measures = []
|
|
||||||
|
|
||||||
for frame in first_frames['dataframes'] + last_frames['dataframes']:
|
|
||||||
for measures in frame['usage'].values():
|
|
||||||
real_length += len(measures)
|
|
||||||
paginated_measures += measures
|
|
||||||
paginated_measures = sorted(
|
|
||||||
paginated_measures, key=lambda x: x['groupby']['id'])
|
|
||||||
|
|
||||||
all_measures = []
|
|
||||||
for frame in all_frames['dataframes']:
|
|
||||||
for measures in frame['usage'].values():
|
|
||||||
all_measures += measures
|
|
||||||
all_measures = sorted(
|
|
||||||
all_measures, key=lambda x: x['groupby']['id'])
|
|
||||||
|
|
||||||
self.assertEqual(expected_length, real_length)
|
|
||||||
self.assertEqual(paginated_measures, all_measures)
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2018 Objectif Libre
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# @author: Luka Peschke
|
|
||||||
#
|
|
||||||
import testtools
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from gnocchiclient import exceptions as gexceptions
|
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from cloudkitty.tests.storage.v2 import base_functional
|
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipUnless(is_functional_test(), 'Test is not a functional test')
|
|
||||||
class GnocchiBaseFunctionalStorageTest(
|
|
||||||
base_functional.BaseFunctionalStorageTest):
|
|
||||||
|
|
||||||
storage_backend = 'gnocchi'
|
|
||||||
storage_version = 2
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(GnocchiBaseFunctionalStorageTest, self).setUp()
|
|
||||||
self.conf.import_group(
|
|
||||||
'storage_gnocchi', 'cloudkitty.storage.v2.gnocchi')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_status(cls):
|
|
||||||
status = cls.storage._conn.status.get()
|
|
||||||
return status['storage']['summary']['measures']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def wait_for_backend(cls):
|
|
||||||
while True:
|
|
||||||
status = cls._get_status()
|
|
||||||
if status == 0:
|
|
||||||
break
|
|
||||||
LOG.info('Waiting for gnocchi to have processed all measures, {} '
|
|
||||||
'left.'.format(status))
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def cleanup_backend(cls):
|
|
||||||
for res_type in cls.storage._get_ck_resource_types():
|
|
||||||
batch_query = {">=": {"started_at": "1970-01-01T01:00:00"}}
|
|
||||||
cls.storage._conn.resource.batch_delete(
|
|
||||||
batch_query, resource_type=res_type)
|
|
||||||
try:
|
|
||||||
cls.storage._conn.resource_type.delete(res_type)
|
|
||||||
except gexceptions.BadRequest:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
cls.storage._conn.archive_policy.delete(
|
|
||||||
'cloudkitty_archive_policy')
|
|
||||||
except gexceptions.BadRequest:
|
|
||||||
pass
|
|
||||||
@@ -323,5 +323,4 @@ class StorageUnitTest(TestCase):
|
|||||||
self.assertEqual(expected_length, retrieved_length)
|
self.assertEqual(expected_length, retrieved_length)
|
||||||
|
|
||||||
|
|
||||||
if not test_utils.is_functional_test():
|
StorageUnitTest.generate_scenarios()
|
||||||
StorageUnitTest.generate_scenarios()
|
|
||||||
|
|||||||
@@ -13,14 +13,10 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
import testtools
|
|
||||||
|
|
||||||
from cloudkitty.common import config as ck_config
|
from cloudkitty.common import config as ck_config
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class ConfigTest(tests.TestCase):
|
class ConfigTest(tests.TestCase):
|
||||||
def test_config(self):
|
def test_config(self):
|
||||||
ck_config.list_opts()
|
ck_config.list_opts()
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import testtools
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
@@ -22,10 +21,8 @@ import pep8
|
|||||||
|
|
||||||
from cloudkitty.hacking import checks
|
from cloudkitty.hacking import checks
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class HackingTestCase(tests.TestCase):
|
class HackingTestCase(tests.TestCase):
|
||||||
"""Hacking test cases
|
"""Hacking test cases
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
#
|
#
|
||||||
import copy
|
import copy
|
||||||
import decimal
|
import decimal
|
||||||
import testtools
|
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
@@ -25,7 +24,6 @@ from oslo_utils import uuidutils
|
|||||||
from cloudkitty.rating import hash
|
from cloudkitty.rating import hash
|
||||||
from cloudkitty.rating.hash.db import api
|
from cloudkitty.rating.hash.db import api
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
TEST_TS = 1388577600
|
TEST_TS = 1388577600
|
||||||
@@ -84,7 +82,6 @@ CK_RESOURCES_DATA = [{
|
|||||||
"unit": "instance"}}]}}]
|
"unit": "instance"}}]}}]
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class HashMapRatingTest(tests.TestCase):
|
class HashMapRatingTest(tests.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(HashMapRatingTest, self).setUp()
|
super(HashMapRatingTest, self).setUp()
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
#
|
#
|
||||||
# @author: Stéphane Albert
|
# @author: Stéphane Albert
|
||||||
#
|
#
|
||||||
import testtools
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
@@ -23,7 +22,6 @@ from oslo_utils import uuidutils
|
|||||||
|
|
||||||
from cloudkitty.fetcher import keystone
|
from cloudkitty.fetcher import keystone
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
class FakeRole(object):
|
class FakeRole(object):
|
||||||
@@ -69,7 +67,6 @@ def Client(**kwargs):
|
|||||||
return FakeKeystoneClient(**kwargs)
|
return FakeKeystoneClient(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class KeystoneFetcherTest(tests.TestCase):
|
class KeystoneFetcherTest(tests.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(KeystoneFetcherTest, self).setUp()
|
super(KeystoneFetcherTest, self).setUp()
|
||||||
|
|||||||
@@ -15,15 +15,12 @@
|
|||||||
#
|
#
|
||||||
# @author: Stéphane Albert
|
# @author: Stéphane Albert
|
||||||
#
|
#
|
||||||
import testtools
|
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslo_messaging import conffixture
|
from oslo_messaging import conffixture
|
||||||
from stevedore import extension
|
from stevedore import extension
|
||||||
|
|
||||||
from cloudkitty import orchestrator
|
from cloudkitty import orchestrator
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
class FakeKeystoneClient(object):
|
class FakeKeystoneClient(object):
|
||||||
@@ -36,7 +33,6 @@ class FakeKeystoneClient(object):
|
|||||||
tenants = FakeTenants()
|
tenants = FakeTenants()
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class OrchestratorTest(tests.TestCase):
|
class OrchestratorTest(tests.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(OrchestratorTest, self).setUp()
|
super(OrchestratorTest, self).setUp()
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
import os.path
|
import os.path
|
||||||
import testtools
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import fixture as config_fixture
|
from oslo_config import fixture as config_fixture
|
||||||
@@ -22,14 +21,12 @@ from oslo_policy import policy as oslo_policy
|
|||||||
|
|
||||||
from cloudkitty.common import policy
|
from cloudkitty.common import policy
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
from cloudkitty import utils
|
from cloudkitty import utils
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class PolicyFileTestCase(tests.TestCase):
|
class PolicyFileTestCase(tests.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -61,7 +58,6 @@ class PolicyFileTestCase(tests.TestCase):
|
|||||||
self.context, action, self.target)
|
self.context, action, self.target)
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class PolicyTestCase(tests.TestCase):
|
class PolicyTestCase(tests.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
import copy
|
import copy
|
||||||
import decimal
|
import decimal
|
||||||
import hashlib
|
import hashlib
|
||||||
import testtools
|
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
@@ -28,7 +27,6 @@ import six
|
|||||||
from cloudkitty.rating import pyscripts
|
from cloudkitty.rating import pyscripts
|
||||||
from cloudkitty.rating.pyscripts.db import api
|
from cloudkitty.rating.pyscripts.db import api
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
FAKE_UUID = '6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
FAKE_UUID = '6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
||||||
@@ -106,7 +104,6 @@ for period in data:
|
|||||||
""".encode('utf-8')
|
""".encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class PyScriptsRatingTest(tests.TestCase):
|
class PyScriptsRatingTest(tests.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(PyScriptsRatingTest, self).setUp()
|
super(PyScriptsRatingTest, self).setUp()
|
||||||
|
|||||||
@@ -15,13 +15,10 @@
|
|||||||
#
|
#
|
||||||
# @author: Stéphane Albert
|
# @author: Stéphane Albert
|
||||||
#
|
#
|
||||||
import testtools
|
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from cloudkitty.db import api as ck_db_api
|
from cloudkitty.db import api as ck_db_api
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
class FakeRPCClient(object):
|
class FakeRPCClient(object):
|
||||||
@@ -42,7 +39,6 @@ class FakeRPCClient(object):
|
|||||||
self._queue.append(cast_data)
|
self._queue.append(cast_data)
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class RatingTest(tests.TestCase):
|
class RatingTest(tests.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RatingTest, self).setUp()
|
super(RatingTest, self).setUp()
|
||||||
|
|||||||
@@ -16,14 +16,11 @@
|
|||||||
# @author: Gauvain Pocentek
|
# @author: Gauvain Pocentek
|
||||||
#
|
#
|
||||||
import datetime
|
import datetime
|
||||||
import testtools
|
|
||||||
|
|
||||||
from cloudkitty import state
|
from cloudkitty import state
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class DBStateManagerTest(tests.TestCase):
|
class DBStateManagerTest(tests.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(DBStateManagerTest, self).setUp()
|
super(DBStateManagerTest, self).setUp()
|
||||||
|
|||||||
@@ -19,13 +19,11 @@ import datetime
|
|||||||
import decimal
|
import decimal
|
||||||
import fractions
|
import fractions
|
||||||
import itertools
|
import itertools
|
||||||
import testtools
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
from cloudkitty import utils as ck_utils
|
from cloudkitty import utils as ck_utils
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +31,6 @@ def iso2dt(iso_str):
|
|||||||
return timeutils.parse_isotime(iso_str)
|
return timeutils.parse_isotime(iso_str)
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class UtilsTimeCalculationsTest(unittest.TestCase):
|
class UtilsTimeCalculationsTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.date_ts = 1416219015
|
self.date_ts = 1416219015
|
||||||
@@ -144,7 +141,6 @@ class UtilsTimeCalculationsTest(unittest.TestCase):
|
|||||||
self.assertEqual(calc_dt, check_dt)
|
self.assertEqual(calc_dt, check_dt)
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class ConvertUnitTest(unittest.TestCase):
|
class ConvertUnitTest(unittest.TestCase):
|
||||||
"""Class testing the convert_unit and num2decimal function"""
|
"""Class testing the convert_unit and num2decimal function"""
|
||||||
possible_args = [
|
possible_args = [
|
||||||
|
|||||||
@@ -16,12 +16,10 @@
|
|||||||
# @author: Stéphane Albert
|
# @author: Stéphane Albert
|
||||||
#
|
#
|
||||||
import copy
|
import copy
|
||||||
import testtools
|
|
||||||
|
|
||||||
from cloudkitty import tests
|
from cloudkitty import tests
|
||||||
from cloudkitty.tests import samples
|
from cloudkitty.tests import samples
|
||||||
from cloudkitty.tests import transformers as t_transformers
|
from cloudkitty.tests import transformers as t_transformers
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
TRANS_METADATA = {
|
TRANS_METADATA = {
|
||||||
'availability_zone': 'nova',
|
'availability_zone': 'nova',
|
||||||
@@ -32,7 +30,6 @@ TRANS_METADATA = {
|
|||||||
'vcpus': '1'}
|
'vcpus': '1'}
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipIf(is_functional_test(), 'Not a functional test')
|
|
||||||
class TransformerBaseTest(tests.TestCase):
|
class TransformerBaseTest(tests.TestCase):
|
||||||
def test_strip_resource_on_dict(self):
|
def test_strip_resource_on_dict(self):
|
||||||
metadata = copy.deepcopy(samples.COMPUTE_METADATA)
|
metadata = copy.deepcopy(samples.COMPUTE_METADATA)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
#
|
#
|
||||||
import copy
|
import copy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from os import getenv
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
@@ -26,10 +25,6 @@ from cloudkitty.tests import samples
|
|||||||
from cloudkitty import utils as ck_utils
|
from cloudkitty import utils as ck_utils
|
||||||
|
|
||||||
|
|
||||||
def is_functional_test():
|
|
||||||
return getenv('TEST_FUNCTIONAL', False)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_v2_storage_data(min_length=10,
|
def generate_v2_storage_data(min_length=10,
|
||||||
nb_projects=2,
|
nb_projects=2,
|
||||||
project_ids=None,
|
project_ids=None,
|
||||||
|
|||||||
@@ -14,49 +14,3 @@ implement the following abstract class:
|
|||||||
You'll then need to register an entrypoint corresponding to your storage
|
You'll then need to register an entrypoint corresponding to your storage
|
||||||
backend in the ``cloudkitty.storage.v2.backends`` section of the ``setup.cfg``
|
backend in the ``cloudkitty.storage.v2.backends`` section of the ``setup.cfg``
|
||||||
file.
|
file.
|
||||||
|
|
||||||
Testing
|
|
||||||
=======
|
|
||||||
|
|
||||||
There is a generic test class for v2 storage backends. It allows to run a
|
|
||||||
functional test suite against a new v2 storage backend.
|
|
||||||
|
|
||||||
.. code:: shell
|
|
||||||
|
|
||||||
$ tree cloudkitty/tests/storage/v2
|
|
||||||
cloudkitty/tests/storage/v2
|
|
||||||
├── base_functional.py
|
|
||||||
├── __init__.py
|
|
||||||
└── test_gnocchi_functional.py
|
|
||||||
|
|
||||||
In order to use the class, add a file called ``test_mybackend_functional.py``
|
|
||||||
to the ``cloudkitty/tests/storage/v2`` directory. You will then need to write a
|
|
||||||
class inheriting from ``BaseFunctionalStorageTest``. Specify the storage version
|
|
||||||
and the backend name as class attributes
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
import testtools
|
|
||||||
|
|
||||||
from cloudkitty.tests.storage.v2 import base_functional
|
|
||||||
from cloudkitty.tests.utils import is_functional_test
|
|
||||||
|
|
||||||
|
|
||||||
@testtools.skipUnless(is_functional_test(), 'Test is not a functional test')
|
|
||||||
class GnocchiBaseFunctionalStorageTest(
|
|
||||||
base_functional.BaseFunctionalStorageTest):
|
|
||||||
|
|
||||||
storage_backend = 'gnocchi'
|
|
||||||
storage_version = 2
|
|
||||||
|
|
||||||
|
|
||||||
Two methods need to be implemented:
|
|
||||||
|
|
||||||
* ``wait_for_backend``. This method is called once data has been once
|
|
||||||
dataframes have been pushed to the storage backend (in gnocchi's case, it
|
|
||||||
waits for all measures to have been processed). It is a classmethod.
|
|
||||||
|
|
||||||
* ``cleanup_backend``: This method is called at the end of the test suite in
|
|
||||||
order to delete all data from the storage backend. It is a classmethod.
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
deprecations:
|
||||||
|
- |
|
||||||
|
The gnocchi v2 storage backend has been removed. Users wanting to use the
|
||||||
|
v2 storage interface must use the InfluxDB backend.
|
||||||
@@ -67,7 +67,6 @@ cloudkitty.storage.v1.backends =
|
|||||||
hybrid = cloudkitty.storage.v1.hybrid:HybridStorage
|
hybrid = cloudkitty.storage.v1.hybrid:HybridStorage
|
||||||
|
|
||||||
cloudkitty.storage.v2.backends =
|
cloudkitty.storage.v2.backends =
|
||||||
gnocchi = cloudkitty.storage.v2.gnocchi:GnocchiStorage
|
|
||||||
influxdb = cloudkitty.storage.v2.influx:InfluxStorage
|
influxdb = cloudkitty.storage.v2.influx:InfluxStorage
|
||||||
|
|
||||||
cloudkitty.storage.hybrid.backends =
|
cloudkitty.storage.hybrid.backends =
|
||||||
|
|||||||
7
tox.ini
7
tox.ini
@@ -71,10 +71,3 @@ local-check-factory = cloudkitty.hacking.checks.factory
|
|||||||
[testenv:releasenotes]
|
[testenv:releasenotes]
|
||||||
basepython = python3
|
basepython = python3
|
||||||
commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||||
|
|
||||||
[testenv:functional]
|
|
||||||
basepython = python3
|
|
||||||
setenv = TEST_FUNCTIONAL = 1
|
|
||||||
# Some tests do push and remove data from the storage backend, so this is done
|
|
||||||
# in order to keep data consistency
|
|
||||||
commands = stestr run --concurrency 1 {posargs}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user