cloudkitty/cloudkitty/billing/hash/__init__.py

294 lines
10 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2014 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 pecan
from pecan import rest
from pecan import routing
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from cloudkitty import billing
from cloudkitty.billing.hash.db import api
from cloudkitty.db import api as db_api
from cloudkitty.openstack.common import log as logging
LOG = logging.getLogger(__name__)
MAP_TYPE = wtypes.Enum(wtypes.text, 'flat', 'rate')
class Mapping(wtypes.Base):
map_type = wtypes.wsattr(MAP_TYPE, default='rate', name='type')
"""Type of the mapping."""
value = wtypes.wsattr(float, mandatory=True)
"""Value of the mapping."""
@classmethod
def sample(cls):
sample = cls(value=4.2)
return sample
class BasicHashMapConfigController(rest.RestController):
"""RestController for hashmap's configuration."""
_custom_actions = {
'types': ['GET']
}
@wsme_pecan.wsexpose([wtypes.text])
def get_types(self):
"""Return the list of every mapping type available.
"""
return MAP_TYPE.values
@pecan.expose()
def _route(self, args, request=None):
if len(args) > 2:
# Taken from base _route function
if request is None:
from pecan import request # noqa
method = request.params.get('_method', request.method).lower()
if request.method == 'GET' and method in ('delete', 'put'):
pecan.abort(405)
if request.method == 'GET':
return routing.lookup_controller(self.get_mapping, args)
return super(BasicHashMapConfigController, self)._route(args)
@wsme_pecan.wsexpose(Mapping, wtypes.text, wtypes.text, wtypes.text)
def get_mapping(self, service, field, key):
"""Get a mapping from full path.
"""
hashmap = api.get_instance()
try:
return hashmap.get_mapping(service, field, key)
except (api.NoSuchService, api.NoSuchField, api.NoSuchMapping) as e:
pecan.abort(400, str(e))
@wsme_pecan.wsexpose([wtypes.text])
def get(self):
"""Get the service list
:return: List of every services' name.
"""
hashmap = api.get_instance()
return [service.name for service in hashmap.list_services()]
@wsme_pecan.wsexpose([wtypes.text], wtypes.text, wtypes.text)
def get_one(self, service=None, field=None):
"""Return the list of every sub keys.
:param service: (Optional) Filter on this service.
:param field: (Optional) Filter on this field.
"""
hashmap = api.get_instance()
if field:
try:
return [mapping.key for mapping in hashmap.list_mappings(
service,
field)]
except (api.NoSuchService, api.NoSuchField) as e:
pecan.abort(400, str(e))
else:
try:
return [f.name for f in hashmap.list_fields(service)]
except api.NoSuchService as e:
pecan.abort(400, str(e))
# FIXME (sheeprine): Still a problem with our routing and the different
# object types. For service/field it's text or a mapping.
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text,
body=Mapping)
def post(self, service, field=None, key=None, mapping=None):
"""Create hashmap fields.
:param service: Name of the service to create.
:param field: (Optional) Name of the field to create.
:param key: (Optional) Name of the key to create.
:param mapping: (Optional) Mapping object to create.
"""
hashmap = api.get_instance()
if field:
if key:
if mapping:
try:
# FIXME(sheeprine): We should return the result
hashmap.create_mapping(
service,
field,
key,
value=mapping.value,
map_type=mapping.map_type
)
pecan.response.headers['Location'] = pecan.request.path
except api.MappingAlreadyExists as e:
pecan.abort(409, str(e))
else:
e = ValueError('Mapping can\'t be empty.')
pecan.abort(400, str(e))
else:
try:
hashmap.create_field(service, field)
pecan.response.headers['Location'] = pecan.request.path
except api.FieldAlreadyExists as e:
pecan.abort(409, str(e))
else:
try:
hashmap.create_service(service)
pecan.response.headers['Location'] = pecan.request.path
except api.ServiceAlreadyExists as e:
pecan.abort(409, str(e))
self.notify_reload()
pecan.response.status = 201
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text,
body=Mapping)
def put(self, service, field, key, mapping):
"""Modify hashmap fields
:param service: Filter on this service.
:param field: Filter on this field.
:param key: Modify the content of this key.
:param mapping: Mapping object to update.
"""
hashmap = api.get_instance()
try:
hashmap.update_mapping(
service,
field,
key,
value=mapping.value,
map_type=mapping.map_type
)
pecan.response.headers['Location'] = pecan.request.path
pecan.response.status = 204
except (api.NoSuchService, api.NoSuchField, api.NoSuchMapping) as e:
pecan.abort(400, str(e))
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text)
def delete(self, service, field=None, key=None):
"""Delete the parent and all the sub keys recursively.
:param service: Name of the service to delete.
:param field: (Optional) Name of the field to delete.
:param key: (Optional) Name of the key to delete.
"""
hashmap = api.get_instance()
try:
if field:
if key:
hashmap.delete_mapping(service, field, key)
else:
hashmap.delete_field(service, field)
else:
hashmap.delete_service(service)
except (api.NoSuchService, api.NoSuchField, api.NoSuchMapping) as e:
pecan.abort(400, str(e))
pecan.response.status = 204
class BasicHashMap(billing.BillingProcessorBase):
module_name = 'hashmap'
description = 'Basic hashmap billing module.'
hot_config = True
config_controller = BasicHashMapConfigController
db_api = api.get_instance()
def __init__(self, tenant_id=None):
super(BasicHashMap, self).__init__(tenant_id)
self._billing_info = {}
self._load_billing_rates()
@property
def enabled(self):
"""Check if the module is enabled
:returns: bool if module is enabled
"""
# FIXME(sheeprine): Hardcoded values to check the state
api = db_api.get_instance()
module_db = api.get_module_enable_state()
return module_db.get_state('hashmap') or False
def reload_config(self):
self._load_billing_rates()
def _load_billing_rates(self):
self._billing_info = {}
hashmap = api.get_instance()
services = hashmap.list_services()
for service in services:
service = service[0]
self._billing_info[service] = {}
fields = hashmap.list_fields(service)
for field in fields:
field = field[0]
self._billing_info[service][field] = {}
mappings = hashmap.list_mappings(service, field)
for mapping in mappings:
mapping = mapping[0]
mapping_db = hashmap.get_mapping(service, field, mapping)
map_dict = {}
map_dict['value'] = mapping_db.value
map_dict['type'] = mapping_db.map_type
self._billing_info[service][field][mapping] = map_dict
def process_service(self, name, data):
if name not in self._billing_info:
return
serv_b_info = self._billing_info[name]
for entry in data:
flat = 0
rate = 1
entry_desc = entry['desc']
for field in serv_b_info:
if field not in entry_desc:
continue
b_info = serv_b_info[field]
key = entry_desc[field]
value = 0
if key in b_info:
value = b_info[key]['value']
elif '_DEFAULT_' in b_info:
value = b_info['_DEFAULT_']
if value:
if b_info[key]['type'] == 'rate':
rate *= value
elif b_info[key]['type'] == 'flat':
new_flat = 0
new_flat = value
if new_flat > flat:
flat = new_flat
entry['billing'] = {'price': flat * rate}
def process(self, data):
for cur_data in data:
cur_usage = cur_data['usage']
for service in cur_usage:
self.process_service(service, cur_usage[service])
return data