Add DataPoint/DataFrame objects
This introduce the DataPoint and DataFrame objects. These are replacing the dicts that were used until now: A DataPoint is an immutable object representing one measure for one metric, with associated price, qty, groupby and metadata. A DataFrame is a collection of DataPoints, with helper functions for point insertion and JSON formatting/loading. Story: 2005890 Task: 35658 Change-Id: I71e95bba0a0cdd049b0fba3e79c7675b6365c37f
This commit is contained in:
		@@ -14,7 +14,6 @@
 | 
			
		||||
#    under the License.
 | 
			
		||||
#
 | 
			
		||||
import datetime
 | 
			
		||||
import decimal
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
import pecan
 | 
			
		||||
@@ -71,30 +70,21 @@ class DataFramesController(rest.RestController):
 | 
			
		||||
        except storage.NoTimeFrame:
 | 
			
		||||
            return storage_models.DataFrameCollection(dataframes=[])
 | 
			
		||||
        for frame in resp['dataframes']:
 | 
			
		||||
            for service, data_list in frame['usage'].items():
 | 
			
		||||
                frame_tenant = None
 | 
			
		||||
                resources = []
 | 
			
		||||
                for data in data_list:
 | 
			
		||||
                    # This means we use a v1 storage backend
 | 
			
		||||
                    if 'desc' in data.keys():
 | 
			
		||||
                        desc = data['desc']
 | 
			
		||||
                    else:
 | 
			
		||||
                        desc = data['metadata'].copy()
 | 
			
		||||
                        desc.update(data.get('groupby', {}))
 | 
			
		||||
                    price = decimal.Decimal(str(data['rating']['price']))
 | 
			
		||||
            resources = []
 | 
			
		||||
            frame_tenant = None
 | 
			
		||||
            for type_, points in frame.itertypes():
 | 
			
		||||
                for point in points:
 | 
			
		||||
                    resource = storage_models.RatedResource(
 | 
			
		||||
                        service=service,
 | 
			
		||||
                        desc=desc,
 | 
			
		||||
                        volume=data['vol']['qty'],
 | 
			
		||||
                        rating=price)
 | 
			
		||||
                        service=type_,
 | 
			
		||||
                        desc=point.desc,
 | 
			
		||||
                        volume=point.qty,
 | 
			
		||||
                        rating=point.price)
 | 
			
		||||
                    if frame_tenant is None:
 | 
			
		||||
                        frame_tenant = desc[scope_key]
 | 
			
		||||
                        frame_tenant = point.desc[scope_key]
 | 
			
		||||
                    resources.append(resource)
 | 
			
		||||
                dataframe = storage_models.DataFrame(
 | 
			
		||||
                    begin=tzutils.local_to_utc(
 | 
			
		||||
                        frame['period']['begin'], naive=True),
 | 
			
		||||
                    end=tzutils.local_to_utc(
 | 
			
		||||
                        frame['period']['end'], naive=True),
 | 
			
		||||
                    begin=tzutils.local_to_utc(frame.start, naive=True),
 | 
			
		||||
                    end=tzutils.local_to_utc(frame.end, naive=True),
 | 
			
		||||
                    tenant_id=frame_tenant,
 | 
			
		||||
                    resources=resources)
 | 
			
		||||
                dataframes.append(dataframe)
 | 
			
		||||
 
 | 
			
		||||
@@ -277,7 +277,7 @@ class BaseCollector(object):
 | 
			
		||||
        if not data:
 | 
			
		||||
            raise NoDataCollected(self.collector_name, name)
 | 
			
		||||
 | 
			
		||||
        return self.t_cloudkitty.format_service(name, data)
 | 
			
		||||
        return name, data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_conf(conf):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										279
									
								
								cloudkitty/dataframe.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								cloudkitty/dataframe.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,279 @@
 | 
			
		||||
# Copyright 2019 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.
 | 
			
		||||
#
 | 
			
		||||
import collections
 | 
			
		||||
import datetime
 | 
			
		||||
import decimal
 | 
			
		||||
import functools
 | 
			
		||||
 | 
			
		||||
import voluptuous
 | 
			
		||||
from werkzeug import datastructures
 | 
			
		||||
 | 
			
		||||
from cloudkitty import json_utils as json
 | 
			
		||||
from cloudkitty import tzutils
 | 
			
		||||
from cloudkitty import validation_utils as vutils
 | 
			
		||||
 | 
			
		||||
# NOTE(peschk_l): qty and price are converted to strings to avoid
 | 
			
		||||
# floating-point conversion issues:
 | 
			
		||||
# Decimal(0.121) == Decimal('0.12099999999999999644728632119')
 | 
			
		||||
# Decimal(str(0.121)) == Decimal('0.121')
 | 
			
		||||
DATAPOINT_SCHEMA = voluptuous.Schema({
 | 
			
		||||
    voluptuous.Required('vol'): {
 | 
			
		||||
        voluptuous.Required('unit'): vutils.get_string_type(),
 | 
			
		||||
        voluptuous.Required('qty'): voluptuous.Coerce(str),
 | 
			
		||||
    },
 | 
			
		||||
    voluptuous.Required('rating', default={}): {
 | 
			
		||||
        voluptuous.Required('price', default=0):
 | 
			
		||||
        voluptuous.Coerce(str),
 | 
			
		||||
    },
 | 
			
		||||
    voluptuous.Required('groupby'): vutils.DictTypeValidator(str, str),
 | 
			
		||||
    voluptuous.Required('metadata'): vutils.DictTypeValidator(str, str),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_DataPointBase = collections.namedtuple(
 | 
			
		||||
    "DataPoint",
 | 
			
		||||
    field_names=("unit", "qty", "price", "groupby", "metadata"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DataPoint(_DataPointBase):
 | 
			
		||||
 | 
			
		||||
    def __new__(cls, unit, qty, price, groupby, metadata):
 | 
			
		||||
        return _DataPointBase.__new__(
 | 
			
		||||
            cls,
 | 
			
		||||
            unit or "undefined",
 | 
			
		||||
            # NOTE(peschk_l): avoids floating-point issues.
 | 
			
		||||
            decimal.Decimal(str(qty) if isinstance(qty, float) else qty),
 | 
			
		||||
            decimal.Decimal(str(price) if isinstance(price, float) else price),
 | 
			
		||||
            datastructures.ImmutableDict(groupby),
 | 
			
		||||
            datastructures.ImmutableDict(metadata),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def set_price(self, price):
 | 
			
		||||
        """Sets the price of the DataPoint and returns a new object."""
 | 
			
		||||
        return self._replace(price=price)
 | 
			
		||||
 | 
			
		||||
    def as_dict(self, legacy=False, mutable=False):
 | 
			
		||||
        """Returns a dict representation of the object.
 | 
			
		||||
 | 
			
		||||
        The returned dict is immutable by default and has the
 | 
			
		||||
        following format::
 | 
			
		||||
           {
 | 
			
		||||
               "vol": {
 | 
			
		||||
                   "unit": "GiB",
 | 
			
		||||
                   "qty": 1.2,
 | 
			
		||||
               },
 | 
			
		||||
               "rating": {
 | 
			
		||||
                   "price": 0.04,
 | 
			
		||||
               },
 | 
			
		||||
               "groupby": {
 | 
			
		||||
                   "group_one": "one",
 | 
			
		||||
                   "group_two": "two",
 | 
			
		||||
               },
 | 
			
		||||
               "metadata": {
 | 
			
		||||
                   "attr_one": "one",
 | 
			
		||||
                   "attr_two": "two",
 | 
			
		||||
                },
 | 
			
		||||
           }
 | 
			
		||||
 | 
			
		||||
        The dict can also be returned in the legacy (v1 storage) format. In
 | 
			
		||||
        that case, `groupby` and `metadata` will be removed and merged together
 | 
			
		||||
        into the `desc` key.
 | 
			
		||||
 | 
			
		||||
        :param legacy: Defaults to False. If True, returned dict is in legacy
 | 
			
		||||
                       format.
 | 
			
		||||
        :type legacy: bool
 | 
			
		||||
        :param mutable: Defaults to False. If True, returns a normal dict
 | 
			
		||||
                        instead of an ImmutableDict.
 | 
			
		||||
        :type mutable: bool
 | 
			
		||||
        """
 | 
			
		||||
        output = {
 | 
			
		||||
            "vol": {
 | 
			
		||||
                "unit": self.unit,
 | 
			
		||||
                "qty": self.qty,
 | 
			
		||||
            },
 | 
			
		||||
            "rating": {
 | 
			
		||||
                "price": self.price,
 | 
			
		||||
            },
 | 
			
		||||
            "groupby": dict(self.groupby) if mutable else self.groupby,
 | 
			
		||||
            "metadata": dict(self.metadata) if mutable else self.metadata,
 | 
			
		||||
        }
 | 
			
		||||
        if legacy:
 | 
			
		||||
            desc = output.pop("metadata")
 | 
			
		||||
            desc.update(output.pop("groupby"))
 | 
			
		||||
            output['desc'] = desc
 | 
			
		||||
 | 
			
		||||
        return output if mutable else datastructures.ImmutableDict(output)
 | 
			
		||||
 | 
			
		||||
    def json(self, legacy=False):
 | 
			
		||||
        """Returns a json representation of the dict returned by `as_dict`.
 | 
			
		||||
 | 
			
		||||
        :param legacy: Defaults to False. If True, returned dict is in legacy
 | 
			
		||||
                       format.
 | 
			
		||||
        :type legacy: bool
 | 
			
		||||
        :rtype: str
 | 
			
		||||
        """
 | 
			
		||||
        return json.dumps(self.as_dict(legacy=legacy, mutable=True))
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_dict(cls, dict_, legacy=False):
 | 
			
		||||
        """Returns a new DataPoint instance build from a dict.
 | 
			
		||||
 | 
			
		||||
        :param dict_: Dict to build the DataPoint from
 | 
			
		||||
        :type dict_: dict
 | 
			
		||||
        :param legacy: Set to true to convert the dict to a the new format
 | 
			
		||||
                       before validating it.
 | 
			
		||||
        :rtype: DataPoint
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            if legacy:
 | 
			
		||||
                dict_['groupby'] = dict_.pop('desc')
 | 
			
		||||
                dict_['metadata'] = {}
 | 
			
		||||
            valid = DATAPOINT_SCHEMA(dict_)
 | 
			
		||||
            return cls(
 | 
			
		||||
                unit=valid["vol"]["unit"],
 | 
			
		||||
                qty=valid["vol"]["qty"],
 | 
			
		||||
                price=valid["rating"]["price"],
 | 
			
		||||
                groupby=valid["groupby"],
 | 
			
		||||
                metadata=valid["metadata"],
 | 
			
		||||
            )
 | 
			
		||||
        except (voluptuous.Invalid, KeyError) as e:
 | 
			
		||||
            raise ValueError("{} isn't a valid DataPoint: {}".format(dict_, e))
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def desc(self):
 | 
			
		||||
        output = dict(self.metadata)
 | 
			
		||||
        output.update(self.groupby)
 | 
			
		||||
        return datastructures.ImmutableDict(output)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DATAFRAME_SCHEMA = voluptuous.Schema({
 | 
			
		||||
    voluptuous.Required('period'): {
 | 
			
		||||
        voluptuous.Required('begin'): voluptuous.Any(
 | 
			
		||||
            datetime.datetime, voluptuous.Coerce(tzutils.dt_from_iso)),
 | 
			
		||||
        voluptuous.Required('end'): voluptuous.Any(
 | 
			
		||||
            datetime.datetime, voluptuous.Coerce(tzutils.dt_from_iso)),
 | 
			
		||||
    },
 | 
			
		||||
    voluptuous.Required('usage'): vutils.IterableValuesDict(
 | 
			
		||||
        str, DataPoint.from_dict),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DataFrame(object):
 | 
			
		||||
 | 
			
		||||
    __slots__ = ("start", "end", "_usage")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, start, end, usage=None):
 | 
			
		||||
        if not isinstance(start, datetime.datetime):
 | 
			
		||||
            raise TypeError(
 | 
			
		||||
                '"start" must be of type datetime.datetime, not {}'.format(
 | 
			
		||||
                    type(start)))
 | 
			
		||||
        if not isinstance(end, datetime.datetime):
 | 
			
		||||
            raise TypeError(
 | 
			
		||||
                '"end" must be of type datetime.datetime, not {}'.format(
 | 
			
		||||
                    type(end)))
 | 
			
		||||
        if usage is not None and not isinstance(usage, dict):
 | 
			
		||||
            raise TypeError(
 | 
			
		||||
                '"usage" must be a dict, not {}'.format(type(usage)))
 | 
			
		||||
        self.start = start
 | 
			
		||||
        self.end = end
 | 
			
		||||
        self._usage = collections.OrderedDict()
 | 
			
		||||
        if usage:
 | 
			
		||||
            for key in sorted(usage.keys()):
 | 
			
		||||
                self.add_points(usage[key], key)
 | 
			
		||||
 | 
			
		||||
    def as_dict(self, legacy=False, mutable=False):
 | 
			
		||||
        output = {
 | 
			
		||||
            "period": {"begin": self.start, "end": self.end},
 | 
			
		||||
            "usage": {
 | 
			
		||||
                key: [v.as_dict(legacy=legacy, mutable=mutable) for v in val]
 | 
			
		||||
                for key, val in self._usage.items()
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        return output if mutable else datastructures.ImmutableDict(output)
 | 
			
		||||
 | 
			
		||||
    def json(self, legacy=False):
 | 
			
		||||
        return json.dumps(self.as_dict(legacy=legacy, mutable=True))
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_dict(cls, dict_, legacy=False):
 | 
			
		||||
        try:
 | 
			
		||||
            schema = DATAFRAME_SCHEMA
 | 
			
		||||
            if legacy:
 | 
			
		||||
                validator = functools.partial(DataPoint.from_dict, legacy=True)
 | 
			
		||||
                # NOTE(peschk_l): __name__ is required for voluptuous exception
 | 
			
		||||
                # message formatting
 | 
			
		||||
                validator.__name__ = 'DataPoint.from_dict'
 | 
			
		||||
                # NOTE(peschk_l): In case the legacy format is required, we
 | 
			
		||||
                # create a new schema where DataPoint.from_dict is called with
 | 
			
		||||
                # legacy=True. The "extend" method does create a new objects,
 | 
			
		||||
                # and replaces existing keys with new ones.
 | 
			
		||||
                schema = DATAFRAME_SCHEMA.extend({
 | 
			
		||||
                    voluptuous.Required('usage'): vutils.IterableValuesDict(
 | 
			
		||||
                        str, validator
 | 
			
		||||
                    ),
 | 
			
		||||
                })
 | 
			
		||||
            valid = schema(dict_)
 | 
			
		||||
            return cls(
 | 
			
		||||
                valid["period"]["begin"],
 | 
			
		||||
                valid["period"]["end"],
 | 
			
		||||
                usage=valid["usage"])
 | 
			
		||||
        except (voluptuous.error.Invalid, KeyError) as e:
 | 
			
		||||
            raise ValueError("{} isn't a valid DataFrame: {}".format(dict_, e))
 | 
			
		||||
 | 
			
		||||
    def add_points(self, points, type_):
 | 
			
		||||
        """Adds multiple points to the DataFrame
 | 
			
		||||
 | 
			
		||||
        :param points: DataPoints to add.
 | 
			
		||||
        :type point: list of DataPoints
 | 
			
		||||
        """
 | 
			
		||||
        if type_ in self._usage:
 | 
			
		||||
            self._usage[type_] += points
 | 
			
		||||
        else:
 | 
			
		||||
            self._usage[type_] = points
 | 
			
		||||
 | 
			
		||||
    def add_point(self, point, type_):
 | 
			
		||||
        """Adds a single point to the DataFrame
 | 
			
		||||
 | 
			
		||||
        :param point: DataPoint to add.
 | 
			
		||||
        :type point: DataPoint
 | 
			
		||||
        """
 | 
			
		||||
        if type_ in self._usage:
 | 
			
		||||
            self._usage[type_].append(point)
 | 
			
		||||
        else:
 | 
			
		||||
            self._usage[type_] = [point]
 | 
			
		||||
 | 
			
		||||
    def iterpoints(self):
 | 
			
		||||
        """Iterates over all datapoints of the dataframe.
 | 
			
		||||
 | 
			
		||||
        Yields (type, point) tuples.
 | 
			
		||||
 | 
			
		||||
        :rtype: (str, DataPoint)
 | 
			
		||||
        """
 | 
			
		||||
        for type_, points in self._usage.items():
 | 
			
		||||
            for point in points:
 | 
			
		||||
                yield type_, point
 | 
			
		||||
 | 
			
		||||
    def itertypes(self):
 | 
			
		||||
        """Iterates over all types of the dataframe.
 | 
			
		||||
 | 
			
		||||
        Yields (type, (point, )) tuples.
 | 
			
		||||
 | 
			
		||||
        :rtype: (str, (DataPoint, ))
 | 
			
		||||
        """
 | 
			
		||||
        for type_, points in self._usage.items():
 | 
			
		||||
            yield type_, points
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return 'DataFrame(metrics=[{}])'.format(','.join(self._usage.keys()))
 | 
			
		||||
@@ -34,6 +34,7 @@ from tooz import coordination
 | 
			
		||||
 | 
			
		||||
from cloudkitty import collector
 | 
			
		||||
from cloudkitty import config  # noqa
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty import extension_manager
 | 
			
		||||
from cloudkitty import messaging
 | 
			
		||||
from cloudkitty import storage
 | 
			
		||||
@@ -249,18 +250,16 @@ class Worker(BaseWorker):
 | 
			
		||||
        next_timestamp = tzutils.add_delta(
 | 
			
		||||
            start_timestamp, timedelta(seconds=self._period))
 | 
			
		||||
 | 
			
		||||
        raw_data = self._collector.retrieve(
 | 
			
		||||
        name, data = self._collector.retrieve(
 | 
			
		||||
            metric,
 | 
			
		||||
            start_timestamp,
 | 
			
		||||
            next_timestamp,
 | 
			
		||||
            self._tenant_id,
 | 
			
		||||
        )
 | 
			
		||||
        if not raw_data:
 | 
			
		||||
        if not data:
 | 
			
		||||
            raise collector.NoDataCollected
 | 
			
		||||
 | 
			
		||||
        return {'period': {'begin': start_timestamp,
 | 
			
		||||
                           'end': next_timestamp},
 | 
			
		||||
                'usage': raw_data}
 | 
			
		||||
        return name, data
 | 
			
		||||
 | 
			
		||||
    def _do_collection(self, metrics, timestamp):
 | 
			
		||||
 | 
			
		||||
@@ -276,7 +275,7 @@ class Worker(BaseWorker):
 | 
			
		||||
                        metric=metric,
 | 
			
		||||
                        ts=timestamp)
 | 
			
		||||
                )
 | 
			
		||||
                return None
 | 
			
		||||
                return metric, None
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                LOG.warning(
 | 
			
		||||
                    '[scope: {scope}, worker: {worker}] Error while collecting'
 | 
			
		||||
@@ -293,8 +292,8 @@ class Worker(BaseWorker):
 | 
			
		||||
                # system in workers
 | 
			
		||||
                sys.exit(1)
 | 
			
		||||
 | 
			
		||||
        return list(filter(
 | 
			
		||||
            lambda x: x is not None,
 | 
			
		||||
        return dict(filter(
 | 
			
		||||
            lambda x: x[1] is not None,
 | 
			
		||||
            eventlet.GreenPool(size=CONF.orchestrator.max_greenthreads).imap(
 | 
			
		||||
                _get_result, metrics)))
 | 
			
		||||
 | 
			
		||||
@@ -307,14 +306,20 @@ class Worker(BaseWorker):
 | 
			
		||||
            metrics = list(self._conf['metrics'].keys())
 | 
			
		||||
 | 
			
		||||
            # Collection
 | 
			
		||||
            data = self._do_collection(metrics, timestamp)
 | 
			
		||||
            usage_data = self._do_collection(metrics, timestamp)
 | 
			
		||||
 | 
			
		||||
            frame = dataframe.DataFrame(
 | 
			
		||||
                start=timestamp,
 | 
			
		||||
                end=tzutils.add_delta(timestamp,
 | 
			
		||||
                                      timedelta(seconds=self._period)),
 | 
			
		||||
                usage=usage_data,
 | 
			
		||||
            )
 | 
			
		||||
            # Rating
 | 
			
		||||
            for processor in self._processors:
 | 
			
		||||
                processor.obj.process(data)
 | 
			
		||||
                frame = processor.obj.process(frame)
 | 
			
		||||
 | 
			
		||||
            # Writing
 | 
			
		||||
            self._storage.push(data, self._tenant_id)
 | 
			
		||||
            self._storage.push([frame], self._tenant_id)
 | 
			
		||||
            self._state.set_state(self._tenant_id, timestamp)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
#
 | 
			
		||||
import decimal
 | 
			
		||||
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty import rating
 | 
			
		||||
from cloudkitty.rating.hash.controllers import root as root_api
 | 
			
		||||
from cloudkitty.rating.hash.db import api as hash_db_api
 | 
			
		||||
@@ -149,9 +150,7 @@ class HashMap(rating.RatingProcessorBase):
 | 
			
		||||
                field_name = field_db.name
 | 
			
		||||
                self._load_field_entries(service_name, field_name, field_uuid)
 | 
			
		||||
 | 
			
		||||
    def add_rating_informations(self, data):
 | 
			
		||||
        if 'rating' not in data:
 | 
			
		||||
            data['rating'] = {'price': 0}
 | 
			
		||||
    def add_rating_informations(self, point):
 | 
			
		||||
        for entry in self._res.values():
 | 
			
		||||
            rate = entry['rate']
 | 
			
		||||
            flat = entry['flat']
 | 
			
		||||
@@ -161,14 +160,14 @@ class HashMap(rating.RatingProcessorBase):
 | 
			
		||||
                else:
 | 
			
		||||
                    rate *= entry['threshold']['cost']
 | 
			
		||||
            res = rate * flat
 | 
			
		||||
            # FIXME(sheeprine): Added here to ensure that qty is decimal
 | 
			
		||||
            res *= decimal.Decimal(data['vol']['qty'])
 | 
			
		||||
            res *= point.qty
 | 
			
		||||
            if entry['threshold']['scope'] == 'service':
 | 
			
		||||
                if entry['threshold']['type'] == 'flat':
 | 
			
		||||
                    res += entry['threshold']['cost']
 | 
			
		||||
                else:
 | 
			
		||||
                    res *= entry['threshold']['cost']
 | 
			
		||||
            data['rating']['price'] += res
 | 
			
		||||
            point = point.set_price(point.price + res)
 | 
			
		||||
        return point
 | 
			
		||||
 | 
			
		||||
    def update_result(self,
 | 
			
		||||
                      group,
 | 
			
		||||
@@ -228,7 +227,7 @@ class HashMap(rating.RatingProcessorBase):
 | 
			
		||||
                        True,
 | 
			
		||||
                        threshold_type)
 | 
			
		||||
 | 
			
		||||
    def process_services(self, service_name, data):
 | 
			
		||||
    def process_services(self, service_name, point):
 | 
			
		||||
        if service_name not in self._entries:
 | 
			
		||||
            return
 | 
			
		||||
        service_mappings = self._entries[service_name]['mappings']
 | 
			
		||||
@@ -238,15 +237,15 @@ class HashMap(rating.RatingProcessorBase):
 | 
			
		||||
                               mapping['cost'])
 | 
			
		||||
        service_thresholds = self._entries[service_name]['thresholds']
 | 
			
		||||
        self.process_thresholds(service_thresholds,
 | 
			
		||||
                                data['vol']['qty'],
 | 
			
		||||
                                point.qty,
 | 
			
		||||
                                'service')
 | 
			
		||||
 | 
			
		||||
    def process_fields(self, service_name, data):
 | 
			
		||||
    def process_fields(self, service_name, point):
 | 
			
		||||
        if service_name not in self._entries:
 | 
			
		||||
            return
 | 
			
		||||
        if 'fields' not in self._entries[service_name]:
 | 
			
		||||
            return
 | 
			
		||||
        desc_data = data['desc']
 | 
			
		||||
        desc_data = point.desc
 | 
			
		||||
        field_mappings = self._entries[service_name]['fields']
 | 
			
		||||
        for field_name, group_mappings in field_mappings.items():
 | 
			
		||||
            if field_name not in desc_data:
 | 
			
		||||
@@ -260,12 +259,12 @@ class HashMap(rating.RatingProcessorBase):
 | 
			
		||||
                                        'field')
 | 
			
		||||
 | 
			
		||||
    def process(self, data):
 | 
			
		||||
        for cur_data in data:
 | 
			
		||||
            cur_usage = cur_data['usage']
 | 
			
		||||
            for service_name, service_data in cur_usage.items():
 | 
			
		||||
                for item in service_data:
 | 
			
		||||
                    self._res = {}
 | 
			
		||||
                    self.process_services(service_name, item)
 | 
			
		||||
                    self.process_fields(service_name, item)
 | 
			
		||||
                    self.add_rating_informations(item)
 | 
			
		||||
        return data
 | 
			
		||||
        output = dataframe.DataFrame(start=data.start, end=data.end)
 | 
			
		||||
 | 
			
		||||
        for service_name, point in data.iterpoints():
 | 
			
		||||
            self._res = {}
 | 
			
		||||
            self.process_services(service_name, point)
 | 
			
		||||
            self.process_fields(service_name, point)
 | 
			
		||||
            output.add_point(self.add_rating_informations(point), service_name)
 | 
			
		||||
 | 
			
		||||
        return output
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,6 @@
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
#
 | 
			
		||||
import decimal
 | 
			
		||||
 | 
			
		||||
from cloudkitty import rating
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -39,10 +37,4 @@ class Noop(rating.RatingProcessorBase):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def process(self, data):
 | 
			
		||||
        for cur_data in data:
 | 
			
		||||
            cur_usage = cur_data['usage']
 | 
			
		||||
            for service in cur_usage:
 | 
			
		||||
                for entry in cur_usage[service]:
 | 
			
		||||
                    if 'rating' not in entry:
 | 
			
		||||
                        entry['rating'] = {'price': decimal.Decimal(0)}
 | 
			
		||||
        return data
 | 
			
		||||
 
 | 
			
		||||
@@ -80,5 +80,5 @@ class PyScripts(rating.RatingProcessorBase):
 | 
			
		||||
 | 
			
		||||
    def process(self, data):
 | 
			
		||||
        for script in self._scripts.values():
 | 
			
		||||
            self.start_script(script['code'], data)
 | 
			
		||||
            data = self.start_script(script['code'], data)
 | 
			
		||||
        return data
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ from oslo_config import cfg
 | 
			
		||||
from oslo_log import log as logging
 | 
			
		||||
from stevedore import driver
 | 
			
		||||
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty.storage import v2 as storage_v2
 | 
			
		||||
from cloudkitty import tzutils
 | 
			
		||||
 | 
			
		||||
@@ -76,18 +77,19 @@ class V1StorageAdapter(storage_v2.BaseStorage):
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __update_frames_timestamps(func, frames, **kwargs):
 | 
			
		||||
        for frame in frames:
 | 
			
		||||
            period = frame['period'] if 'period' in frame.keys() else frame
 | 
			
		||||
            begin = period['begin']
 | 
			
		||||
            end = period['end']
 | 
			
		||||
            if begin:
 | 
			
		||||
                period['begin'] = func(begin, **kwargs)
 | 
			
		||||
            start = frame.start
 | 
			
		||||
            end = frame.end
 | 
			
		||||
            if start:
 | 
			
		||||
                frame.start = func(start, **kwargs)
 | 
			
		||||
            if end:
 | 
			
		||||
                period['end'] = func(end, **kwargs)
 | 
			
		||||
                frame.end = func(end, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def push(self, dataframes, scope_id=None):
 | 
			
		||||
        if dataframes:
 | 
			
		||||
            self._make_dataframes_naive(dataframes)
 | 
			
		||||
            self.storage.append(dataframes, scope_id)
 | 
			
		||||
            self.storage.append(
 | 
			
		||||
                [d.as_dict(mutable=True, legacy=True) for d in dataframes],
 | 
			
		||||
                scope_id)
 | 
			
		||||
            self.storage.commit(scope_id)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@@ -107,12 +109,24 @@ class V1StorageAdapter(storage_v2.BaseStorage):
 | 
			
		||||
            tzutils.local_to_utc(end, naive=True) if end else None,
 | 
			
		||||
            res_type=metric_types,
 | 
			
		||||
            tenant_id=tenant_id)
 | 
			
		||||
        frames = [dataframe.DataFrame.from_dict(frame, legacy=True)
 | 
			
		||||
                  for frame in frames]
 | 
			
		||||
        self._localize_dataframes(frames)
 | 
			
		||||
        return {
 | 
			
		||||
            'total': len(frames),
 | 
			
		||||
            'dataframes': frames,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _localize_total(iterable):
 | 
			
		||||
        for elem in iterable:
 | 
			
		||||
            begin = elem['begin']
 | 
			
		||||
            end = elem['end']
 | 
			
		||||
            if begin:
 | 
			
		||||
                elem['begin'] = tzutils.utc_to_local(begin)
 | 
			
		||||
            if end:
 | 
			
		||||
                elem['end'] = tzutils.utc_to_local(end)
 | 
			
		||||
 | 
			
		||||
    def total(self, groupby=None,
 | 
			
		||||
              begin=None, end=None,
 | 
			
		||||
              metric_types=None,
 | 
			
		||||
@@ -145,8 +159,7 @@ class V1StorageAdapter(storage_v2.BaseStorage):
 | 
			
		||||
                t['type'] = t.get('res_type')
 | 
			
		||||
            else:
 | 
			
		||||
                t['type'] = None
 | 
			
		||||
 | 
			
		||||
        self._localize_dataframes(total)
 | 
			
		||||
        self._localize_total(total)
 | 
			
		||||
        return {
 | 
			
		||||
            'total': len(total),
 | 
			
		||||
            'results': total,
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ class RatedDataFrame(Base, models.ModelBase):
 | 
			
		||||
        res_dict['rating'] = rating_dict
 | 
			
		||||
        res_dict['desc'] = json.loads(self.desc)
 | 
			
		||||
        res_dict['vol'] = vol_dict
 | 
			
		||||
        res_dict['tenant_id'] = self.tenant_id
 | 
			
		||||
        res_dict['desc']['tenant_id'] = self.tenant_id
 | 
			
		||||
 | 
			
		||||
        # Add resource to the usage dict
 | 
			
		||||
        usage_dict = {}
 | 
			
		||||
 
 | 
			
		||||
@@ -53,39 +53,8 @@ class BaseStorage(object):
 | 
			
		||||
    def push(self, dataframes, scope_id=None):
 | 
			
		||||
        """Pushes dataframes to the storage backend
 | 
			
		||||
 | 
			
		||||
        A dataframe has the following format::
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
                "usage": {
 | 
			
		||||
                    "bananas": [ # metric name
 | 
			
		||||
                        {
 | 
			
		||||
                            "vol": {
 | 
			
		||||
                                "unit": "banana",
 | 
			
		||||
                                "qty": 1
 | 
			
		||||
                            },
 | 
			
		||||
                            "rating": {
 | 
			
		||||
                                "price": 1
 | 
			
		||||
                            },
 | 
			
		||||
                            "groupby": {
 | 
			
		||||
                                "xxx_id": "hello",
 | 
			
		||||
                                "yyy_id": "bye",
 | 
			
		||||
                            },
 | 
			
		||||
                            "metadata": {
 | 
			
		||||
                                "flavor": "chocolate",
 | 
			
		||||
                                "eaten_by": "gorilla",
 | 
			
		||||
                            },
 | 
			
		||||
                       }
 | 
			
		||||
                    ],
 | 
			
		||||
                    "metric_name2": [...],
 | 
			
		||||
                }
 | 
			
		||||
               "period": {
 | 
			
		||||
                    "begin": "1239781290", # timestamp
 | 
			
		||||
                    "end": "1239793490", # timestamp
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        :param dataframes: List of dataframes
 | 
			
		||||
        :type dataframes: list
 | 
			
		||||
        :type dataframes: [cloudkitty.dataframe.DataFrame]
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
 
 | 
			
		||||
@@ -12,15 +12,14 @@
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
#
 | 
			
		||||
import copy
 | 
			
		||||
import datetime
 | 
			
		||||
import decimal
 | 
			
		||||
 | 
			
		||||
import influxdb
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_log import log
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty.storage import v2 as v2_storage
 | 
			
		||||
from cloudkitty import tzutils
 | 
			
		||||
 | 
			
		||||
@@ -112,21 +111,27 @@ class InfluxClient(object):
 | 
			
		||||
    def append_point(self,
 | 
			
		||||
                     metric_type,
 | 
			
		||||
                     timestamp,
 | 
			
		||||
                     qty, price, unit,
 | 
			
		||||
                     fields, tags):
 | 
			
		||||
        """Adds two points to commit to InfluxDB"""
 | 
			
		||||
                     point):
 | 
			
		||||
        """Adds a point to commit to InfluxDB.
 | 
			
		||||
 | 
			
		||||
        measurement_fields = copy.deepcopy(fields)
 | 
			
		||||
        measurement_fields['qty'] = float(qty)
 | 
			
		||||
        measurement_fields['price'] = float(price)
 | 
			
		||||
        measurement_fields['unit'] = unit
 | 
			
		||||
        :param metric_type: Name of the metric type
 | 
			
		||||
        :type metric_type: str
 | 
			
		||||
        :param timestamp: Timestamp of the time
 | 
			
		||||
        :type timestamp: datetime.datetime
 | 
			
		||||
        :param point: Point to push
 | 
			
		||||
        :type point: dataframe.DataPoint
 | 
			
		||||
        """
 | 
			
		||||
        measurement_fields = dict(point.metadata)
 | 
			
		||||
        measurement_fields['qty'] = float(point.qty)
 | 
			
		||||
        measurement_fields['price'] = float(point.price)
 | 
			
		||||
        measurement_fields['unit'] = point.unit
 | 
			
		||||
        # Unfortunately, this seems to be the fastest way: Having several
 | 
			
		||||
        # measurements would imply a high client-side workload, and this allows
 | 
			
		||||
        # us to filter out unrequired keys
 | 
			
		||||
        measurement_fields['groupby'] = '|'.join(tags.keys())
 | 
			
		||||
        measurement_fields['metadata'] = '|'.join(fields.keys())
 | 
			
		||||
        measurement_fields['groupby'] = '|'.join(point.groupby.keys())
 | 
			
		||||
        measurement_fields['metadata'] = '|'.join(point.metadata.keys())
 | 
			
		||||
 | 
			
		||||
        measurement_tags = copy.deepcopy(tags)
 | 
			
		||||
        measurement_tags = dict(point.groupby)
 | 
			
		||||
        measurement_tags['type'] = metric_type
 | 
			
		||||
 | 
			
		||||
        self._points.append({
 | 
			
		||||
@@ -243,19 +248,10 @@ class InfluxStorage(v2_storage.BaseStorage):
 | 
			
		||||
 | 
			
		||||
    def push(self, dataframes, scope_id=None):
 | 
			
		||||
 | 
			
		||||
        for dataframe in dataframes:
 | 
			
		||||
            timestamp = dataframe['period']['begin']
 | 
			
		||||
            for metric_name, metrics in dataframe['usage'].items():
 | 
			
		||||
                for metric in metrics:
 | 
			
		||||
                    self._conn.append_point(
 | 
			
		||||
                        metric_name,
 | 
			
		||||
                        timestamp,
 | 
			
		||||
                        metric['vol']['qty'],
 | 
			
		||||
                        metric['rating']['price'],
 | 
			
		||||
                        metric['vol']['unit'],
 | 
			
		||||
                        metric['metadata'],
 | 
			
		||||
                        metric['groupby'],
 | 
			
		||||
                    )
 | 
			
		||||
        for frame in dataframes:
 | 
			
		||||
            timestamp = frame.start
 | 
			
		||||
            for type_, point in frame.iterpoints():
 | 
			
		||||
                self._conn.append_point(type_, timestamp, point)
 | 
			
		||||
 | 
			
		||||
        self._conn.commit()
 | 
			
		||||
 | 
			
		||||
@@ -269,21 +265,17 @@ class InfluxStorage(v2_storage.BaseStorage):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _point_to_dataframe_entry(point):
 | 
			
		||||
        groupby = (point.pop('groupby', None) or '').split('|')
 | 
			
		||||
        groupby = [g for g in groupby if g]
 | 
			
		||||
        metadata = (point.pop('metadata', None) or '').split('|')
 | 
			
		||||
        metadata = [m for m in metadata if m]
 | 
			
		||||
        return {
 | 
			
		||||
            'vol': {
 | 
			
		||||
                'unit': point['unit'],
 | 
			
		||||
                'qty': decimal.Decimal(point['qty']),
 | 
			
		||||
            },
 | 
			
		||||
            'rating': {
 | 
			
		||||
                'price': point['price'],
 | 
			
		||||
            },
 | 
			
		||||
            'groupby': {key: point.get(key, '') for key in groupby},
 | 
			
		||||
            'metadata': {key: point.get(key, '') for key in metadata},
 | 
			
		||||
        }
 | 
			
		||||
        groupby = filter(lambda x: bool(x),
 | 
			
		||||
                         (point.pop('groupby', None) or '').split('|'))
 | 
			
		||||
        metadata = filter(lambda x: bool(x),
 | 
			
		||||
                          (point.pop('metadata', None) or '').split('|'))
 | 
			
		||||
        return dataframe.DataPoint(
 | 
			
		||||
            point['unit'],
 | 
			
		||||
            point['qty'],
 | 
			
		||||
            point['price'],
 | 
			
		||||
            {key: point.get(key, '') for key in groupby},
 | 
			
		||||
            {key: point.get(key, '') for key in metadata},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _build_dataframes(self, points):
 | 
			
		||||
        dataframes = {}
 | 
			
		||||
@@ -291,21 +283,17 @@ class InfluxStorage(v2_storage.BaseStorage):
 | 
			
		||||
            point_type = point['type']
 | 
			
		||||
            time = tzutils.dt_from_iso(point['time'])
 | 
			
		||||
            if time not in dataframes.keys():
 | 
			
		||||
                dataframes[time] = {
 | 
			
		||||
                    'period': {
 | 
			
		||||
                        'begin': time,
 | 
			
		||||
                        'end': tzutils.add_delta(
 | 
			
		||||
                            time, datetime.timedelta(seconds=self._period))
 | 
			
		||||
                    },
 | 
			
		||||
                    'usage': {},
 | 
			
		||||
                }
 | 
			
		||||
            usage = dataframes[time]['usage']
 | 
			
		||||
            if point_type not in usage.keys():
 | 
			
		||||
                usage[point_type] = []
 | 
			
		||||
            usage[point_type].append(self._point_to_dataframe_entry(point))
 | 
			
		||||
                dataframes[time] = dataframe.DataFrame(
 | 
			
		||||
                    start=time,
 | 
			
		||||
                    end=tzutils.add_delta(
 | 
			
		||||
                        time, datetime.timedelta(seconds=self._period)),
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            dataframes[time].add_point(
 | 
			
		||||
                self._point_to_dataframe_entry(point), point_type)
 | 
			
		||||
 | 
			
		||||
        output = list(dataframes.values())
 | 
			
		||||
        output.sort(key=lambda x: x['period']['begin'])
 | 
			
		||||
        output.sort(key=lambda frame: frame.start)
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
    def retrieve(self, begin=None, end=None,
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ from cloudkitty import collector
 | 
			
		||||
from cloudkitty.collector import exceptions
 | 
			
		||||
from cloudkitty.collector import prometheus
 | 
			
		||||
from cloudkitty.common.prometheus_client import PrometheusResponseError
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty import tests
 | 
			
		||||
from cloudkitty.tests import samples
 | 
			
		||||
from cloudkitty import transformer
 | 
			
		||||
@@ -119,34 +120,17 @@ class PrometheusCollectorTest(tests.TestCase):
 | 
			
		||||
        self.assertEqual(expected, actual)
 | 
			
		||||
 | 
			
		||||
    def test_format_retrieve(self):
 | 
			
		||||
        expected = {
 | 
			
		||||
            'http_requests_total': [
 | 
			
		||||
                {
 | 
			
		||||
                    'desc': {
 | 
			
		||||
                        'bar': '', 'foo': '', 'project_id': '',
 | 
			
		||||
                        'code': '200', 'instance': 'localhost:9090',
 | 
			
		||||
                    },
 | 
			
		||||
                    'groupby': {'bar': '', 'foo': '', 'project_id': ''},
 | 
			
		||||
                    'metadata': {'code': '200', 'instance': 'localhost:9090'},
 | 
			
		||||
                    'vol': {
 | 
			
		||||
                        'qty': Decimal('7'),
 | 
			
		||||
                        'unit': 'instance'
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    'desc': {
 | 
			
		||||
                        'bar': '', 'foo': '', 'project_id': '',
 | 
			
		||||
                        'code': '200', 'instance': 'localhost:9090',
 | 
			
		||||
                    },
 | 
			
		||||
                    'groupby': {'bar': '', 'foo': '', 'project_id': ''},
 | 
			
		||||
                    'metadata': {'code': '200', 'instance': 'localhost:9090'},
 | 
			
		||||
                    'vol': {
 | 
			
		||||
                        'qty': Decimal('42'),
 | 
			
		||||
                        'unit': 'instance'
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
        expected_name = 'http_requests_total'
 | 
			
		||||
        expected_data = [
 | 
			
		||||
            dataframe.DataPoint(
 | 
			
		||||
                'instance', '7', '0',
 | 
			
		||||
                {'bar': '', 'foo': '', 'project_id': ''},
 | 
			
		||||
                {'code': '200', 'instance': 'localhost:9090'}),
 | 
			
		||||
            dataframe.DataPoint(
 | 
			
		||||
                'instance', '42', '0',
 | 
			
		||||
                {'bar': '', 'foo': '', 'project_id': ''},
 | 
			
		||||
                {'code': '200', 'instance': 'localhost:9090'}),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        no_response = mock.patch(
 | 
			
		||||
            'cloudkitty.common.prometheus_client.PrometheusClient.get_instant',
 | 
			
		||||
@@ -154,7 +138,7 @@ class PrometheusCollectorTest(tests.TestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        with no_response:
 | 
			
		||||
            actual = self.collector.retrieve(
 | 
			
		||||
            actual_name, actual_data = self.collector.retrieve(
 | 
			
		||||
                metric_name='http_requests_total',
 | 
			
		||||
                start=samples.FIRST_PERIOD_BEGIN,
 | 
			
		||||
                end=samples.FIRST_PERIOD_END,
 | 
			
		||||
@@ -162,7 +146,8 @@ class PrometheusCollectorTest(tests.TestCase):
 | 
			
		||||
                q_filter=None,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(expected, actual)
 | 
			
		||||
        self.assertEqual(expected_name, actual_name)
 | 
			
		||||
        self.assertEqual(expected_data, actual_data)
 | 
			
		||||
 | 
			
		||||
    def test_format_retrieve_raise_NoDataCollected(self):
 | 
			
		||||
        no_response = mock.patch(
 | 
			
		||||
 
 | 
			
		||||
@@ -14,10 +14,12 @@
 | 
			
		||||
#    under the License.
 | 
			
		||||
#
 | 
			
		||||
import abc
 | 
			
		||||
import collections
 | 
			
		||||
import datetime
 | 
			
		||||
import decimal
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from dateutil import tz
 | 
			
		||||
from gabbi import fixture
 | 
			
		||||
import mock
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
@@ -35,6 +37,7 @@ import wsmeext.pecan as wsme_pecan
 | 
			
		||||
 | 
			
		||||
from cloudkitty.api import app
 | 
			
		||||
from cloudkitty.api import middleware
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty import db
 | 
			
		||||
from cloudkitty.db import api as ck_db_api
 | 
			
		||||
from cloudkitty import messaging
 | 
			
		||||
@@ -48,7 +51,8 @@ from cloudkitty.tests import utils as test_utils
 | 
			
		||||
from cloudkitty import tzutils
 | 
			
		||||
from cloudkitty import utils as ck_utils
 | 
			
		||||
 | 
			
		||||
INITIAL_TIMESTAMP = 1420070400
 | 
			
		||||
 | 
			
		||||
INITIAL_DT = datetime.datetime(2015, 1, 1, tzinfo=tz.UTC)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UUIDFixture(fixture.GabbiFixture):
 | 
			
		||||
@@ -294,41 +298,31 @@ class QuoteFakeRPC(BaseFakeRPC):
 | 
			
		||||
 | 
			
		||||
class BaseStorageDataFixture(fixture.GabbiFixture):
 | 
			
		||||
    def create_fake_data(self, begin, end, project_id):
 | 
			
		||||
        if isinstance(begin, int):
 | 
			
		||||
            begin = ck_utils.ts2dt(begin)
 | 
			
		||||
        if isinstance(end, int):
 | 
			
		||||
            end = ck_utils.ts2dt(end)
 | 
			
		||||
        data = [{
 | 
			
		||||
            "period": {
 | 
			
		||||
                "begin": begin,
 | 
			
		||||
                "end": end},
 | 
			
		||||
            "usage": {
 | 
			
		||||
                "cpu": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "desc": {
 | 
			
		||||
                            "dummy": True,
 | 
			
		||||
                            "fake_meta": 1.0,
 | 
			
		||||
                            "project_id": project_id},
 | 
			
		||||
                        "vol": {
 | 
			
		||||
                            "qty": 1,
 | 
			
		||||
                            "unit": "nothing"},
 | 
			
		||||
                        "rating": {
 | 
			
		||||
                            "price": decimal.Decimal('1.337')}}]}}, {
 | 
			
		||||
            "period": {
 | 
			
		||||
                "begin": begin,
 | 
			
		||||
                "end": end},
 | 
			
		||||
            "usage": {
 | 
			
		||||
                "image.size": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "desc": {
 | 
			
		||||
                            "dummy": True,
 | 
			
		||||
                            "fake_meta": 1.0,
 | 
			
		||||
                            "project_id": project_id},
 | 
			
		||||
                        "vol": {
 | 
			
		||||
                            "qty": 1,
 | 
			
		||||
                            "unit": "nothing"},
 | 
			
		||||
                        "rating": {
 | 
			
		||||
                            "price": decimal.Decimal('0.121')}}]}}]
 | 
			
		||||
 | 
			
		||||
        cpu_point = dataframe.DataPoint(
 | 
			
		||||
            unit="nothing",
 | 
			
		||||
            qty=1,
 | 
			
		||||
            groupby={"fake_meta": 1.0, "project_id": project_id},
 | 
			
		||||
            metadata={"dummy": True},
 | 
			
		||||
            price=decimal.Decimal('1.337'),
 | 
			
		||||
        )
 | 
			
		||||
        image_point = dataframe.DataPoint(
 | 
			
		||||
            unit="nothing",
 | 
			
		||||
            qty=1,
 | 
			
		||||
            groupby={"fake_meta": 1.0, "project_id": project_id},
 | 
			
		||||
            metadata={"dummy": True},
 | 
			
		||||
            price=decimal.Decimal('0.121'),
 | 
			
		||||
        )
 | 
			
		||||
        data = [
 | 
			
		||||
            dataframe.DataFrame(
 | 
			
		||||
                start=begin, end=end,
 | 
			
		||||
                usage=collections.OrderedDict({"cpu": [cpu_point]}),
 | 
			
		||||
            ),
 | 
			
		||||
            dataframe.DataFrame(
 | 
			
		||||
                start=begin, end=end,
 | 
			
		||||
                usage=collections.OrderedDict({"image.size": [image_point]}),
 | 
			
		||||
            ),
 | 
			
		||||
        ]
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def start_fixture(self):
 | 
			
		||||
@@ -356,33 +350,38 @@ class BaseStorageDataFixture(fixture.GabbiFixture):
 | 
			
		||||
class StorageDataFixture(BaseStorageDataFixture):
 | 
			
		||||
    def initialize_data(self):
 | 
			
		||||
        nodata_duration = (24 * 3 + 12) * 3600
 | 
			
		||||
        hour_delta = datetime.timedelta(seconds=3600)
 | 
			
		||||
        tenant_list = ['8f82cc70-e50c-466e-8624-24bdea811375',
 | 
			
		||||
                       '7606a24a-b8ad-4ae0-be6c-3d7a41334a2e']
 | 
			
		||||
        data_ts = INITIAL_TIMESTAMP + nodata_duration + 3600
 | 
			
		||||
        data_duration = (24 * 2 + 8) * 3600
 | 
			
		||||
        for i in range(data_ts,
 | 
			
		||||
                       data_ts + data_duration,
 | 
			
		||||
                       3600):
 | 
			
		||||
        data_dt = INITIAL_DT + datetime.timedelta(
 | 
			
		||||
            seconds=nodata_duration + 3600)
 | 
			
		||||
        data_duration = datetime.timedelta(seconds=(24 * 2 + 8) * 3600)
 | 
			
		||||
 | 
			
		||||
        iter_dt = data_dt
 | 
			
		||||
        while iter_dt < data_dt + data_duration:
 | 
			
		||||
            data = self.create_fake_data(
 | 
			
		||||
                i, i + 3600, tenant_list[0])
 | 
			
		||||
                iter_dt, iter_dt + hour_delta, tenant_list[0])
 | 
			
		||||
            self.storage.push(data, tenant_list[0])
 | 
			
		||||
        half_duration = int(data_duration / 2)
 | 
			
		||||
        for i in range(data_ts,
 | 
			
		||||
                       data_ts + half_duration,
 | 
			
		||||
                       3600):
 | 
			
		||||
            data = self.create_fake_data(i, i + 3600, tenant_list[1])
 | 
			
		||||
            iter_dt += hour_delta
 | 
			
		||||
 | 
			
		||||
        iter_dt = data_dt
 | 
			
		||||
        while iter_dt < data_dt + data_duration / 2:
 | 
			
		||||
            data = self.create_fake_data(
 | 
			
		||||
                iter_dt, iter_dt + hour_delta, tenant_list[1])
 | 
			
		||||
            self.storage.push(data, tenant_list[1])
 | 
			
		||||
            iter_dt += hour_delta
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NowStorageDataFixture(BaseStorageDataFixture):
 | 
			
		||||
    def initialize_data(self):
 | 
			
		||||
        begin = ck_utils.get_month_start_timestamp()
 | 
			
		||||
        for i in range(begin,
 | 
			
		||||
                       begin + 3600 * 12,
 | 
			
		||||
                       3600):
 | 
			
		||||
        dt = tzutils.get_month_start(naive=True).replace(tzinfo=tz.UTC)
 | 
			
		||||
        hour_delta = datetime.timedelta(seconds=3600)
 | 
			
		||||
        limit = dt + hour_delta * 12
 | 
			
		||||
        while dt < limit:
 | 
			
		||||
            project_id = '3d9a1b33-482f-42fd-aef9-b575a3da9369'
 | 
			
		||||
            data = self.create_fake_data(i, i + 3600, project_id)
 | 
			
		||||
            data = self.create_fake_data(dt, dt + hour_delta, project_id)
 | 
			
		||||
            self.storage.push(data, project_id)
 | 
			
		||||
            dt += hour_delta
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ScopeStateFixture(fixture.GabbiFixture):
 | 
			
		||||
 
 | 
			
		||||
@@ -75,8 +75,8 @@ tests:
 | 
			
		||||
      $.dataframes[0].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[0].resources[0].rating: "1.337"
 | 
			
		||||
      $.dataframes[0].resources[0].service: "cpu"
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
      $.dataframes[1].tenant_id: "8f82cc70-e50c-466e-8624-24bdea811375"
 | 
			
		||||
      $.dataframes[1].begin: "2015-01-04T13:00:00"
 | 
			
		||||
      $.dataframes[1].end: "2015-01-04T14:00:00"
 | 
			
		||||
@@ -84,8 +84,8 @@ tests:
 | 
			
		||||
      $.dataframes[1].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[1].resources[0].rating: "0.121"
 | 
			
		||||
      $.dataframes[1].resources[0].service: "image.size"
 | 
			
		||||
      $.dataframes[1].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[1].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[1].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[1].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
 | 
			
		||||
  - name: fetch data for the second tenant
 | 
			
		||||
    url: /v1/storage/dataframes
 | 
			
		||||
@@ -103,8 +103,8 @@ tests:
 | 
			
		||||
      $.dataframes[0].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[0].resources[0].rating: "1.337"
 | 
			
		||||
      $.dataframes[0].resources[0].service: "cpu"
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
      $.dataframes[1].tenant_id: "7606a24a-b8ad-4ae0-be6c-3d7a41334a2e"
 | 
			
		||||
      $.dataframes[1].begin: "2015-01-04T13:00:00"
 | 
			
		||||
      $.dataframes[1].end: "2015-01-04T14:00:00"
 | 
			
		||||
@@ -112,8 +112,8 @@ tests:
 | 
			
		||||
      $.dataframes[1].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[1].resources[0].rating: "0.121"
 | 
			
		||||
      $.dataframes[1].resources[0].service: "image.size"
 | 
			
		||||
      $.dataframes[1].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[1].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[1].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[1].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
 | 
			
		||||
  - name: fetch data for multiple tenants
 | 
			
		||||
    url: /v1/storage/dataframes
 | 
			
		||||
@@ -130,8 +130,8 @@ tests:
 | 
			
		||||
      $.dataframes[0].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[0].resources[0].rating: "1.337"
 | 
			
		||||
      $.dataframes[0].resources[0].service: "cpu"
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
      $.dataframes[1].tenant_id: "8f82cc70-e50c-466e-8624-24bdea811375"
 | 
			
		||||
      $.dataframes[1].begin: "2015-01-04T13:00:00"
 | 
			
		||||
      $.dataframes[1].end: "2015-01-04T14:00:00"
 | 
			
		||||
@@ -139,8 +139,8 @@ tests:
 | 
			
		||||
      $.dataframes[1].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[1].resources[0].rating: "0.121"
 | 
			
		||||
      $.dataframes[1].resources[0].service: "image.size"
 | 
			
		||||
      $.dataframes[1].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[1].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[1].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[1].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
      $.dataframes[2].tenant_id: "7606a24a-b8ad-4ae0-be6c-3d7a41334a2e"
 | 
			
		||||
      $.dataframes[2].begin: "2015-01-04T13:00:00"
 | 
			
		||||
      $.dataframes[2].end: "2015-01-04T14:00:00"
 | 
			
		||||
@@ -148,8 +148,8 @@ tests:
 | 
			
		||||
      $.dataframes[2].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[2].resources[0].rating: "1.337"
 | 
			
		||||
      $.dataframes[2].resources[0].service: "cpu"
 | 
			
		||||
      $.dataframes[2].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[2].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[2].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[2].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
      $.dataframes[3].tenant_id: "7606a24a-b8ad-4ae0-be6c-3d7a41334a2e"
 | 
			
		||||
      $.dataframes[3].begin: "2015-01-04T13:00:00"
 | 
			
		||||
      $.dataframes[3].end: "2015-01-04T14:00:00"
 | 
			
		||||
@@ -157,8 +157,8 @@ tests:
 | 
			
		||||
      $.dataframes[3].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[3].resources[0].rating: "0.121"
 | 
			
		||||
      $.dataframes[3].resources[0].service: "image.size"
 | 
			
		||||
      $.dataframes[3].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[3].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[3].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[3].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
 | 
			
		||||
  - name: fetch data filtering on cpu service and tenant
 | 
			
		||||
    url: /v1/storage/dataframes
 | 
			
		||||
@@ -177,8 +177,8 @@ tests:
 | 
			
		||||
      $.dataframes[0].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[0].resources[0].rating: "1.337"
 | 
			
		||||
      $.dataframes[0].resources[0].service: "cpu"
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
 | 
			
		||||
  - name: fetch data filtering on image service and tenant
 | 
			
		||||
    url: /v1/storage/dataframes
 | 
			
		||||
@@ -197,8 +197,8 @@ tests:
 | 
			
		||||
      $.dataframes[0].resources[0].volume: "1"
 | 
			
		||||
      $.dataframes[0].resources[0].rating: "0.121"
 | 
			
		||||
      $.dataframes[0].resources[0].service: "image.size"
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: true
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: 1.0
 | 
			
		||||
      $.dataframes[0].resources[0].desc.dummy: 'True'
 | 
			
		||||
      $.dataframes[0].resources[0].desc.fake_meta: '1.0'
 | 
			
		||||
 | 
			
		||||
  - name: fetch data filtering on service with no data and tenant
 | 
			
		||||
    url: /v1/storage/dataframes
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ import decimal
 | 
			
		||||
 | 
			
		||||
from oslo_utils import uuidutils
 | 
			
		||||
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty import utils as ck_utils
 | 
			
		||||
 | 
			
		||||
# These have a different format in order to check that both forms are supported
 | 
			
		||||
@@ -40,21 +41,24 @@ COMPUTE_METADATA = {
 | 
			
		||||
    'flavor': 'm1.nano',
 | 
			
		||||
    'image_id': 'f5600101-8fa2-4864-899e-ebcb7ed6b568',
 | 
			
		||||
    'instance_id': '26c084e1-b8f1-4cbc-a7ec-e8b356788a17',
 | 
			
		||||
    'id': '1558f911-b55a-4fd2-9173-c8f1f23e5639',
 | 
			
		||||
    'resource_id': '1558f911-b55a-4fd2-9173-c8f1f23e5639',
 | 
			
		||||
    'memory': '64',
 | 
			
		||||
    'metadata': {
 | 
			
		||||
        'farm': 'prod'
 | 
			
		||||
    },
 | 
			
		||||
    'name': 'prod1',
 | 
			
		||||
    'vcpus': '1'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
COMPUTE_GROUPBY = {
 | 
			
		||||
    'id': '1558f911-b55a-4fd2-9173-c8f1f23e5639',
 | 
			
		||||
    'project_id': 'f266f30b11f246b589fd266f85eeec39',
 | 
			
		||||
    'user_id': '55b3379b949243009ee96972fbf51ed1',
 | 
			
		||||
    'vcpus': '1'}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
IMAGE_METADATA = {
 | 
			
		||||
    'checksum': '836c69cbcd1dc4f225daedbab6edc7c7',
 | 
			
		||||
    'resource_id': '7b5b73f2-9181-4307-a710-b1aa6472526d',
 | 
			
		||||
    'id': '7b5b73f2-9181-4307-a710-b1aa6472526d',
 | 
			
		||||
    'container_format': 'aki',
 | 
			
		||||
    'created_at': '2014-06-04T16:26:01',
 | 
			
		||||
    'deleted': 'False',
 | 
			
		||||
@@ -67,48 +71,43 @@ IMAGE_METADATA = {
 | 
			
		||||
    'protected': 'False',
 | 
			
		||||
    'size': '4969360',
 | 
			
		||||
    'status': 'active',
 | 
			
		||||
    'updated_at': '2014-06-04T16:26:02'}
 | 
			
		||||
    'updated_at': '2014-06-04T16:26:02',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
IMAGE_GROUPBY = {
 | 
			
		||||
    'id': '7b5b73f2-9181-4307-a710-b1aa6472526d',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FIRST_PERIOD = {
 | 
			
		||||
    'begin': FIRST_PERIOD_BEGIN,
 | 
			
		||||
    'end': FIRST_PERIOD_END}
 | 
			
		||||
    'end': FIRST_PERIOD_END,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SECOND_PERIOD = {
 | 
			
		||||
    'begin': SECOND_PERIOD_BEGIN,
 | 
			
		||||
    'end': SECOND_PERIOD_END}
 | 
			
		||||
    'end': SECOND_PERIOD_END,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
COLLECTED_DATA = [
 | 
			
		||||
    dataframe.DataFrame(start=FIRST_PERIOD["begin"],
 | 
			
		||||
                        end=FIRST_PERIOD["end"]),
 | 
			
		||||
    dataframe.DataFrame(start=SECOND_PERIOD["begin"],
 | 
			
		||||
                        end=SECOND_PERIOD["end"]),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
_INSTANCE_POINT = dataframe.DataPoint(
 | 
			
		||||
    'instance', '1.0', '0.42', COMPUTE_GROUPBY, COMPUTE_METADATA)
 | 
			
		||||
 | 
			
		||||
_IMAGE_SIZE_POINT = dataframe.DataPoint(
 | 
			
		||||
    'image', '1.0', '0.1337', IMAGE_GROUPBY, IMAGE_METADATA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
COLLECTED_DATA[0].add_point(_INSTANCE_POINT, 'instance')
 | 
			
		||||
COLLECTED_DATA[0].add_point(_IMAGE_SIZE_POINT, 'image.size')
 | 
			
		||||
COLLECTED_DATA[1].add_point(_INSTANCE_POINT, 'instance')
 | 
			
		||||
 | 
			
		||||
COLLECTED_DATA = [{
 | 
			
		||||
    'period': FIRST_PERIOD,
 | 
			
		||||
    'usage': {
 | 
			
		||||
        'instance': [{
 | 
			
		||||
            'desc': COMPUTE_METADATA,
 | 
			
		||||
            'vol': {
 | 
			
		||||
                'qty': decimal.Decimal(1.0),
 | 
			
		||||
                'unit': 'instance'}}],
 | 
			
		||||
        'image.size': [{
 | 
			
		||||
            'desc': IMAGE_METADATA,
 | 
			
		||||
            'vol': {
 | 
			
		||||
                'qty': decimal.Decimal(1.0),
 | 
			
		||||
                'unit': 'image'}}]
 | 
			
		||||
    }}, {
 | 
			
		||||
    'period': SECOND_PERIOD,
 | 
			
		||||
    'usage': {
 | 
			
		||||
        'instance': [{
 | 
			
		||||
            'desc': COMPUTE_METADATA,
 | 
			
		||||
            'vol': {
 | 
			
		||||
                'qty': decimal.Decimal(1.0),
 | 
			
		||||
                'unit': 'instance'}}]
 | 
			
		||||
    },
 | 
			
		||||
}]
 | 
			
		||||
 | 
			
		||||
RATED_DATA = copy.deepcopy(COLLECTED_DATA)
 | 
			
		||||
RATED_DATA[0]['usage']['instance'][0]['rating'] = {
 | 
			
		||||
    'price': decimal.Decimal('0.42')}
 | 
			
		||||
RATED_DATA[0]['usage']['image.size'][0]['rating'] = {
 | 
			
		||||
    'price': decimal.Decimal('0.1337')}
 | 
			
		||||
RATED_DATA[1]['usage']['instance'][0]['rating'] = {
 | 
			
		||||
    'price': decimal.Decimal('0.42')}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DEFAULT_METRICS_CONF = {
 | 
			
		||||
    "metrics": {
 | 
			
		||||
@@ -221,33 +220,6 @@ DEFAULT_METRICS_CONF = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def split_storage_data(raw_data):
 | 
			
		||||
    final_data = []
 | 
			
		||||
    for frame in raw_data:
 | 
			
		||||
        frame['period']['begin'] = ck_utils.dt2iso(frame['period']['begin'])
 | 
			
		||||
        frame['period']['end'] = ck_utils.dt2iso(frame['period']['end'])
 | 
			
		||||
        usage_buffer = frame.pop('usage')
 | 
			
		||||
        # Sort to have a consistent result as we are converting it to a list
 | 
			
		||||
        for service, data in sorted(usage_buffer.items()):
 | 
			
		||||
            new_frame = copy.deepcopy(frame)
 | 
			
		||||
            new_frame['usage'] = {service: data}
 | 
			
		||||
            new_frame['usage'][service][0]['tenant_id'] = TENANT
 | 
			
		||||
            final_data.append(new_frame)
 | 
			
		||||
    return final_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# FIXME(sheeprine): storage is not using decimal for rates, we need to
 | 
			
		||||
# transition to decimal.
 | 
			
		||||
STORED_DATA = copy.deepcopy(COLLECTED_DATA)
 | 
			
		||||
STORED_DATA[0]['usage']['instance'][0]['rating'] = {
 | 
			
		||||
    'price': 0.42}
 | 
			
		||||
STORED_DATA[0]['usage']['image.size'][0]['rating'] = {
 | 
			
		||||
    'price': 0.1337}
 | 
			
		||||
STORED_DATA[1]['usage']['instance'][0]['rating'] = {
 | 
			
		||||
    'price': 0.42}
 | 
			
		||||
 | 
			
		||||
STORED_DATA = split_storage_data(STORED_DATA)
 | 
			
		||||
 | 
			
		||||
METRICS_CONF = DEFAULT_METRICS_CONF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -306,7 +278,7 @@ V2_STORAGE_SAMPLE = {
 | 
			
		||||
        },
 | 
			
		||||
        "groupby": {
 | 
			
		||||
            "id": uuidutils.generate_uuid(),
 | 
			
		||||
            "project_id": COMPUTE_METADATA['project_id'],
 | 
			
		||||
            "project_id": COMPUTE_GROUPBY['project_id'],
 | 
			
		||||
        },
 | 
			
		||||
        "metadata": {
 | 
			
		||||
            "flavor": "m1.nano",
 | 
			
		||||
@@ -323,7 +295,7 @@ V2_STORAGE_SAMPLE = {
 | 
			
		||||
        },
 | 
			
		||||
        "groupby": {
 | 
			
		||||
            "id": uuidutils.generate_uuid(),
 | 
			
		||||
            "project_id": COMPUTE_METADATA['project_id'],
 | 
			
		||||
            "project_id": COMPUTE_GROUPBY['project_id'],
 | 
			
		||||
        },
 | 
			
		||||
        "metadata": {
 | 
			
		||||
            "disk_format": "qcow2",
 | 
			
		||||
@@ -339,7 +311,7 @@ V2_STORAGE_SAMPLE = {
 | 
			
		||||
        },
 | 
			
		||||
        "groupby": {
 | 
			
		||||
            "id": uuidutils.generate_uuid(),
 | 
			
		||||
            "project_id": COMPUTE_METADATA['project_id'],
 | 
			
		||||
            "project_id": COMPUTE_GROUPBY['project_id'],
 | 
			
		||||
        },
 | 
			
		||||
        "metadata": {
 | 
			
		||||
            "volume_type": "ceph-region1"
 | 
			
		||||
@@ -355,7 +327,7 @@ V2_STORAGE_SAMPLE = {
 | 
			
		||||
        },
 | 
			
		||||
        "groupby": {
 | 
			
		||||
            "id": uuidutils.generate_uuid(),
 | 
			
		||||
            "project_id": COMPUTE_METADATA['project_id'],
 | 
			
		||||
            "project_id": COMPUTE_GROUPBY['project_id'],
 | 
			
		||||
        },
 | 
			
		||||
        "metadata": {
 | 
			
		||||
            "instance_id": uuidutils.generate_uuid(),
 | 
			
		||||
@@ -371,7 +343,7 @@ V2_STORAGE_SAMPLE = {
 | 
			
		||||
        },
 | 
			
		||||
        "groupby": {
 | 
			
		||||
            "id": uuidutils.generate_uuid(),
 | 
			
		||||
            "project_id": COMPUTE_METADATA['project_id'],
 | 
			
		||||
            "project_id": COMPUTE_GROUPBY['project_id'],
 | 
			
		||||
        },
 | 
			
		||||
        "metadata": {
 | 
			
		||||
            "instance_id": uuidutils.generate_uuid(),
 | 
			
		||||
@@ -387,7 +359,7 @@ V2_STORAGE_SAMPLE = {
 | 
			
		||||
        },
 | 
			
		||||
        "groupby": {
 | 
			
		||||
            "id": uuidutils.generate_uuid(),
 | 
			
		||||
            "project_id": COMPUTE_METADATA['project_id'],
 | 
			
		||||
            "project_id": COMPUTE_GROUPBY['project_id'],
 | 
			
		||||
        },
 | 
			
		||||
        "metadata": {
 | 
			
		||||
            "state": "attached",
 | 
			
		||||
@@ -403,7 +375,7 @@ V2_STORAGE_SAMPLE = {
 | 
			
		||||
        },
 | 
			
		||||
        "groupby": {
 | 
			
		||||
            "id": uuidutils.generate_uuid(),
 | 
			
		||||
            "project_id": COMPUTE_METADATA['project_id'],
 | 
			
		||||
            "project_id": COMPUTE_GROUPBY['project_id'],
 | 
			
		||||
        },
 | 
			
		||||
        "metadata": {
 | 
			
		||||
            "object_id": uuidutils.generate_uuid(),
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import unittest
 | 
			
		||||
 | 
			
		||||
import mock
 | 
			
		||||
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty.storage.v2 import influx
 | 
			
		||||
from cloudkitty.tests import TestCase
 | 
			
		||||
 | 
			
		||||
@@ -40,24 +41,28 @@ class TestInfluxDBStorage(TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_point_to_dataframe_entry_valid_point(self):
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            influx.InfluxStorage._point_to_dataframe_entry(self.point), {
 | 
			
		||||
                'vol': {'unit': 'banana', 'qty': 42},
 | 
			
		||||
                'rating': {'price': 1.0},
 | 
			
		||||
                'groupby': {'one': '1', 'two': '2'},
 | 
			
		||||
                'metadata': {'1': 'one', '2': 'two'},
 | 
			
		||||
            }
 | 
			
		||||
            influx.InfluxStorage._point_to_dataframe_entry(self.point),
 | 
			
		||||
            dataframe.DataPoint(
 | 
			
		||||
                'banana',
 | 
			
		||||
                42,
 | 
			
		||||
                1,
 | 
			
		||||
                {'one': '1', 'two': '2'},
 | 
			
		||||
                {'1': 'one', '2': 'two'},
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_point_to_dataframe_entry_invalid_groupby_metadata(self):
 | 
			
		||||
        self.point['groupby'] = 'a'
 | 
			
		||||
        self.point['metadata'] = None
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            influx.InfluxStorage._point_to_dataframe_entry(self.point), {
 | 
			
		||||
                'vol': {'unit': 'banana', 'qty': 42},
 | 
			
		||||
                'rating': {'price': 1.0},
 | 
			
		||||
                'groupby': {'a': ''},
 | 
			
		||||
                'metadata': {}
 | 
			
		||||
            }
 | 
			
		||||
            influx.InfluxStorage._point_to_dataframe_entry(self.point),
 | 
			
		||||
            dataframe.DataPoint(
 | 
			
		||||
                'banana',
 | 
			
		||||
                42,
 | 
			
		||||
                1,
 | 
			
		||||
                {'a': ''},
 | 
			
		||||
                {},
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -70,15 +70,15 @@ class StorageUnitTest(TestCase):
 | 
			
		||||
        total = 0
 | 
			
		||||
        qty = 0
 | 
			
		||||
        length = 0
 | 
			
		||||
        for data_part in data:
 | 
			
		||||
            for mtype, usage_part in data_part['usage'].items():
 | 
			
		||||
        for dataframe in data:
 | 
			
		||||
            for mtype, points in dataframe.itertypes():
 | 
			
		||||
                if types is not None and mtype not in types:
 | 
			
		||||
                    continue
 | 
			
		||||
                for item in usage_part:
 | 
			
		||||
                for point in points:
 | 
			
		||||
                    if project_id is None or \
 | 
			
		||||
                       project_id == item['groupby']['project_id']:
 | 
			
		||||
                        total += item['rating']['price']
 | 
			
		||||
                        qty += item['vol']['qty']
 | 
			
		||||
                       project_id == point.groupby['project_id']:
 | 
			
		||||
                        total += point.price
 | 
			
		||||
                        qty += point.qty
 | 
			
		||||
                        length += 1
 | 
			
		||||
 | 
			
		||||
        return round(float(total), 5), round(float(qty), 5), length
 | 
			
		||||
@@ -274,10 +274,8 @@ class StorageUnitTest(TestCase):
 | 
			
		||||
        frames = self.storage.retrieve(begin=begin, end=end)
 | 
			
		||||
        self.assertEqual(frames['total'], expected_length)
 | 
			
		||||
 | 
			
		||||
        retrieved_length = 0
 | 
			
		||||
        for data_part in frames['dataframes']:
 | 
			
		||||
            for usage_part in data_part['usage'].values():
 | 
			
		||||
                retrieved_length += len(usage_part)
 | 
			
		||||
        retrieved_length = sum(len(list(frame.iterpoints()))
 | 
			
		||||
                               for frame in frames['dataframes'])
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(expected_length, retrieved_length)
 | 
			
		||||
 | 
			
		||||
@@ -292,10 +290,8 @@ class StorageUnitTest(TestCase):
 | 
			
		||||
                                       metric_types=['image.size'])
 | 
			
		||||
        self.assertEqual(frames['total'], expected_length)
 | 
			
		||||
 | 
			
		||||
        retrieved_length = 0
 | 
			
		||||
        for data_part in frames['dataframes']:
 | 
			
		||||
            for usage_part in data_part['usage'].values():
 | 
			
		||||
                retrieved_length += len(usage_part)
 | 
			
		||||
        retrieved_length = sum(len(list(frame.iterpoints()))
 | 
			
		||||
                               for frame in frames['dataframes'])
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(expected_length, retrieved_length)
 | 
			
		||||
 | 
			
		||||
@@ -313,10 +309,8 @@ class StorageUnitTest(TestCase):
 | 
			
		||||
                                       metric_types=['image.size', 'instance'])
 | 
			
		||||
        self.assertEqual(frames['total'], expected_length)
 | 
			
		||||
 | 
			
		||||
        retrieved_length = 0
 | 
			
		||||
        for data_part in frames['dataframes']:
 | 
			
		||||
            for usage_part in data_part['usage'].values():
 | 
			
		||||
                retrieved_length += len(usage_part)
 | 
			
		||||
        retrieved_length = sum(len(list(frame.iterpoints()))
 | 
			
		||||
                               for frame in frames['dataframes'])
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(expected_length, retrieved_length)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										274
									
								
								cloudkitty/tests/test_dataframe.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								cloudkitty/tests/test_dataframe.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,274 @@
 | 
			
		||||
# Copyright 2019 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.
 | 
			
		||||
#
 | 
			
		||||
import copy
 | 
			
		||||
import datetime
 | 
			
		||||
import decimal
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from dateutil import tz
 | 
			
		||||
from werkzeug import datastructures
 | 
			
		||||
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty import json_utils as json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestDataPoint(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    default_params = {
 | 
			
		||||
        'qty': 0,
 | 
			
		||||
        'price': 0,
 | 
			
		||||
        'unit': None,
 | 
			
		||||
        'groupby': {},
 | 
			
		||||
        'metadata': {},
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def test_create_empty_datapoint(self):
 | 
			
		||||
        point = dataframe.DataPoint(**self.default_params)
 | 
			
		||||
        self.assertEqual(point.qty, decimal.Decimal(0))
 | 
			
		||||
        self.assertEqual(point.price, decimal.Decimal(0))
 | 
			
		||||
        self.assertEqual(point.unit, "undefined")
 | 
			
		||||
        self.assertEqual(point.groupby, {})
 | 
			
		||||
 | 
			
		||||
    def test_readonly_attrs(self):
 | 
			
		||||
        point = dataframe.DataPoint(**self.default_params)
 | 
			
		||||
        for attr in ("qty", "price", "unit"):
 | 
			
		||||
            self.assertRaises(AttributeError, setattr, point, attr, 'x')
 | 
			
		||||
 | 
			
		||||
    def test_properties(self):
 | 
			
		||||
        params = copy.deepcopy(self.default_params)
 | 
			
		||||
        groupby = {"group_one": "one", "group_two": "two"}
 | 
			
		||||
        metadata = {"meta_one": "one", "meta_two": "two"}
 | 
			
		||||
        params.update({'groupby': groupby, 'metadata': metadata})
 | 
			
		||||
        point = dataframe.DataPoint(**params)
 | 
			
		||||
        self.assertEqual(point.groupby, groupby)
 | 
			
		||||
        self.assertEqual(point.metadata, metadata)
 | 
			
		||||
 | 
			
		||||
    def test_as_dict_mutable_standard(self):
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            dataframe.DataPoint(
 | 
			
		||||
                **self.default_params).as_dict(mutable=True),
 | 
			
		||||
            {
 | 
			
		||||
                "vol": {"unit": "undefined", "qty": decimal.Decimal(0)},
 | 
			
		||||
                "rating": {"price": decimal.Decimal(0)},
 | 
			
		||||
                "groupby": {},
 | 
			
		||||
                "metadata": {},
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_as_dict_mutable_legacy(self):
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            dataframe.DataPoint(**self.default_params).as_dict(
 | 
			
		||||
                legacy=True, mutable=True),
 | 
			
		||||
            {
 | 
			
		||||
                "vol": {"unit": "undefined", "qty": decimal.Decimal(0)},
 | 
			
		||||
                "rating": {"price": decimal.Decimal(0)},
 | 
			
		||||
                "desc": {},
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_as_dict_immutable(self):
 | 
			
		||||
        point_dict = dataframe.DataPoint(**self.default_params).as_dict()
 | 
			
		||||
        self.assertIsInstance(point_dict, datastructures.ImmutableDict)
 | 
			
		||||
        self.assertEqual(dict(point_dict), {
 | 
			
		||||
            "vol": {"unit": "undefined", "qty": decimal.Decimal(0)},
 | 
			
		||||
            "rating": {"price": decimal.Decimal(0)},
 | 
			
		||||
            "groupby": {},
 | 
			
		||||
            "metadata": {},
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def test_json_standard(self):
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            json.loads(dataframe.DataPoint(**self.default_params).json()), {
 | 
			
		||||
                "vol": {"unit": "undefined", "qty": decimal.Decimal(0)},
 | 
			
		||||
                "rating": {"price": decimal.Decimal(0)},
 | 
			
		||||
                "groupby": {},
 | 
			
		||||
                "metadata": {},
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_json_legacy(self):
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            json.loads(dataframe.DataPoint(
 | 
			
		||||
                **self.default_params).json(legacy=True)),
 | 
			
		||||
            {
 | 
			
		||||
                "vol": {"unit": "undefined", "qty": decimal.Decimal(0)},
 | 
			
		||||
                "rating": {"price": decimal.Decimal(0)},
 | 
			
		||||
                "desc": {},
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_from_dict_valid_dict(self):
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            dataframe.DataPoint(
 | 
			
		||||
                unit="amazing_unit",
 | 
			
		||||
                qty=3,
 | 
			
		||||
                price=0,
 | 
			
		||||
                groupby={"g_one": "one", "g_two": "two"},
 | 
			
		||||
                metadata={"m_one": "one", "m_two": "two"},
 | 
			
		||||
            ).as_dict(),
 | 
			
		||||
            dataframe.DataPoint.from_dict({
 | 
			
		||||
                "vol": {"unit": "amazing_unit", "qty": 3},
 | 
			
		||||
                "groupby": {"g_one": "one", "g_two": "two"},
 | 
			
		||||
                "metadata": {"m_one": "one", "m_two": "two"},
 | 
			
		||||
            }).as_dict(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_from_dict_invalid(self):
 | 
			
		||||
        invalid = {
 | 
			
		||||
            "vol": {},
 | 
			
		||||
            "desc": {"a": "b"},
 | 
			
		||||
        }
 | 
			
		||||
        self.assertRaises(ValueError, dataframe.DataPoint.from_dict, invalid)
 | 
			
		||||
 | 
			
		||||
    def test_set_price(self):
 | 
			
		||||
        point = dataframe.DataPoint(**self.default_params)
 | 
			
		||||
        self.assertEqual(point.price, decimal.Decimal(0))
 | 
			
		||||
        self.assertEqual(point.set_price(42).price, decimal.Decimal(42))
 | 
			
		||||
        self.assertEqual(point.set_price(1337).price, decimal.Decimal(1337))
 | 
			
		||||
 | 
			
		||||
    def test_desc(self):
 | 
			
		||||
        params = copy.deepcopy(self.default_params)
 | 
			
		||||
        params['groupby'] = {'group_one': 'one', 'group_two': 'two'}
 | 
			
		||||
        params['metadata'] = {'meta_one': 'one', 'meta_two': 'two'}
 | 
			
		||||
        point = dataframe.DataPoint(**params)
 | 
			
		||||
        self.assertEqual(point.desc, {
 | 
			
		||||
            'group_one': 'one',
 | 
			
		||||
            'group_two': 'two',
 | 
			
		||||
            'meta_one': 'one',
 | 
			
		||||
            'meta_two': 'two',
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestDataFrame(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_dataframe_add_points(self):
 | 
			
		||||
        start = datetime.datetime(2019, 3, 4, 1, tzinfo=tz.UTC)
 | 
			
		||||
        end = datetime.datetime(2019, 3, 4, 2, tzinfo=tz.UTC)
 | 
			
		||||
        df = dataframe.DataFrame(start=start, end=end)
 | 
			
		||||
        a_points = [dataframe.DataPoint(**TestDataPoint.default_params)
 | 
			
		||||
                    for _ in range(2)]
 | 
			
		||||
        b_points = [dataframe.DataPoint(**TestDataPoint.default_params)
 | 
			
		||||
                    for _ in range(4)]
 | 
			
		||||
 | 
			
		||||
        df.add_point(a_points[0], 'service_a')
 | 
			
		||||
        df.add_points(a_points[1:], 'service_a')
 | 
			
		||||
        df.add_points(b_points[:2], 'service_b')
 | 
			
		||||
        df.add_points(b_points[2:3], 'service_b')
 | 
			
		||||
        df.add_point(b_points[3], 'service_b')
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(dict(df.as_dict()), {
 | 
			
		||||
            'period': {'begin': start, 'end': end},
 | 
			
		||||
            'usage': {
 | 
			
		||||
                'service_a': [
 | 
			
		||||
                    dataframe.DataPoint(
 | 
			
		||||
                        **TestDataPoint.default_params).as_dict()
 | 
			
		||||
                    for _ in range(2)],
 | 
			
		||||
                'service_b': [
 | 
			
		||||
                    dataframe.DataPoint(
 | 
			
		||||
                        **TestDataPoint.default_params).as_dict()
 | 
			
		||||
                    for _ in range(4)],
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def test_properties(self):
 | 
			
		||||
        start = datetime.datetime(2019, 6, 1, tzinfo=tz.UTC)
 | 
			
		||||
        end = datetime.datetime(2019, 6, 1, 1, tzinfo=tz.UTC)
 | 
			
		||||
        df = dataframe.DataFrame(start=start, end=end)
 | 
			
		||||
        self.assertEqual(df.start, start)
 | 
			
		||||
        self.assertEqual(df.end, end)
 | 
			
		||||
 | 
			
		||||
    def test_json(self):
 | 
			
		||||
        start = datetime.datetime(2019, 3, 4, 1, tzinfo=tz.UTC)
 | 
			
		||||
        end = datetime.datetime(2019, 3, 4, 2, tzinfo=tz.UTC)
 | 
			
		||||
        df = dataframe.DataFrame(start=start, end=end)
 | 
			
		||||
        a_points = [dataframe.DataPoint(**TestDataPoint.default_params)
 | 
			
		||||
                    for _ in range(2)]
 | 
			
		||||
        b_points = [dataframe.DataPoint(**TestDataPoint.default_params)
 | 
			
		||||
                    for _ in range(4)]
 | 
			
		||||
        df.add_points(a_points, 'service_a')
 | 
			
		||||
        df.add_points(b_points, 'service_b')
 | 
			
		||||
 | 
			
		||||
        self.maxDiff = None
 | 
			
		||||
        self.assertEqual(json.loads(df.json()), json.loads(json.dumps({
 | 
			
		||||
            'period': {'begin': start.isoformat(),
 | 
			
		||||
                       'end': end.isoformat()},
 | 
			
		||||
            'usage': {
 | 
			
		||||
                'service_a': [
 | 
			
		||||
                    dataframe.DataPoint(
 | 
			
		||||
                        **TestDataPoint.default_params).as_dict()
 | 
			
		||||
                    for _ in range(2)],
 | 
			
		||||
                'service_b': [
 | 
			
		||||
                    dataframe.DataPoint(
 | 
			
		||||
                        **TestDataPoint.default_params).as_dict()
 | 
			
		||||
                    for _ in range(4)],
 | 
			
		||||
            }
 | 
			
		||||
        })))
 | 
			
		||||
 | 
			
		||||
    def test_from_dict_valid_dict(self):
 | 
			
		||||
        start = datetime.datetime(2019, 1, 2, 12, tzinfo=tz.UTC)
 | 
			
		||||
        end = datetime.datetime(2019, 1, 2, 13, tzinfo=tz.UTC)
 | 
			
		||||
        point = dataframe.DataPoint(
 | 
			
		||||
            'unit', 0, 0, {'g_one': 'one'}, {'m_two': 'two'})
 | 
			
		||||
        usage = {'metric_x': [point]}
 | 
			
		||||
        dict_usage = {'metric_x': [point.as_dict(mutable=True)]}
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            dataframe.DataFrame(start, end, usage).as_dict(),
 | 
			
		||||
            dataframe.DataFrame.from_dict({
 | 
			
		||||
                'period': {'begin': start, 'end': end},
 | 
			
		||||
                'usage': dict_usage,
 | 
			
		||||
            }).as_dict(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_from_dict_valid_dict_date_as_str(self):
 | 
			
		||||
        start = datetime.datetime(2019, 1, 2, 12, tzinfo=tz.UTC)
 | 
			
		||||
        end = datetime.datetime(2019, 1, 2, 13, tzinfo=tz.UTC)
 | 
			
		||||
        point = dataframe.DataPoint(
 | 
			
		||||
            'unit', 0, 0, {'g_one': 'one'}, {'m_two': 'two'})
 | 
			
		||||
        usage = {'metric_x': [point]}
 | 
			
		||||
        dict_usage = {'metric_x': [point.as_dict(mutable=True)]}
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            dataframe.DataFrame(start, end, usage).as_dict(),
 | 
			
		||||
            dataframe.DataFrame.from_dict({
 | 
			
		||||
                'period': {'begin': start.isoformat(), 'end': end.isoformat()},
 | 
			
		||||
                'usage': dict_usage,
 | 
			
		||||
            }).as_dict(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_from_dict_invalid_dict(self):
 | 
			
		||||
        self.assertRaises(
 | 
			
		||||
            ValueError, dataframe.DataFrame.from_dict, {'usage': None})
 | 
			
		||||
 | 
			
		||||
    def test_repr(self):
 | 
			
		||||
        start = datetime.datetime(2019, 3, 4, 1, tzinfo=tz.UTC)
 | 
			
		||||
        end = datetime.datetime(2019, 3, 4, 2, tzinfo=tz.UTC)
 | 
			
		||||
        df = dataframe.DataFrame(start=start, end=end)
 | 
			
		||||
        points = [dataframe.DataPoint(**TestDataPoint.default_params)
 | 
			
		||||
                  for _ in range(4)]
 | 
			
		||||
        df.add_points(points, 'metric_x')
 | 
			
		||||
        self.assertEqual(str(df), "DataFrame(metrics=[metric_x])")
 | 
			
		||||
        df.add_points(points, 'metric_y')
 | 
			
		||||
        self.assertEqual(str(df), "DataFrame(metrics=[metric_x,metric_y])")
 | 
			
		||||
 | 
			
		||||
    def test_iterpoints(self):
 | 
			
		||||
        start = datetime.datetime(2019, 3, 4, 1, tzinfo=tz.UTC)
 | 
			
		||||
        end = datetime.datetime(2019, 3, 4, 2, tzinfo=tz.UTC)
 | 
			
		||||
        df = dataframe.DataFrame(start=start, end=end)
 | 
			
		||||
        points = [dataframe.DataPoint(**TestDataPoint.default_params)
 | 
			
		||||
                  for _ in range(4)]
 | 
			
		||||
        df.add_points(points, 'metric_x')
 | 
			
		||||
        expected = [
 | 
			
		||||
            ('metric_x', dataframe.DataPoint(**TestDataPoint.default_params))
 | 
			
		||||
            for _ in range(4)]
 | 
			
		||||
        self.assertEqual(list(df.iterpoints()), expected)
 | 
			
		||||
@@ -14,11 +14,13 @@
 | 
			
		||||
#    under the License.
 | 
			
		||||
#
 | 
			
		||||
import copy
 | 
			
		||||
import datetime
 | 
			
		||||
import decimal
 | 
			
		||||
 | 
			
		||||
import mock
 | 
			
		||||
from oslo_utils import uuidutils
 | 
			
		||||
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty.rating import hash
 | 
			
		||||
from cloudkitty.rating.hash.db import api
 | 
			
		||||
from cloudkitty import tests
 | 
			
		||||
@@ -26,10 +28,10 @@ from cloudkitty import tests
 | 
			
		||||
 | 
			
		||||
TEST_TS = 1388577600
 | 
			
		||||
FAKE_UUID = '6c1b8a30-797f-4b7e-ad66-9879b79059fb'
 | 
			
		||||
CK_RESOURCES_DATA = [{
 | 
			
		||||
CK_RESOURCES_DATA = [dataframe.DataFrame.from_dict({
 | 
			
		||||
    "period": {
 | 
			
		||||
        "begin": "2014-10-01T00:00:00",
 | 
			
		||||
        "end": "2014-10-01T01:00:00"},
 | 
			
		||||
        "begin": datetime.datetime(2014, 10, 1),
 | 
			
		||||
        "end": datetime.datetime(2014, 10, 1, 1)},
 | 
			
		||||
    "usage": {
 | 
			
		||||
        "compute": [
 | 
			
		||||
            {
 | 
			
		||||
@@ -94,7 +96,7 @@ CK_RESOURCES_DATA = [{
 | 
			
		||||
                    "vcpus": "1"},
 | 
			
		||||
                "vol": {
 | 
			
		||||
                    "qty": 1,
 | 
			
		||||
                    "unit": "instance"}}]}}]
 | 
			
		||||
                    "unit": "instance"}}]}}, legacy=True)]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HashMapRatingTest(tests.TestCase):
 | 
			
		||||
@@ -859,21 +861,24 @@ class HashMapRatingTest(tests.TestCase):
 | 
			
		||||
            map_type='flat',
 | 
			
		||||
            service_id=service_db.service_id)
 | 
			
		||||
        self._hash.reload_config()
 | 
			
		||||
        actual_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        expected_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        for cur_data in actual_data:
 | 
			
		||||
            cur_usage = cur_data['usage']
 | 
			
		||||
            for service_name, service_data in cur_usage.items():
 | 
			
		||||
                for item in service_data:
 | 
			
		||||
                    self._hash._res = {}
 | 
			
		||||
                    self._hash.process_services(service_name, item)
 | 
			
		||||
                    self._hash.add_rating_informations(item)
 | 
			
		||||
        compute_list = expected_data[0]['usage']['compute']
 | 
			
		||||
        actual_data = dataframe.DataFrame(start=expected_data[0].start,
 | 
			
		||||
                                          end=expected_data[0].end)
 | 
			
		||||
        for cur_data in expected_data:
 | 
			
		||||
            for service_name, point in cur_data.iterpoints():
 | 
			
		||||
                self._hash._res = {}
 | 
			
		||||
                self._hash.process_services(service_name, point)
 | 
			
		||||
                actual_data.add_point(
 | 
			
		||||
                    self._hash.add_rating_informations(point), service_name)
 | 
			
		||||
        actual_data = [actual_data]
 | 
			
		||||
        df_dicts = [d.as_dict(mutable=True) for d in expected_data]
 | 
			
		||||
        compute_list = df_dicts[0]['usage']['compute']
 | 
			
		||||
        compute_list[0]['rating'] = {'price': decimal.Decimal('2.757')}
 | 
			
		||||
        compute_list[1]['rating'] = {'price': decimal.Decimal('5.514')}
 | 
			
		||||
        compute_list[2]['rating'] = {'price': decimal.Decimal('5.514')}
 | 
			
		||||
        compute_list[3]['rating'] = {'price': decimal.Decimal('2.757')}
 | 
			
		||||
        self.assertEqual(expected_data, actual_data)
 | 
			
		||||
        self.assertEqual(df_dicts, [d.as_dict(mutable=True)
 | 
			
		||||
                                    for d in actual_data])
 | 
			
		||||
 | 
			
		||||
    def test_process_fields(self):
 | 
			
		||||
        service_db = self._db_api.create_service('compute')
 | 
			
		||||
@@ -900,21 +905,24 @@ class HashMapRatingTest(tests.TestCase):
 | 
			
		||||
            map_type='flat',
 | 
			
		||||
            field_id=flavor_field.field_id)
 | 
			
		||||
        self._hash.reload_config()
 | 
			
		||||
        actual_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        expected_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        for cur_data in actual_data:
 | 
			
		||||
            cur_usage = cur_data['usage']
 | 
			
		||||
            for service_name, service_data in cur_usage.items():
 | 
			
		||||
                for item in service_data:
 | 
			
		||||
                    self._hash._res = {}
 | 
			
		||||
                    self._hash.process_fields(service_name, item)
 | 
			
		||||
                    self._hash.add_rating_informations(item)
 | 
			
		||||
        compute_list = expected_data[0]['usage']['compute']
 | 
			
		||||
        actual_data = dataframe.DataFrame(start=expected_data[0].start,
 | 
			
		||||
                                          end=expected_data[0].end)
 | 
			
		||||
        for cur_data in expected_data:
 | 
			
		||||
            for service_name, point in cur_data.iterpoints():
 | 
			
		||||
                self._hash._res = {}
 | 
			
		||||
                self._hash.process_fields(service_name, point)
 | 
			
		||||
                actual_data.add_point(
 | 
			
		||||
                    self._hash.add_rating_informations(point), service_name)
 | 
			
		||||
        actual_data = [actual_data]
 | 
			
		||||
        df_dicts = [d.as_dict(mutable=True) for d in expected_data]
 | 
			
		||||
        compute_list = df_dicts[0]['usage']['compute']
 | 
			
		||||
        compute_list[0]['rating'] = {'price': decimal.Decimal('1.337')}
 | 
			
		||||
        compute_list[1]['rating'] = {'price': decimal.Decimal('2.84')}
 | 
			
		||||
        compute_list[2]['rating'] = {'price': decimal.Decimal('0')}
 | 
			
		||||
        compute_list[3]['rating'] = {'price': decimal.Decimal('1.47070')}
 | 
			
		||||
        self.assertEqual(expected_data, actual_data)
 | 
			
		||||
        self.assertEqual(df_dicts, [d.as_dict(mutable=True)
 | 
			
		||||
                                    for d in actual_data])
 | 
			
		||||
 | 
			
		||||
    def test_process_fields_no_match(self):
 | 
			
		||||
        service_db = self._db_api.create_service('compute')
 | 
			
		||||
@@ -926,18 +934,19 @@ class HashMapRatingTest(tests.TestCase):
 | 
			
		||||
            map_type='flat',
 | 
			
		||||
            field_id=flavor_field.field_id)
 | 
			
		||||
        self._hash.reload_config()
 | 
			
		||||
        actual_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        expected_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        for cur_data in actual_data:
 | 
			
		||||
            cur_usage = cur_data['usage']
 | 
			
		||||
            for service_name, service_data in cur_usage.items():
 | 
			
		||||
                for item in service_data:
 | 
			
		||||
                    self._hash._res = {}
 | 
			
		||||
                    self._hash.process_fields(service_name, item)
 | 
			
		||||
                    self._hash.add_rating_informations(item)
 | 
			
		||||
        for elem in expected_data[0]['usage']['compute']:
 | 
			
		||||
            elem['rating'] = {'price': decimal.Decimal('0')}
 | 
			
		||||
        self.assertEqual(expected_data, actual_data)
 | 
			
		||||
        actual_data = dataframe.DataFrame(start=expected_data[0].start,
 | 
			
		||||
                                          end=expected_data[0].end)
 | 
			
		||||
        for cur_data in expected_data:
 | 
			
		||||
            for service_name, point in cur_data.iterpoints():
 | 
			
		||||
                self._hash._res = {}
 | 
			
		||||
                self._hash.process_fields(service_name, point)
 | 
			
		||||
                actual_data.add_point(
 | 
			
		||||
                    self._hash.add_rating_informations(point), service_name)
 | 
			
		||||
        actual_data = [actual_data]
 | 
			
		||||
        df_dicts = [d.as_dict(mutable=True) for d in expected_data]
 | 
			
		||||
        self.assertEqual(df_dicts, [d.as_dict(mutable=True)
 | 
			
		||||
                                    for d in actual_data])
 | 
			
		||||
 | 
			
		||||
    def test_process_field_threshold(self):
 | 
			
		||||
        service_db = self._db_api.create_service('compute')
 | 
			
		||||
@@ -954,21 +963,24 @@ class HashMapRatingTest(tests.TestCase):
 | 
			
		||||
            map_type='flat',
 | 
			
		||||
            field_id=field_db.field_id)
 | 
			
		||||
        self._hash.reload_config()
 | 
			
		||||
        actual_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        expected_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        for cur_data in actual_data:
 | 
			
		||||
            cur_usage = cur_data['usage']
 | 
			
		||||
            for service_name, service_data in cur_usage.items():
 | 
			
		||||
                for item in service_data:
 | 
			
		||||
                    self._hash._res = {}
 | 
			
		||||
                    self._hash.process_fields(service_name, item)
 | 
			
		||||
                    self._hash.add_rating_informations(item)
 | 
			
		||||
        compute_list = expected_data[0]['usage']['compute']
 | 
			
		||||
        actual_data = dataframe.DataFrame(start=expected_data[0].start,
 | 
			
		||||
                                          end=expected_data[0].end)
 | 
			
		||||
        for cur_data in expected_data:
 | 
			
		||||
            for service_name, point in cur_data.iterpoints():
 | 
			
		||||
                self._hash._res = {}
 | 
			
		||||
                self._hash.process_fields(service_name, point)
 | 
			
		||||
                actual_data.add_point(
 | 
			
		||||
                    self._hash.add_rating_informations(point), service_name)
 | 
			
		||||
        actual_data = [actual_data]
 | 
			
		||||
        df_dicts = [d.as_dict(mutable=True) for d in expected_data]
 | 
			
		||||
        compute_list = df_dicts[0]['usage']['compute']
 | 
			
		||||
        compute_list[0]['rating'] = {'price': decimal.Decimal('0.1337')}
 | 
			
		||||
        compute_list[1]['rating'] = {'price': decimal.Decimal('0.4')}
 | 
			
		||||
        compute_list[2]['rating'] = {'price': decimal.Decimal('0.4')}
 | 
			
		||||
        compute_list[3]['rating'] = {'price': decimal.Decimal('0.1337')}
 | 
			
		||||
        self.assertEqual(expected_data, actual_data)
 | 
			
		||||
        self.assertEqual(df_dicts, [d.as_dict(mutable=True)
 | 
			
		||||
                                    for d in actual_data])
 | 
			
		||||
 | 
			
		||||
    def test_process_field_threshold_no_match(self):
 | 
			
		||||
        service_db = self._db_api.create_service('compute')
 | 
			
		||||
@@ -980,18 +992,18 @@ class HashMapRatingTest(tests.TestCase):
 | 
			
		||||
            map_type='flat',
 | 
			
		||||
            field_id=field_db.field_id)
 | 
			
		||||
        self._hash.reload_config()
 | 
			
		||||
        actual_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        expected_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        for cur_data in actual_data:
 | 
			
		||||
            cur_usage = cur_data['usage']
 | 
			
		||||
            for service_name, service_data in cur_usage.items():
 | 
			
		||||
                for item in service_data:
 | 
			
		||||
                    self._hash._res = {}
 | 
			
		||||
                    self._hash.process_fields(service_name, item)
 | 
			
		||||
                    self._hash.add_rating_informations(item)
 | 
			
		||||
        for elem in expected_data[0]['usage']['compute']:
 | 
			
		||||
            elem['rating'] = {'price': decimal.Decimal('0')}
 | 
			
		||||
        self.assertEqual(expected_data, actual_data)
 | 
			
		||||
        actual_data = dataframe.DataFrame(start=expected_data[0].start,
 | 
			
		||||
                                          end=expected_data[0].end)
 | 
			
		||||
        for cur_data in expected_data:
 | 
			
		||||
            for service_name, point in cur_data.iterpoints():
 | 
			
		||||
                self._hash._res = {}
 | 
			
		||||
                self._hash.process_fields(service_name, point)
 | 
			
		||||
                actual_data.add_point(
 | 
			
		||||
                    self._hash.add_rating_informations(point), service_name)
 | 
			
		||||
        actual_data = [actual_data]
 | 
			
		||||
        self.assertEqual([d.as_dict(mutable=True) for d in expected_data],
 | 
			
		||||
                         [d.as_dict(mutable=True) for d in actual_data])
 | 
			
		||||
 | 
			
		||||
    def test_process_service_threshold(self):
 | 
			
		||||
        service_db = self._db_api.create_service('compute')
 | 
			
		||||
@@ -1006,21 +1018,24 @@ class HashMapRatingTest(tests.TestCase):
 | 
			
		||||
            map_type='flat',
 | 
			
		||||
            service_id=service_db.service_id)
 | 
			
		||||
        self._hash.reload_config()
 | 
			
		||||
        actual_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        expected_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        for cur_data in actual_data:
 | 
			
		||||
            cur_usage = cur_data['usage']
 | 
			
		||||
            for service_name, service_data in cur_usage.items():
 | 
			
		||||
                for item in service_data:
 | 
			
		||||
                    self._hash._res = {}
 | 
			
		||||
                    self._hash.process_services(service_name, item)
 | 
			
		||||
                    self._hash.add_rating_informations(item)
 | 
			
		||||
        compute_list = expected_data[0]['usage']['compute']
 | 
			
		||||
        actual_data = dataframe.DataFrame(start=expected_data[0].start,
 | 
			
		||||
                                          end=expected_data[0].end)
 | 
			
		||||
        for cur_data in expected_data:
 | 
			
		||||
            for service_name, point in cur_data.iterpoints():
 | 
			
		||||
                self._hash._res = {}
 | 
			
		||||
                self._hash.process_services(service_name, point)
 | 
			
		||||
                actual_data.add_point(
 | 
			
		||||
                    self._hash.add_rating_informations(point), service_name)
 | 
			
		||||
        actual_data = [actual_data]
 | 
			
		||||
        df_dicts = [d.as_dict(mutable=True) for d in expected_data]
 | 
			
		||||
        compute_list = df_dicts[0]['usage']['compute']
 | 
			
		||||
        compute_list[0]['rating'] = {'price': decimal.Decimal('0.1')}
 | 
			
		||||
        compute_list[1]['rating'] = {'price': decimal.Decimal('0.15')}
 | 
			
		||||
        compute_list[2]['rating'] = {'price': decimal.Decimal('0.15')}
 | 
			
		||||
        compute_list[3]['rating'] = {'price': decimal.Decimal('0.1')}
 | 
			
		||||
        self.assertEqual(expected_data, actual_data)
 | 
			
		||||
        self.assertEqual(df_dicts, [d.as_dict(mutable=True)
 | 
			
		||||
                                    for d in actual_data])
 | 
			
		||||
 | 
			
		||||
    def test_update_result_flat(self):
 | 
			
		||||
        self._hash.update_result(
 | 
			
		||||
@@ -1155,13 +1170,16 @@ class HashMapRatingTest(tests.TestCase):
 | 
			
		||||
            field_id=memory_db.field_id,
 | 
			
		||||
            group_id=group_db.group_id)
 | 
			
		||||
        self._hash.reload_config()
 | 
			
		||||
        actual_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        expected_data = copy.deepcopy(CK_RESOURCES_DATA)
 | 
			
		||||
        compute_list = expected_data[0]['usage']['compute']
 | 
			
		||||
        actual_data = dataframe.DataFrame(start=expected_data[0].start,
 | 
			
		||||
                                          end=expected_data[0].end)
 | 
			
		||||
        df_dicts = [d.as_dict(mutable=True) for d in expected_data]
 | 
			
		||||
        compute_list = df_dicts[0]['usage']['compute']
 | 
			
		||||
        compute_list[0]['rating'] = {'price': decimal.Decimal('2.487')}
 | 
			
		||||
        compute_list[1]['rating'] = {'price': decimal.Decimal('5.564')}
 | 
			
		||||
        # 8vcpu mapping * 2 + service_mapping * 1 + 128m ram threshold * 2
 | 
			
		||||
        compute_list[2]['rating'] = {'price': decimal.Decimal('34.40')}
 | 
			
		||||
        compute_list[3]['rating'] = {'price': decimal.Decimal('2.6357')}
 | 
			
		||||
        self._hash.process(actual_data)
 | 
			
		||||
        self.assertEqual(expected_data, actual_data)
 | 
			
		||||
        actual_data = [self._hash.process(d) for d in expected_data]
 | 
			
		||||
        self.assertEqual(df_dicts, [d.as_dict(mutable=True)
 | 
			
		||||
                                    for d in actual_data])
 | 
			
		||||
 
 | 
			
		||||
@@ -182,31 +182,31 @@ class WorkerTest(tests.TestCase):
 | 
			
		||||
        self.worker._collect = mock.MagicMock()
 | 
			
		||||
 | 
			
		||||
    def test_do_collection_all_valid(self):
 | 
			
		||||
        side_effect = [
 | 
			
		||||
        metrics = ['metric{}'.format(i) for i in range(5)]
 | 
			
		||||
        side_effect = [(
 | 
			
		||||
            metrics[i],
 | 
			
		||||
            {'period': {'begin': 0,
 | 
			
		||||
                        'end': 3600},
 | 
			
		||||
             'usage': [i]}
 | 
			
		||||
            for i in range(5)
 | 
			
		||||
        ]
 | 
			
		||||
             'usage': i},
 | 
			
		||||
        ) for i in range(5)]
 | 
			
		||||
        self.worker._collect.side_effect = side_effect
 | 
			
		||||
        metrics = ['metric{}'.format(i) for i in range(5)]
 | 
			
		||||
        output = sorted(self.worker._do_collection(metrics, 0),
 | 
			
		||||
                        key=lambda x: x['usage'][0])
 | 
			
		||||
        output = sorted(self.worker._do_collection(metrics, 0).items(),
 | 
			
		||||
                        key=lambda x: x[1]['usage'])
 | 
			
		||||
        self.assertEqual(side_effect, output)
 | 
			
		||||
 | 
			
		||||
    def test_do_collection_some_empty(self):
 | 
			
		||||
        side_effect = [
 | 
			
		||||
        metrics = ['metric{}'.format(i) for i in range(7)]
 | 
			
		||||
        side_effect = [(
 | 
			
		||||
            metrics[i],
 | 
			
		||||
            {'period': {'begin': 0,
 | 
			
		||||
                        'end': 3600},
 | 
			
		||||
             'usage': [i]}
 | 
			
		||||
            for i in range(5)
 | 
			
		||||
        ]
 | 
			
		||||
             'usage': i},
 | 
			
		||||
        ) for i in range(5)]
 | 
			
		||||
        side_effect.insert(2, collector.NoDataCollected('a', 'b'))
 | 
			
		||||
        side_effect.insert(4, collector.NoDataCollected('a', 'b'))
 | 
			
		||||
        self.worker._collect.side_effect = side_effect
 | 
			
		||||
        metrics = ['metric{}'.format(i) for i in range(7)]
 | 
			
		||||
        output = sorted(self.worker._do_collection(metrics, 0),
 | 
			
		||||
                        key=lambda x: x['usage'][0])
 | 
			
		||||
        output = sorted(self.worker._do_collection(metrics, 0).items(),
 | 
			
		||||
                        key=lambda x: x[1]['usage'])
 | 
			
		||||
        self.assertEqual([
 | 
			
		||||
            i for i in side_effect
 | 
			
		||||
            if not isinstance(i, collector.NoDataCollected)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import random
 | 
			
		||||
 | 
			
		||||
from oslo_utils import uuidutils
 | 
			
		||||
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty.tests import samples
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -32,25 +33,25 @@ def generate_v2_storage_data(min_length=10,
 | 
			
		||||
    elif not isinstance(project_ids, list):
 | 
			
		||||
        project_ids = [project_ids]
 | 
			
		||||
 | 
			
		||||
    usage = {}
 | 
			
		||||
    df = dataframe.DataFrame(start=start, end=end)
 | 
			
		||||
    for metric_name, sample in samples.V2_STORAGE_SAMPLE.items():
 | 
			
		||||
        dataframes = []
 | 
			
		||||
        datapoints = []
 | 
			
		||||
        for project_id in project_ids:
 | 
			
		||||
            data = [copy.deepcopy(sample)
 | 
			
		||||
                    for i in range(min_length + random.randint(1, 10))]
 | 
			
		||||
            for elem in data:
 | 
			
		||||
                elem['groupby']['id'] = uuidutils.generate_uuid()
 | 
			
		||||
                elem['groupby']['project_id'] = project_id
 | 
			
		||||
            dataframes += data
 | 
			
		||||
        usage[metric_name] = dataframes
 | 
			
		||||
            datapoints += [dataframe.DataPoint(
 | 
			
		||||
                elem['vol']['unit'],
 | 
			
		||||
                elem['vol']['qty'],
 | 
			
		||||
                elem['rating']['price'],
 | 
			
		||||
                elem['groupby'],
 | 
			
		||||
                elem['metadata'],
 | 
			
		||||
            ) for elem in data]
 | 
			
		||||
        df.add_points(datapoints, metric_name)
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        'usage': usage,
 | 
			
		||||
        'period': {
 | 
			
		||||
            'begin': start,
 | 
			
		||||
            'end': end
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return df
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def load_conf(*args):
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@
 | 
			
		||||
 | 
			
		||||
from oslo_log import log
 | 
			
		||||
 | 
			
		||||
from cloudkitty import dataframe
 | 
			
		||||
from cloudkitty import transformer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -24,15 +25,16 @@ LOG = log.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
class CloudKittyFormatTransformer(transformer.BaseTransformer):
 | 
			
		||||
    def format_item(self, groupby, metadata, unit, qty=1.0):
 | 
			
		||||
        data = {}
 | 
			
		||||
        data['groupby'] = groupby
 | 
			
		||||
        data['metadata'] = metadata
 | 
			
		||||
        # For backward compatibility.
 | 
			
		||||
        data['desc'] = data['groupby'].copy()
 | 
			
		||||
        data['desc'].update(data['metadata'])
 | 
			
		||||
        data['vol'] = {'unit': unit, 'qty': qty}
 | 
			
		||||
        # data = {}
 | 
			
		||||
        # data['groupby'] = groupby
 | 
			
		||||
        # data['metadata'] = metadata
 | 
			
		||||
        # # For backward compatibility.
 | 
			
		||||
        # data['desc'] = data['groupby'].copy()
 | 
			
		||||
        # data['desc'].update(data['metadata'])
 | 
			
		||||
        # data['vol'] = {'unit': unit, 'qty': qty}
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
        return dataframe.DataPoint(unit, qty, 0, groupby, metadata)
 | 
			
		||||
        # return data
 | 
			
		||||
 | 
			
		||||
    def format_service(self, service, items):
 | 
			
		||||
        data = {}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,5 @@
 | 
			
		||||
---
 | 
			
		||||
other:
 | 
			
		||||
  - |
 | 
			
		||||
    Data frames/points are now internally represented as objects rather than
 | 
			
		||||
    dicts.
 | 
			
		||||
		Reference in New Issue
	
	Block a user