Remove gnocchi and gnocchihybrid storage
As announced during the Queens cycle, the gnocchi and gnocchihybrid storage backends have been removed. Change-Id: I7654721cfaf7a48be8789ae4eb6939b4910ec9db Task: 6294 Story: 2001503
This commit is contained in:
@@ -24,7 +24,7 @@ import cloudkitty.config
|
|||||||
import cloudkitty.orchestrator
|
import cloudkitty.orchestrator
|
||||||
import cloudkitty.service
|
import cloudkitty.service
|
||||||
import cloudkitty.storage
|
import cloudkitty.storage
|
||||||
import cloudkitty.storage.gnocchi
|
import cloudkitty.storage.hybrid.backends.gnocchi
|
||||||
import cloudkitty.tenant_fetcher
|
import cloudkitty.tenant_fetcher
|
||||||
import cloudkitty.tenant_fetcher.keystone
|
import cloudkitty.tenant_fetcher.keystone
|
||||||
import cloudkitty.utils
|
import cloudkitty.utils
|
||||||
@@ -55,7 +55,7 @@ _opts = [
|
|||||||
('storage', list(itertools.chain(
|
('storage', list(itertools.chain(
|
||||||
cloudkitty.storage.storage_opts))),
|
cloudkitty.storage.storage_opts))),
|
||||||
('storage_gnocchi', list(itertools.chain(
|
('storage_gnocchi', list(itertools.chain(
|
||||||
cloudkitty.storage.gnocchi.gnocchi_storage_opts))),
|
cloudkitty.storage.hybrid.backends.gnocchi.gnocchi_storage_opts))),
|
||||||
('tenant_fetcher', list(itertools.chain(
|
('tenant_fetcher', list(itertools.chain(
|
||||||
cloudkitty.tenant_fetcher.fetchers_opts))),
|
cloudkitty.tenant_fetcher.fetchers_opts))),
|
||||||
(None, list(itertools.chain(
|
(None, list(itertools.chain(
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ def get_storage(collector=None):
|
|||||||
cfg.CONF.storage.backend,
|
cfg.CONF.storage.backend,
|
||||||
invoke_on_load=True,
|
invoke_on_load=True,
|
||||||
invoke_kwds=storage_args).driver
|
invoke_kwds=storage_args).driver
|
||||||
if cfg.CONF.storage.backend not in ['sqlalchemy', 'hybrid']:
|
|
||||||
LOG.warning('{} storage backend is deprecated and will be removed '
|
|
||||||
'in a future release.'.format(cfg.CONF.storage.backend))
|
|
||||||
return backend
|
return backend
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,415 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 (c) Openstack Foundation
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
#
|
|
||||||
# @author: Sergio Colinas
|
|
||||||
#
|
|
||||||
import datetime
|
|
||||||
import decimal
|
|
||||||
import json
|
|
||||||
|
|
||||||
import dateutil.parser
|
|
||||||
from gnocchiclient import client as gclient
|
|
||||||
from gnocchiclient import exceptions as gexceptions
|
|
||||||
from keystoneauth1 import loading as ks_loading
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cloudkitty import storage
|
|
||||||
from cloudkitty import utils as ck_utils
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
CONF = cfg.CONF
|
|
||||||
|
|
||||||
METRICS_CONF = ck_utils.get_metrics_conf(CONF.collect.metrics_conf)
|
|
||||||
|
|
||||||
GNOCCHI_STORAGE_OPTS = 'storage_gnocchi'
|
|
||||||
gnocchi_storage_opts = [
|
|
||||||
cfg.StrOpt('interface',
|
|
||||||
default='internalURL',
|
|
||||||
help='endpoint url type'),
|
|
||||||
cfg.StrOpt('archive_policy_name',
|
|
||||||
default='rating',
|
|
||||||
help='Gnocchi storage archive policy name.'),
|
|
||||||
# The archive policy definition MUST include the collect period granularity
|
|
||||||
cfg.StrOpt('archive_policy_definition',
|
|
||||||
default='[{"granularity": '
|
|
||||||
+ six.text_type(METRICS_CONF['period']) +
|
|
||||||
', "timespan": "90 days"}, '
|
|
||||||
'{"granularity": 86400, "timespan": "360 days"}, '
|
|
||||||
'{"granularity": 2592000, "timespan": "1800 days"}]',
|
|
||||||
help='Gnocchi storage archive policy definition.'), ]
|
|
||||||
CONF.register_opts(gnocchi_storage_opts, GNOCCHI_STORAGE_OPTS)
|
|
||||||
|
|
||||||
ks_loading.register_session_conf_options(
|
|
||||||
CONF,
|
|
||||||
GNOCCHI_STORAGE_OPTS)
|
|
||||||
ks_loading.register_auth_conf_options(
|
|
||||||
CONF,
|
|
||||||
GNOCCHI_STORAGE_OPTS)
|
|
||||||
|
|
||||||
CLOUDKITTY_STATE_RESOURCE = 'cloudkitty_state'
|
|
||||||
CLOUDKITTY_STATE_METRIC = 'state'
|
|
||||||
|
|
||||||
|
|
||||||
class GnocchiStorage(storage.BaseStorage):
|
|
||||||
"""Gnocchi Storage Backend.
|
|
||||||
|
|
||||||
Driver used to add full native support for gnocchi, improving performance
|
|
||||||
and taking advantage of gnocchi capabilities.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super(GnocchiStorage, self).__init__(**kwargs)
|
|
||||||
self.auth = ks_loading.load_auth_from_conf_options(
|
|
||||||
CONF,
|
|
||||||
GNOCCHI_STORAGE_OPTS)
|
|
||||||
self.session = ks_loading.load_session_from_conf_options(
|
|
||||||
CONF,
|
|
||||||
GNOCCHI_STORAGE_OPTS,
|
|
||||||
auth=self.auth)
|
|
||||||
self._conn = gclient.Client(
|
|
||||||
'1',
|
|
||||||
session=self.session,
|
|
||||||
adapter_options={'connect_retries': 3,
|
|
||||||
'interface': CONF.storage_gnocchi.interface})
|
|
||||||
self._measures = {}
|
|
||||||
self._archive_policy_name = (
|
|
||||||
CONF.storage_gnocchi.archive_policy_name)
|
|
||||||
self._archive_policy_definition = json.loads(
|
|
||||||
CONF.storage_gnocchi.archive_policy_definition)
|
|
||||||
self._period = METRICS_CONF['period']
|
|
||||||
if "period" in kwargs:
|
|
||||||
self._period = kwargs["period"]
|
|
||||||
|
|
||||||
def init(self):
|
|
||||||
# Creates rating archive-policy if not exists
|
|
||||||
try:
|
|
||||||
self._conn.archive_policy.get(self._archive_policy_name)
|
|
||||||
except gexceptions.ArchivePolicyNotFound:
|
|
||||||
ck_policy = {}
|
|
||||||
ck_policy["name"] = self._archive_policy_name
|
|
||||||
ck_policy["back_window"] = 0
|
|
||||||
ck_policy["aggregation_methods"] = ["sum", ]
|
|
||||||
ck_policy["definition"] = self._archive_policy_definition
|
|
||||||
self._conn.archive_policy.create(ck_policy)
|
|
||||||
# Creates state resource if it doesn't exist
|
|
||||||
try:
|
|
||||||
self._conn.resource_type.create(
|
|
||||||
{'name': CLOUDKITTY_STATE_RESOURCE})
|
|
||||||
except gexceptions.ResourceAlreadyExists:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _get_or_create_resource(self, resource_type, tenant_id):
|
|
||||||
"""Return the id of a resource or create it.
|
|
||||||
|
|
||||||
:param resource_type: The type of the resource.
|
|
||||||
:type metric_name: str
|
|
||||||
:param tenant_id: Owner's resource tenant id.
|
|
||||||
:type metric_name: str
|
|
||||||
"""
|
|
||||||
query = {"=": {"project_id": tenant_id}}
|
|
||||||
resources = self._conn.resource.search(
|
|
||||||
resource_type=resource_type,
|
|
||||||
query=query,
|
|
||||||
limit=1)
|
|
||||||
if not resources:
|
|
||||||
# NOTE(sheeprine): We don't have the user id information and we are
|
|
||||||
# doing rating on a per tenant basis. Put garbage in it
|
|
||||||
resource = self._conn.resource.create(
|
|
||||||
resource_type=resource_type,
|
|
||||||
resource={'id': uuidutils.generate_uuid(),
|
|
||||||
'user_id': None,
|
|
||||||
'project_id': tenant_id})
|
|
||||||
return resource['id']
|
|
||||||
return resources[0]['id']
|
|
||||||
|
|
||||||
def _get_or_create_metric(self, metric_name, resource_id):
|
|
||||||
"""Return the metric id from a metric or create it.
|
|
||||||
|
|
||||||
:param metric_name: The name of the metric.
|
|
||||||
:type metric_name: str
|
|
||||||
:param resource_id: Resource id containing the metric.
|
|
||||||
:type metric_name: str
|
|
||||||
"""
|
|
||||||
resource = self._conn.resource.get(
|
|
||||||
resource_type='generic',
|
|
||||||
resource_id=resource_id,
|
|
||||||
history=False)
|
|
||||||
metric_id = resource["metrics"].get(metric_name)
|
|
||||||
if not metric_id:
|
|
||||||
new_metric = {}
|
|
||||||
new_metric["archive_policy_name"] = self._archive_policy_name
|
|
||||||
new_metric["name"] = metric_name
|
|
||||||
new_metric["resource_id"] = resource_id
|
|
||||||
metric = self._conn.metric.create(new_metric)
|
|
||||||
metric_id = metric['id']
|
|
||||||
return metric_id
|
|
||||||
|
|
||||||
def _pre_commit(self, tenant_id):
|
|
||||||
measures = self._measures.pop(tenant_id, {})
|
|
||||||
self._measures[tenant_id] = dict()
|
|
||||||
for resource_id, metrics in measures.items():
|
|
||||||
total = metrics.pop('total.cost')
|
|
||||||
total_id = self._get_or_create_metric(
|
|
||||||
'total.cost',
|
|
||||||
resource_id)
|
|
||||||
# TODO(sheeprine): Find a better way to handle total
|
|
||||||
total_value = sum([decimal.Decimal(val["value"]) for val in total])
|
|
||||||
total_timestamp = max([dateutil.parser.parse(val["timestamp"])
|
|
||||||
for val in total])
|
|
||||||
self._measures[tenant_id][total_id] = [{
|
|
||||||
'timestamp': total_timestamp.isoformat(),
|
|
||||||
'value': six.text_type(total_value)}]
|
|
||||||
for metric_name, values in metrics.items():
|
|
||||||
metric_id = self._get_or_create_metric(
|
|
||||||
metric_name,
|
|
||||||
resource_id)
|
|
||||||
self._measures[tenant_id][metric_id] = values
|
|
||||||
state_resource_id = self._get_or_create_resource(
|
|
||||||
CLOUDKITTY_STATE_RESOURCE,
|
|
||||||
tenant_id)
|
|
||||||
state_metric_id = self._get_or_create_metric(
|
|
||||||
CLOUDKITTY_STATE_METRIC,
|
|
||||||
state_resource_id)
|
|
||||||
self._measures[tenant_id][state_metric_id] = [{
|
|
||||||
'timestamp': self.usage_start_dt.get(tenant_id).isoformat(),
|
|
||||||
'value': 1}]
|
|
||||||
|
|
||||||
def _commit(self, tenant_id):
|
|
||||||
if tenant_id in self._measures:
|
|
||||||
self._conn.metric.batch_metrics_measures(
|
|
||||||
self._measures[tenant_id])
|
|
||||||
|
|
||||||
def _post_commit(self, tenant_id):
|
|
||||||
super(GnocchiStorage, self)._post_commit(tenant_id)
|
|
||||||
if tenant_id in self._measures:
|
|
||||||
del self._measures[tenant_id]
|
|
||||||
|
|
||||||
def _append_metric(self, resource_id, metric_name, value, tenant_id):
|
|
||||||
sample = {}
|
|
||||||
sample["timestamp"] = self.usage_start_dt.get(tenant_id).isoformat()
|
|
||||||
sample["value"] = six.text_type(value)
|
|
||||||
measures = self._measures.get(tenant_id) or dict()
|
|
||||||
if not measures:
|
|
||||||
self._measures[tenant_id] = measures
|
|
||||||
metrics = measures.get(resource_id) or dict()
|
|
||||||
if not metrics:
|
|
||||||
measures[resource_id] = metrics
|
|
||||||
metrics[metric_name] = [sample]
|
|
||||||
|
|
||||||
def _dispatch(self, data, tenant_id):
|
|
||||||
for metric_name, metrics in data.items():
|
|
||||||
for item in metrics:
|
|
||||||
resource_id = item["desc"]["resource_id"]
|
|
||||||
price = item["rating"]["price"]
|
|
||||||
self._append_metric(
|
|
||||||
resource_id,
|
|
||||||
metric_name + ".cost",
|
|
||||||
price,
|
|
||||||
tenant_id)
|
|
||||||
self._append_metric(
|
|
||||||
resource_id,
|
|
||||||
'total.cost',
|
|
||||||
price,
|
|
||||||
tenant_id)
|
|
||||||
self._has_data[tenant_id] = True
|
|
||||||
|
|
||||||
def set_state(self, state, tenant_id):
|
|
||||||
state_resource_id = self._get_or_create_resource(
|
|
||||||
CLOUDKITTY_STATE_RESOURCE,
|
|
||||||
tenant_id)
|
|
||||||
state_metric_id = self._get_or_create_metric(
|
|
||||||
CLOUDKITTY_STATE_METRIC,
|
|
||||||
state_resource_id)
|
|
||||||
self._conn.metric.add_measures(
|
|
||||||
state_metric_id,
|
|
||||||
[{'timestamp': state.isoformat(),
|
|
||||||
'value': 1}])
|
|
||||||
|
|
||||||
def get_state(self, tenant_id=None):
|
|
||||||
# Return the last written frame's timestamp.
|
|
||||||
query = {"=": {"project_id": tenant_id}} if tenant_id else {}
|
|
||||||
state_resource_id = self._get_or_create_resource(
|
|
||||||
CLOUDKITTY_STATE_RESOURCE,
|
|
||||||
tenant_id)
|
|
||||||
try:
|
|
||||||
# (aolwas) add "refresh=True" to be sure to get all posted
|
|
||||||
# measures for this particular metric
|
|
||||||
r = self._conn.metric.get_measures(
|
|
||||||
metric=CLOUDKITTY_STATE_METRIC,
|
|
||||||
resource_id=state_resource_id,
|
|
||||||
query=query,
|
|
||||||
aggregation="sum",
|
|
||||||
limit=1,
|
|
||||||
granularity=self._period,
|
|
||||||
needed_overlap=0,
|
|
||||||
refresh=True)
|
|
||||||
except gexceptions.MetricNotFound:
|
|
||||||
return
|
|
||||||
if len(r) > 0:
|
|
||||||
# NOTE(lukapeschke) Since version 5.0.0, gnocchiclient returns a
|
|
||||||
# datetime object instead of a timestamp. This fixture is made
|
|
||||||
# to ensure compatibility with all versions
|
|
||||||
try:
|
|
||||||
# (aolwas) According http://gnocchi.xyz/rest.html#metrics,
|
|
||||||
# gnocchi always returns measures ordered by timestamp
|
|
||||||
return ck_utils.dt2ts(dateutil.parser.parse(r[-1][0]))
|
|
||||||
except TypeError:
|
|
||||||
return ck_utils.dt2ts(r[-1][0])
|
|
||||||
|
|
||||||
def get_total(self, begin=None, end=None, tenant_id=None,
|
|
||||||
service=None, groupby=None):
|
|
||||||
# Get total rate in timeframe from gnocchi
|
|
||||||
metric = "total.cost"
|
|
||||||
if service:
|
|
||||||
metric = service + ".cost"
|
|
||||||
# We need to pass a query to force a post in gnocchi client metric
|
|
||||||
# aggregation, so we use one that always meets
|
|
||||||
query = {"and": [{">": {"started_at": "1900-01-01T00:00"}}]}
|
|
||||||
if tenant_id:
|
|
||||||
query = {"=": {"project_id": tenant_id}}
|
|
||||||
# TODO(Aaron): need support with groupby
|
|
||||||
if groupby:
|
|
||||||
LOG.warning('Now get total with groupby not support '
|
|
||||||
'in gnocchi storage backend')
|
|
||||||
# TODO(sheeprine): Use server side aggregation
|
|
||||||
r = self._conn.metric.aggregation(metrics=metric, query=query,
|
|
||||||
start=begin, stop=end,
|
|
||||||
aggregation="sum",
|
|
||||||
granularity=self._period,
|
|
||||||
needed_overlap=0)
|
|
||||||
|
|
||||||
rate = sum([measure[2] for measure in r]) if len(r) else 0
|
|
||||||
# Return a list of dict
|
|
||||||
totallist = []
|
|
||||||
total = dict(begin=begin, end=end, rate=rate)
|
|
||||||
totallist.append(total)
|
|
||||||
return totallist
|
|
||||||
|
|
||||||
def get_tenants(self, begin, end):
|
|
||||||
# We need to pass a query to force a post in gnocchi client metric
|
|
||||||
# aggregation, so we use one that always meets
|
|
||||||
query = {'=': {'type': 'cloudkitty_state'}}
|
|
||||||
r = self._conn.metric.aggregation(
|
|
||||||
metrics=CLOUDKITTY_STATE_METRIC,
|
|
||||||
query=query,
|
|
||||||
start=begin,
|
|
||||||
stop=end,
|
|
||||||
aggregation="sum",
|
|
||||||
granularity=self._period,
|
|
||||||
needed_overlap=0,
|
|
||||||
resource_type=CLOUDKITTY_STATE_RESOURCE,
|
|
||||||
groupby="project_id")
|
|
||||||
projects = [measures["group"]["project_id"]
|
|
||||||
for measures in r if len(measures["measures"])]
|
|
||||||
if len(projects) > 0:
|
|
||||||
return projects
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _get_resource_data(self, res_type, resource_id, begin, end):
|
|
||||||
# Get resource information from gnocchi
|
|
||||||
return self._collector.resource_info(
|
|
||||||
resource_name=res_type,
|
|
||||||
start=begin,
|
|
||||||
end=end,
|
|
||||||
project_id=None)
|
|
||||||
|
|
||||||
def _to_cloudkitty(self, res_type, resource_data, measure):
|
|
||||||
# NOTE(lukapeschke) Since version 5.0.0, gnocchiclient returns a
|
|
||||||
# datetime object instead of a timestamp. This fixture is made
|
|
||||||
# to ensure compatibility with all versions
|
|
||||||
try:
|
|
||||||
begin = dateutil.parser.parse(measure[0])
|
|
||||||
end = (dateutil.parser.parse(measure[0]) +
|
|
||||||
datetime.timedelta(seconds=self._period))
|
|
||||||
except TypeError:
|
|
||||||
begin = measure[0]
|
|
||||||
end = begin + datetime.timedelta(seconds=self._period)
|
|
||||||
cost = decimal.Decimal(measure[2])
|
|
||||||
# Rating informations
|
|
||||||
rating_dict = {}
|
|
||||||
rating_dict['price'] = cost
|
|
||||||
|
|
||||||
# Encapsulate informations in a resource dict
|
|
||||||
res_dict = {}
|
|
||||||
# TODO(sheeprine): Properly recurse on elements
|
|
||||||
resource_data = resource_data[0]
|
|
||||||
res_dict['desc'] = resource_data['desc']
|
|
||||||
if "qty" in resource_data["vol"]:
|
|
||||||
resource_data["vol"]["qty"] = (
|
|
||||||
decimal.Decimal(resource_data["vol"]["qty"]))
|
|
||||||
res_dict['vol'] = resource_data['vol']
|
|
||||||
res_dict['rating'] = rating_dict
|
|
||||||
res_dict['tenant_id'] = resource_data['desc']['project_id']
|
|
||||||
|
|
||||||
# Add resource to the usage dict
|
|
||||||
usage_dict = {}
|
|
||||||
usage_dict[res_type] = [res_dict]
|
|
||||||
|
|
||||||
# Time informations
|
|
||||||
period_dict = {}
|
|
||||||
period_dict['begin'] = begin.isoformat()
|
|
||||||
period_dict['end'] = end.isoformat()
|
|
||||||
|
|
||||||
# Add period to the resource informations
|
|
||||||
ck_dict = {}
|
|
||||||
ck_dict['period'] = period_dict
|
|
||||||
ck_dict['usage'] = usage_dict
|
|
||||||
return ck_dict
|
|
||||||
|
|
||||||
def get_time_frame(self, begin, end, **filters):
|
|
||||||
tenant_id = filters.get('tenant_id')
|
|
||||||
query = dict()
|
|
||||||
if tenant_id:
|
|
||||||
query['='] = {'project_id': tenant_id}
|
|
||||||
else:
|
|
||||||
# NOTE(sheeprine): Dummy filter to comply with gnocchi
|
|
||||||
query['!='] = {'project_id': None}
|
|
||||||
try:
|
|
||||||
res_map = METRICS_CONF['services_objects']
|
|
||||||
except KeyError:
|
|
||||||
res_map = self._collector.retrieve_mappings
|
|
||||||
res_type = filters.get('res_type')
|
|
||||||
resources = [res_type] if res_type else res_map.keys()
|
|
||||||
ck_res = []
|
|
||||||
for resource in resources:
|
|
||||||
resource_type = res_map[resource]
|
|
||||||
r = self._conn.metric.aggregation(
|
|
||||||
metrics=resource + ".cost",
|
|
||||||
resource_type=resource_type,
|
|
||||||
query=query,
|
|
||||||
start=begin,
|
|
||||||
stop=end,
|
|
||||||
granularity=self._period,
|
|
||||||
aggregation="sum",
|
|
||||||
needed_overlap=0,
|
|
||||||
groupby=["type", "id"])
|
|
||||||
for resource_measures in r:
|
|
||||||
resource_data = None
|
|
||||||
for measure in resource_measures["measures"]:
|
|
||||||
if not resource_data:
|
|
||||||
resource_data = self._get_resource_data(
|
|
||||||
res_type=resource,
|
|
||||||
resource_id=resource_measures["group"]["id"],
|
|
||||||
begin=begin,
|
|
||||||
end=end)
|
|
||||||
ck_res.append(
|
|
||||||
self._to_cloudkitty(
|
|
||||||
res_type=resource,
|
|
||||||
resource_data=resource_data,
|
|
||||||
measure=measure))
|
|
||||||
return ck_res
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2015 Objectif Libre
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
#
|
|
||||||
# @author: Stéphane Albert
|
|
||||||
#
|
|
||||||
import decimal
|
|
||||||
|
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from cloudkitty.storage.gnocchi_hybrid import migration
|
|
||||||
from cloudkitty.storage.gnocchi_hybrid import models
|
|
||||||
from cloudkitty.storage import sqlalchemy as sql_storage
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class GnocchiHybridStorage(sql_storage.SQLAlchemyStorage):
|
|
||||||
"""Gnocchi Hybrid Storage Backend
|
|
||||||
|
|
||||||
Driver used to add support for gnocchi until the creation of custom
|
|
||||||
resources is supported in gnocchi.
|
|
||||||
"""
|
|
||||||
frame_model = models.HybridRatedDataframe
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def init():
|
|
||||||
migration.upgrade('head')
|
|
||||||
|
|
||||||
def _append_time_frame(self, res_type, frame, tenant_id):
|
|
||||||
rating_dict = frame.get('rating', {})
|
|
||||||
rate = rating_dict.get('price')
|
|
||||||
if not rate:
|
|
||||||
rate = decimal.Decimal(0)
|
|
||||||
if res_type == '_NO_DATA_':
|
|
||||||
resource_ref = res_type
|
|
||||||
else:
|
|
||||||
resource_ref = frame.get('resource_id')
|
|
||||||
if not resource_ref:
|
|
||||||
LOG.warning('Trying to store data collected outside of '
|
|
||||||
'gnocchi. This driver can only be used with '
|
|
||||||
'the gnocchi collector. Data not stored!')
|
|
||||||
return
|
|
||||||
self.add_time_frame(begin=self.usage_start_dt.get(tenant_id),
|
|
||||||
end=self.usage_end_dt.get(tenant_id),
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
res_type=res_type,
|
|
||||||
resource_ref=resource_ref,
|
|
||||||
rate=rate)
|
|
||||||
|
|
||||||
def add_time_frame(self, **kwargs):
|
|
||||||
"""Create a new time frame.
|
|
||||||
|
|
||||||
:param begin: Start of the dataframe.
|
|
||||||
:param end: End of the dataframe.
|
|
||||||
:param res_type: Type of the resource.
|
|
||||||
:param rate: Calculated rate for this dataframe.
|
|
||||||
:param tenant_id: tenant_id of the dataframe owner.
|
|
||||||
:param resource_ref: Reference to the gnocchi metric (UUID).
|
|
||||||
"""
|
|
||||||
super(GnocchiHybridStorage, self).add_time_frame(**kwargs)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2015 Objectif Libre
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
#
|
|
||||||
# @author: Stéphane Albert
|
|
||||||
#
|
|
||||||
from cloudkitty.common.db.alembic import env # noqa
|
|
||||||
from cloudkitty.storage.gnocchi_hybrid import models
|
|
||||||
|
|
||||||
target_metadata = models.Base.metadata
|
|
||||||
version_table = 'storage_gnocchi_hybrid_alembic'
|
|
||||||
|
|
||||||
|
|
||||||
env.run_migrations_online(target_metadata, version_table)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Copyright ${create_date.year} OpenStack Foundation
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
|
|
||||||
"""${message}
|
|
||||||
|
|
||||||
Revision ID: ${up_revision}
|
|
||||||
Revises: ${down_revision}
|
|
||||||
Create Date: ${create_date}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = ${repr(up_revision)}
|
|
||||||
down_revision = ${repr(down_revision)}
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
${imports if imports else ""}
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
${upgrades if upgrades else "pass"}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Initial migration.
|
|
||||||
|
|
||||||
Revision ID: 4c2f20df7491
|
|
||||||
Revises: None
|
|
||||||
Create Date: 2015-11-18 11:44:09.175326
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '4c2f20df7491'
|
|
||||||
down_revision = None
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.create_table(
|
|
||||||
'ghybrid_dataframes',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('begin', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('end', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('res_type', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('rate', sa.Numeric(precision=20, scale=8), nullable=False),
|
|
||||||
sa.Column('resource_ref', sa.String(length=32), nullable=False),
|
|
||||||
sa.Column('tenant_id', sa.String(length=32), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
mysql_charset='utf8',
|
|
||||||
mysql_engine='InnoDB')
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Fixed UUID length problem.
|
|
||||||
|
|
||||||
Revision ID: d39836d70aee
|
|
||||||
Revises: 4c2f20df7491
|
|
||||||
Create Date: 2016-05-11 14:04:10.984006
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'd39836d70aee'
|
|
||||||
down_revision = '4c2f20df7491'
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table('ghybrid_dataframes') as batch_op:
|
|
||||||
batch_op.alter_column(
|
|
||||||
'resource_ref',
|
|
||||||
type_=sa.String(36),
|
|
||||||
existing_type=sa.String(32))
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2015 Objectif Libre
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
#
|
|
||||||
# @author: Stéphane Albert
|
|
||||||
#
|
|
||||||
import os
|
|
||||||
|
|
||||||
from cloudkitty.common.db.alembic import migration
|
|
||||||
|
|
||||||
ALEMBIC_REPO = os.path.join(os.path.dirname(__file__), 'alembic')
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade(revision):
|
|
||||||
config = migration.load_alembic_config(ALEMBIC_REPO)
|
|
||||||
return migration.upgrade(config, revision)
|
|
||||||
|
|
||||||
|
|
||||||
def version():
|
|
||||||
config = migration.load_alembic_config(ALEMBIC_REPO)
|
|
||||||
return migration.version(config)
|
|
||||||
|
|
||||||
|
|
||||||
def revision(message, autogenerate):
|
|
||||||
config = migration.load_alembic_config(ALEMBIC_REPO)
|
|
||||||
return migration.revision(config, message, autogenerate)
|
|
||||||
|
|
||||||
|
|
||||||
def stamp(revision):
|
|
||||||
config = migration.load_alembic_config(ALEMBIC_REPO)
|
|
||||||
return migration.stamp(config, revision)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2015 Objectif Libre
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
#
|
|
||||||
# @author: Stéphane Albert
|
|
||||||
#
|
|
||||||
from oslo_db.sqlalchemy import models
|
|
||||||
import sqlalchemy
|
|
||||||
from sqlalchemy.ext import declarative
|
|
||||||
|
|
||||||
from cloudkitty import utils as ck_utils
|
|
||||||
|
|
||||||
Base = declarative.declarative_base()
|
|
||||||
|
|
||||||
|
|
||||||
class HybridRatedDataframe(Base, models.ModelBase):
|
|
||||||
"""A hybrid rated dataframe.
|
|
||||||
|
|
||||||
"""
|
|
||||||
__table_args__ = {'mysql_charset': "utf8",
|
|
||||||
'mysql_engine': "InnoDB"}
|
|
||||||
__tablename__ = 'ghybrid_dataframes'
|
|
||||||
|
|
||||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
|
||||||
primary_key=True)
|
|
||||||
begin = sqlalchemy.Column(sqlalchemy.DateTime,
|
|
||||||
nullable=False)
|
|
||||||
end = sqlalchemy.Column(sqlalchemy.DateTime,
|
|
||||||
nullable=False)
|
|
||||||
res_type = sqlalchemy.Column(sqlalchemy.String(255),
|
|
||||||
nullable=False)
|
|
||||||
rate = sqlalchemy.Column(sqlalchemy.Numeric(20, 8),
|
|
||||||
nullable=False)
|
|
||||||
resource_ref = sqlalchemy.Column(sqlalchemy.String(36),
|
|
||||||
nullable=False)
|
|
||||||
tenant_id = sqlalchemy.Column(sqlalchemy.String(32),
|
|
||||||
nullable=True)
|
|
||||||
|
|
||||||
def to_cloudkitty(self, collector=None):
|
|
||||||
if not collector:
|
|
||||||
raise Exception('Gnocchi storage needs a reference '
|
|
||||||
'to the collector.')
|
|
||||||
# Rating informations
|
|
||||||
rating_dict = {}
|
|
||||||
rating_dict['price'] = self.rate
|
|
||||||
|
|
||||||
# Resource information from gnocchi
|
|
||||||
resource_data = collector.resource_info(
|
|
||||||
resource_name=self.res_type,
|
|
||||||
start=ck_utils.dt2ts(self.begin),
|
|
||||||
end=ck_utils.dt2ts(self.end),
|
|
||||||
project_id=self.tenant_id)
|
|
||||||
|
|
||||||
# Encapsulate informations in a resource dict
|
|
||||||
res_dict = {}
|
|
||||||
resource_data = resource_data[0]
|
|
||||||
res_dict['desc'] = resource_data['desc']
|
|
||||||
res_dict['vol'] = resource_data['vol']
|
|
||||||
res_dict['rating'] = rating_dict
|
|
||||||
res_dict['tenant_id'] = self.tenant_id
|
|
||||||
|
|
||||||
# Add resource to the usage dict
|
|
||||||
usage_dict = {}
|
|
||||||
usage_dict[self.res_type] = [res_dict]
|
|
||||||
|
|
||||||
# Time informations
|
|
||||||
period_dict = {}
|
|
||||||
period_dict['begin'] = ck_utils.dt2iso(self.begin)
|
|
||||||
period_dict['end'] = ck_utils.dt2iso(self.end)
|
|
||||||
|
|
||||||
# Add period to the resource informations
|
|
||||||
ck_dict = {}
|
|
||||||
ck_dict['period'] = period_dict
|
|
||||||
ck_dict['usage'] = usage_dict
|
|
||||||
return ck_dict
|
|
||||||
@@ -139,27 +139,28 @@ The following shows the basic configuration items:
|
|||||||
|
|
||||||
The tenant named ``service`` is also commonly called ``services``
|
The tenant named ``service`` is also commonly called ``services``
|
||||||
|
|
||||||
It is now time to configure the storage backend. Four storage backends are
|
It is now time to configure the storage backend. Two storage backends are
|
||||||
available: ``sqlalchemy``, ``hybrid``, ``gnocchihybrid``, and ``gnocchi``.
|
available: ``sqlalchemy`` and ``hybrid`` (which will soon become the v2
|
||||||
|
storage).
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
||||||
[storage]
|
[storage]
|
||||||
backend = gnocchi
|
backend = hybrid
|
||||||
|
|
||||||
As you will see in the following example, collector and storage backends
|
As you will see in the following example, collector and storage backends
|
||||||
sometimes need additional configuration sections. (The tenant fetcher works the
|
sometimes need additional configuration sections. (The tenant fetcher works the
|
||||||
same way, but for now, only Keystone is supported). The section's name has the
|
same way). The section's name has the following format:
|
||||||
following format: ``{backend_name}_{backend_type}`` (``gnocchi_collector`` for
|
``{backend_name}_{backend_type}`` (``gnocchi_collector`` for example), except
|
||||||
example), except for ``storage_gnocchi``.
|
for ``storage_gnocchi``.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
The section name format should become ``{backend_type}_{backend_name}`` for
|
The section name format should become ``{backend_type}_{backend_name}`` for
|
||||||
all sections in the future (``storage_gnocchi`` style).
|
all sections in the future (``storage_gnocchi`` style).
|
||||||
|
|
||||||
If you want to use the pure gnocchi storage or the hybrid storage with a
|
If you want to use the hybrid storage with a gnocchi backend, add the following
|
||||||
gnocchi backend, add the following entry:
|
entry:
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
deprecations:
|
||||||
|
- |
|
||||||
|
The gnocchi and gnocchihybrid storage backends have been removed.
|
||||||
@@ -67,8 +67,6 @@ cloudkitty.rating.processors =
|
|||||||
|
|
||||||
cloudkitty.storage.backends =
|
cloudkitty.storage.backends =
|
||||||
sqlalchemy = cloudkitty.storage.sqlalchemy:SQLAlchemyStorage
|
sqlalchemy = cloudkitty.storage.sqlalchemy:SQLAlchemyStorage
|
||||||
gnocchihybrid = cloudkitty.storage.gnocchi_hybrid:GnocchiHybridStorage
|
|
||||||
gnocchi = cloudkitty.storage.gnocchi:GnocchiStorage
|
|
||||||
hybrid = cloudkitty.storage.hybrid:HybridStorage
|
hybrid = cloudkitty.storage.hybrid:HybridStorage
|
||||||
|
|
||||||
cloudkitty.storage.hybrid.backends =
|
cloudkitty.storage.hybrid.backends =
|
||||||
|
|||||||
Reference in New Issue
Block a user