Split the api controllers and resources
- Move the v1 controllers to a separate controllers package - Split each controller into separate packages - Move the v1 resources to a datamodels package - Split each resource into separate packages - Get the RPC client and RPC target from a global object rather than from the request context - Changed the base Controller API (breaking change) Co-Authored-By: Guillaume Espanel <guillaume.espanel@objectif-libre.com> Change-Id: Ic73d9e85dbfd637b40dc45111e21dc7c800b6ed0
This commit is contained in:
parent
987ac72a7a
commit
1f21760064
@ -19,18 +19,13 @@ import os
|
||||
from wsgiref import simple_server
|
||||
|
||||
from oslo.config import cfg
|
||||
try:
|
||||
import oslo_messaging as messaging
|
||||
except ImportError:
|
||||
from oslo import messaging
|
||||
from paste import deploy
|
||||
import pecan
|
||||
|
||||
from cloudkitty.api import config as api_config
|
||||
from cloudkitty.api import hooks
|
||||
from cloudkitty.common import rpc
|
||||
from cloudkitty import config # noqa
|
||||
from cloudkitty.openstack.common import log as logging
|
||||
from cloudkitty import rpc
|
||||
from cloudkitty import storage
|
||||
|
||||
|
||||
@ -69,10 +64,7 @@ def setup_app(pecan_config=None, extra_hooks=None):
|
||||
|
||||
app_conf = get_pecan_config()
|
||||
|
||||
target = messaging.Target(topic='cloudkitty',
|
||||
version='1.0')
|
||||
|
||||
client = rpc.get_client(target)
|
||||
client = rpc.get_client()
|
||||
|
||||
storage_backend = storage.get_storage()
|
||||
|
||||
|
@ -16,7 +16,7 @@ from cloudkitty import config # noqa
|
||||
|
||||
# Pecan Application Configurations
|
||||
app = {
|
||||
'root': 'cloudkitty.api.controllers.root.RootController',
|
||||
'root': 'cloudkitty.api.root.RootController',
|
||||
'modules': ['cloudkitty.api'],
|
||||
'static_root': '%(confdir)s/public',
|
||||
'template_path': '%(confdir)s/templates',
|
||||
|
@ -1,366 +0,0 @@
|
||||
# -*- 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 datetime
|
||||
import decimal
|
||||
|
||||
from oslo.config import cfg
|
||||
import pecan
|
||||
from pecan import rest
|
||||
from stevedore import extension
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from cloudkitty.api.controllers import types as cktypes
|
||||
from cloudkitty import config # noqa
|
||||
from cloudkitty.db import api as db_api
|
||||
from cloudkitty.openstack.common import log as logging
|
||||
from cloudkitty import storage as ck_storage
|
||||
from cloudkitty import utils as ck_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CLOUDKITTY_SERVICES = wtypes.Enum(wtypes.text,
|
||||
*CONF.collect.services)
|
||||
|
||||
|
||||
class ResourceDescriptor(wtypes.Base):
|
||||
"""Type describing a resource in CloudKitty.
|
||||
|
||||
"""
|
||||
|
||||
service = CLOUDKITTY_SERVICES
|
||||
"""Name of the service."""
|
||||
|
||||
# FIXME(sheeprine): values should be dynamic
|
||||
# Testing with ironic dynamic type
|
||||
desc = {wtypes.text: cktypes.MultiType(wtypes.text, int, float, dict)}
|
||||
"""Description of the resources parameters."""
|
||||
|
||||
volume = decimal.Decimal
|
||||
"""Number of resources."""
|
||||
|
||||
def to_json(self):
|
||||
res_dict = {}
|
||||
res_dict[self.service] = [{'desc': self.desc,
|
||||
'vol': {'qty': self.volume,
|
||||
'unit': 'undef'}
|
||||
}]
|
||||
return res_dict
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(service='compute',
|
||||
desc={
|
||||
'image_id': 'a41fba37-2429-4f15-aa00-b5bc4bf557bf'
|
||||
},
|
||||
volume=decimal.Decimal(1))
|
||||
return sample
|
||||
|
||||
|
||||
class RatedResource(ResourceDescriptor):
|
||||
"""Represents a rated CloudKitty resource."""
|
||||
|
||||
billing = decimal.Decimal
|
||||
|
||||
def to_json(self):
|
||||
res_dict = super(RatedResource, self).to_json()
|
||||
res_dict['billing'] = self.billing
|
||||
return res_dict
|
||||
|
||||
|
||||
class ServiceToCollectorMapping(wtypes.Base):
|
||||
"""Type describing a service to collector mapping.
|
||||
|
||||
"""
|
||||
|
||||
service = wtypes.text
|
||||
"""Name of the service."""
|
||||
|
||||
collector = wtypes.text
|
||||
"""Name of the collector."""
|
||||
|
||||
def to_json(self):
|
||||
res_dict = {}
|
||||
res_dict[self.service] = self.collector
|
||||
return res_dict
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(service='compute',
|
||||
collector='ceilometer')
|
||||
return sample
|
||||
|
||||
|
||||
class MappingController(rest.RestController):
|
||||
"""REST Controller managing service to collector mapping.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._db = db_api.get_instance().get_service_to_collector_mapping()
|
||||
|
||||
@wsme_pecan.wsexpose([wtypes.text])
|
||||
def get_all(self):
|
||||
"""Return the list of every services mapped.
|
||||
|
||||
:return: List of every services mapped.
|
||||
"""
|
||||
return [mapping.service for mapping in self._db.list_services()]
|
||||
|
||||
@wsme_pecan.wsexpose(ServiceToCollectorMapping, wtypes.text)
|
||||
def get_one(self, service):
|
||||
"""Return a service to collector mapping.
|
||||
|
||||
:param service: Name of the service to filter on.
|
||||
"""
|
||||
try:
|
||||
return self._db.get_mapping(service)
|
||||
except db_api.NoSuchMapping as e:
|
||||
pecan.abort(400, str(e))
|
||||
pecan.response.status = 200
|
||||
|
||||
@wsme_pecan.wsexpose(ServiceToCollectorMapping,
|
||||
wtypes.text,
|
||||
body=wtypes.text)
|
||||
def post(self, service, collector):
|
||||
"""Create or modify a mapping.
|
||||
|
||||
:param service: Name of the service to map a collector to.
|
||||
:param collector: Name of the collector.
|
||||
"""
|
||||
return self._db.set_mapping(service, collector)
|
||||
|
||||
@wsme_pecan.wsexpose(None, body=wtypes.text)
|
||||
def delete(self, service):
|
||||
"""Delete a mapping.
|
||||
|
||||
:param service: Name of the service to suppress the mapping from.
|
||||
"""
|
||||
try:
|
||||
self._db.delete_mapping(service)
|
||||
except db_api.NoSuchMapping as e:
|
||||
pecan.abort(400, str(e))
|
||||
pecan.response.status = 204
|
||||
|
||||
|
||||
class CollectorController(rest.RestController):
|
||||
"""REST Controller managing collector modules.
|
||||
|
||||
"""
|
||||
|
||||
mapping = MappingController()
|
||||
|
||||
_custom_actions = {
|
||||
'state': ['GET', 'POST']
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._db = db_api.get_instance().get_module_enable_state()
|
||||
|
||||
@wsme_pecan.wsexpose(bool, wtypes.text)
|
||||
def state(self, collector):
|
||||
"""Query the enable state of a collector.
|
||||
|
||||
:param collector: Name of the collector.
|
||||
:return: State of the collector.
|
||||
"""
|
||||
return self._db.get_state('collector_{}'.format(collector))
|
||||
|
||||
@wsme_pecan.wsexpose(bool, wtypes.text, body=bool)
|
||||
def post_state(self, collector, state):
|
||||
"""Set the enable state of a collector.
|
||||
|
||||
:param collector: Name of the collector.
|
||||
:param state: New state for the collector.
|
||||
:return: State of the collector.
|
||||
"""
|
||||
return self._db.set_state('collector_{}'.format(collector), state)
|
||||
|
||||
|
||||
class ModulesController(rest.RestController):
|
||||
"""REST Controller managing billing modules.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.extensions = extension.ExtensionManager(
|
||||
'cloudkitty.billing.processors',
|
||||
# FIXME(sheeprine): don't want to load it here as we just need the
|
||||
# controller
|
||||
invoke_on_load=True
|
||||
)
|
||||
self.expose_modules()
|
||||
|
||||
def expose_modules(self):
|
||||
"""Load billing modules to expose API controllers.
|
||||
|
||||
"""
|
||||
for ext in self.extensions:
|
||||
# FIXME(sheeprine): we should notify two modules with same name
|
||||
if not hasattr(self, ext.name):
|
||||
setattr(self, ext.name, ext.obj.controller())
|
||||
|
||||
@wsme_pecan.wsexpose([wtypes.text])
|
||||
def get(self):
|
||||
"""Return the list of loaded modules.
|
||||
|
||||
:return: Name of every loaded modules.
|
||||
"""
|
||||
return [ext for ext in self.extensions.names()]
|
||||
|
||||
|
||||
class BillingController(rest.RestController):
|
||||
|
||||
_custom_actions = {
|
||||
'quote': ['POST'],
|
||||
}
|
||||
|
||||
modules = ModulesController()
|
||||
|
||||
@wsme_pecan.wsexpose(float, body=[ResourceDescriptor])
|
||||
def quote(self, res_data):
|
||||
"""Get an instant quote based on multiple resource descriptions.
|
||||
|
||||
:param res_data: List of resource descriptions.
|
||||
:return: Total price for these descriptions.
|
||||
"""
|
||||
client = pecan.request.rpc_client.prepare(namespace='billing')
|
||||
res_dict = {}
|
||||
for res in res_data:
|
||||
if res.service not in res_dict:
|
||||
res_dict[res.service] = []
|
||||
json_data = res.to_json()
|
||||
res_dict[res.service].extend(json_data[res.service])
|
||||
|
||||
res = client.call({}, 'quote', res_data=[{'usage': res_dict}])
|
||||
return res
|
||||
|
||||
|
||||
class ReportController(rest.RestController):
|
||||
"""REST Controller managing the reporting.
|
||||
|
||||
"""
|
||||
|
||||
_custom_actions = {
|
||||
'total': ['GET'],
|
||||
'tenants': ['GET']
|
||||
}
|
||||
|
||||
@wsme_pecan.wsexpose([wtypes.text],
|
||||
datetime.datetime,
|
||||
datetime.datetime)
|
||||
def tenants(self, begin=None, end=None):
|
||||
"""Return the list of rated tenants.
|
||||
|
||||
"""
|
||||
storage = pecan.request.storage_backend
|
||||
tenants = storage.get_tenants(begin, end)
|
||||
return tenants
|
||||
|
||||
@wsme_pecan.wsexpose(float,
|
||||
datetime.datetime,
|
||||
datetime.datetime,
|
||||
wtypes.text)
|
||||
def total(self, begin=None, end=None, tenant_id=None):
|
||||
"""Return the amount to pay for a given period.
|
||||
|
||||
"""
|
||||
storage = pecan.request.storage_backend
|
||||
# FIXME(sheeprine): We should filter on user id.
|
||||
# Use keystone token information by default but make it overridable and
|
||||
# enforce it by policy engine
|
||||
total = storage.get_total(begin, end, tenant_id)
|
||||
return total
|
||||
|
||||
|
||||
class DataFrame(wtypes.Base):
|
||||
"""Type describing a stored dataframe."""
|
||||
|
||||
begin = datetime.datetime
|
||||
"""Begin date for the sample."""
|
||||
|
||||
end = datetime.datetime
|
||||
"""End date for the sample."""
|
||||
|
||||
tenant_id = wtypes.text
|
||||
"""Tenant owner of the sample."""
|
||||
|
||||
resources = [RatedResource]
|
||||
"""A resource list."""
|
||||
|
||||
def to_json(self):
|
||||
return {'begin': self.begin,
|
||||
'end': self.end,
|
||||
'tenant_id': self.tenant_id,
|
||||
'resources': self.resources}
|
||||
|
||||
|
||||
class StorageController(rest.RestController):
|
||||
"""REST Controller to access stored data frames."""
|
||||
|
||||
@wsme_pecan.wsexpose([DataFrame], datetime.datetime, datetime.datetime,
|
||||
wtypes.text)
|
||||
def get_all(self, begin, end, tenant_id=None):
|
||||
"""Return a list of rated resources for a time period and a tenant.
|
||||
|
||||
:param begin: Start of the period
|
||||
:param end: End of the period
|
||||
:return: List of RatedResource objects.
|
||||
"""
|
||||
|
||||
begin_ts = ck_utils.dt2ts(begin)
|
||||
end_ts = ck_utils.dt2ts(end)
|
||||
backend = pecan.request.storage_backend
|
||||
|
||||
try:
|
||||
frames = backend.get_time_frame(begin_ts, end_ts,
|
||||
tenant_id=tenant_id)
|
||||
except ck_storage.NoTimeFrame:
|
||||
return []
|
||||
|
||||
ret = []
|
||||
for frame in frames:
|
||||
for service, data_list in frame['usage'].items():
|
||||
resources = []
|
||||
for data in data_list:
|
||||
desc = data['desc'] if data['desc'] else {}
|
||||
price = decimal.Decimal(data['billing']['price'])
|
||||
resource = RatedResource(service=service,
|
||||
desc=desc,
|
||||
volume=data['vol']['qty'],
|
||||
billing=price)
|
||||
resources.append(resource)
|
||||
data_frame = DataFrame(
|
||||
begin=ck_utils.iso2dt(frame['period']['begin']),
|
||||
end=ck_utils.iso2dt(frame['period']['end']),
|
||||
tenant_id=tenant_id, # FIXME
|
||||
resources=resources)
|
||||
ret.append(data_frame)
|
||||
return ret
|
||||
|
||||
|
||||
class V1Controller(rest.RestController):
|
||||
"""API version 1 controller.
|
||||
|
||||
"""
|
||||
|
||||
collector = CollectorController()
|
||||
billing = BillingController()
|
||||
report = ReportController()
|
||||
storage = StorageController()
|
@ -20,7 +20,7 @@ from pecan import rest
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from cloudkitty.api.controllers import v1
|
||||
from cloudkitty.api.v1 import controllers as v1_api
|
||||
from cloudkitty.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -112,7 +112,7 @@ class RootController(rest.RestController):
|
||||
|
||||
"""
|
||||
|
||||
v1 = v1.V1Controller()
|
||||
v1 = v1_api.V1Controller()
|
||||
|
||||
@wsme_pecan.wsexpose([APIVersion])
|
||||
def get(self):
|
||||
@ -124,7 +124,7 @@ class RootController(rest.RestController):
|
||||
ver1 = APIVersion(
|
||||
id='v1',
|
||||
status='EXPERIMENTAL',
|
||||
updated='2014-08-11T16:00:00Z',
|
||||
updated='2015-03-09T16:00:00Z',
|
||||
links=[
|
||||
APILink(
|
||||
rel='self',
|
34
cloudkitty/api/v1/controllers/__init__.py
Normal file
34
cloudkitty/api/v1/controllers/__init__.py
Normal file
@ -0,0 +1,34 @@
|
||||
# -*- 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
|
||||
#
|
||||
from pecan import rest
|
||||
|
||||
from cloudkitty.api.v1.controllers import billing as billing_api
|
||||
from cloudkitty.api.v1.controllers import collector as collector_api
|
||||
from cloudkitty.api.v1.controllers import report as report_api
|
||||
from cloudkitty.api.v1.controllers import storage as storage_api
|
||||
|
||||
|
||||
class V1Controller(rest.RestController):
|
||||
"""API version 1 controller.
|
||||
|
||||
"""
|
||||
|
||||
billing = billing_api.BillingController()
|
||||
collector = collector_api.CollectorController()
|
||||
report = report_api.ReportController()
|
||||
storage = storage_api.StorageController()
|
164
cloudkitty/api/v1/controllers/billing.py
Normal file
164
cloudkitty/api/v1/controllers/billing.py
Normal file
@ -0,0 +1,164 @@
|
||||
# -*- 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
|
||||
#
|
||||
from oslo.config import cfg
|
||||
import pecan
|
||||
from pecan import rest
|
||||
from stevedore import extension
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from cloudkitty.api.v1.datamodels import billing as billing_models
|
||||
from cloudkitty import config # noqa
|
||||
from cloudkitty.openstack.common import log as logging
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModulesController(rest.RestController):
|
||||
"""REST Controller managing billing modules."""
|
||||
|
||||
def __init__(self):
|
||||
self.extensions = extension.ExtensionManager(
|
||||
'cloudkitty.billing.processors',
|
||||
# FIXME(sheeprine): don't want to load it here as we just need the
|
||||
# controller
|
||||
invoke_on_load=True
|
||||
)
|
||||
|
||||
@wsme_pecan.wsexpose(billing_models.CloudkittyModuleCollection)
|
||||
def get_all(self):
|
||||
"""return the list of loaded modules.
|
||||
|
||||
:return: name of every loaded modules.
|
||||
"""
|
||||
modules_list = []
|
||||
for module in self.extensions:
|
||||
infos = module.obj.module_info.copy()
|
||||
infos['module_id'] = infos.pop('name')
|
||||
modules_list.append(billing_models.CloudkittyModule(**infos))
|
||||
|
||||
return billing_models.CloudkittyModuleCollection(
|
||||
modules=modules_list
|
||||
)
|
||||
|
||||
@wsme_pecan.wsexpose(billing_models.CloudkittyModule, wtypes.text)
|
||||
def get_one(self, module_id):
|
||||
"""return a module
|
||||
|
||||
:return: CloudKittyModule
|
||||
"""
|
||||
try:
|
||||
module = self.extensions[module_id]
|
||||
except KeyError:
|
||||
pecan.abort(404)
|
||||
infos = module.obj.module_info.copy()
|
||||
infos['module_id'] = infos.pop('name')
|
||||
return billing_models.CloudkittyModule(**infos)
|
||||
|
||||
@wsme_pecan.wsexpose(billing_models.CloudkittyModule,
|
||||
wtypes.text,
|
||||
body=billing_models.CloudkittyModule,
|
||||
status_code=302)
|
||||
def put(self, module_id, module):
|
||||
"""Change the state of a module (enabled/disabled)
|
||||
|
||||
:param module_id: name of the module to modify
|
||||
:param module: CloudKittyModule object describing the new desired state
|
||||
## :return: CloudKittyModule object describing the desired state
|
||||
"""
|
||||
try:
|
||||
self.extensions[module_id].obj.set_state(module.enabled)
|
||||
except KeyError:
|
||||
pecan.abort(404)
|
||||
pecan.response.location = pecan.request.path
|
||||
|
||||
|
||||
class UnconfigurableController(rest.RestController):
|
||||
"""This controller raises an error when requested."""
|
||||
|
||||
@wsme_pecan.wsexpose(None)
|
||||
def put(self):
|
||||
self.abort()
|
||||
|
||||
@wsme_pecan.wsexpose(None)
|
||||
def get(self):
|
||||
self.abort()
|
||||
|
||||
def abort(self):
|
||||
pecan.abort(409, "Module is not configurable")
|
||||
|
||||
|
||||
class ModulesExposer(rest.RestController):
|
||||
"""REST Controller exposing billing modules.
|
||||
|
||||
This is the controller that exposes the modules own configuration
|
||||
settings.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.extensions = extension.ExtensionManager(
|
||||
'cloudkitty.billing.processors',
|
||||
# FIXME(sheeprine): don't want to load it here as we just need the
|
||||
# controller
|
||||
invoke_on_load=True
|
||||
)
|
||||
self.expose_modules()
|
||||
|
||||
def expose_modules(self):
|
||||
"""Load billing modules to expose API controllers."""
|
||||
for ext in self.extensions:
|
||||
# FIXME(sheeprine): we should notify two modules with same name
|
||||
if not hasattr(self, ext.name):
|
||||
if not ext.obj.config_controller:
|
||||
ext.obj.config_controller = UnconfigurableController
|
||||
setattr(self, ext.name, ext.obj.config_controller())
|
||||
|
||||
|
||||
class BillingController(rest.RestController):
|
||||
"""The BillingController is exposed by the API.
|
||||
|
||||
The BillingControler connects the ModulesExposer, ModulesController
|
||||
and a quote action to the API.
|
||||
"""
|
||||
|
||||
_custom_actions = {
|
||||
'quote': ['POST'],
|
||||
}
|
||||
|
||||
modules = ModulesController()
|
||||
module_config = ModulesExposer()
|
||||
|
||||
@wsme_pecan.wsexpose(float,
|
||||
body=billing_models.CloudkittyResourceCollection)
|
||||
def quote(self, res_data):
|
||||
"""Get an instant quote based on multiple resource descriptions.
|
||||
|
||||
:param res_data: List of resource descriptions.
|
||||
:return: Total price for these descriptions.
|
||||
"""
|
||||
client = pecan.request.rpc_client.prepare(namespace='billing')
|
||||
res_dict = {}
|
||||
for res in res_data.resources:
|
||||
if res.service not in res_dict:
|
||||
res_dict[res.service] = []
|
||||
json_data = res.to_json()
|
||||
res_dict[res.service].extend(json_data[res.service])
|
||||
|
||||
res = client.call({}, 'quote', res_data=[{'usage': res_dict}])
|
||||
return res
|
83
cloudkitty/api/v1/controllers/collector.py
Normal file
83
cloudkitty/api/v1/controllers/collector.py
Normal file
@ -0,0 +1,83 @@
|
||||
# -*- 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 wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from cloudkitty.api.v1.datamodels import collector as collector_models
|
||||
from cloudkitty.db import api as db_api
|
||||
|
||||
|
||||
class MappingController(rest.RestController):
|
||||
"""REST Controller managing service to collector mappings."""
|
||||
|
||||
def __init__(self):
|
||||
self._db = db_api.get_instance().get_service_to_collector_mapping()
|
||||
|
||||
@wsme_pecan.wsexpose([wtypes.text])
|
||||
def get_all(self):
|
||||
"""Return the list of every services mapped.
|
||||
|
||||
:return: List of every services mapped.
|
||||
"""
|
||||
return [mapping.service for mapping in self._db.list_services()]
|
||||
|
||||
@wsme_pecan.wsexpose(collector_models.ServiceToCollectorMapping,
|
||||
wtypes.text)
|
||||
def get_one(self, service):
|
||||
"""Return a service to collector mapping.
|
||||
|
||||
:param service: Name of the service to filter on.
|
||||
"""
|
||||
try:
|
||||
return self._db.get_mapping(service)
|
||||
except db_api.NoSuchMapping as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
|
||||
class CollectorController(rest.RestController):
|
||||
"""REST Controller managing collector modules."""
|
||||
|
||||
mapping = MappingController()
|
||||
|
||||
_custom_actions = {
|
||||
'state': ['GET', 'POST']
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._db = db_api.get_instance().get_module_enable_state()
|
||||
|
||||
@wsme_pecan.wsexpose(bool, wtypes.text)
|
||||
def state(self, collector):
|
||||
"""Query the enable state of a collector.
|
||||
|
||||
:param collector: Name of the collector.
|
||||
:return: State of the collector.
|
||||
"""
|
||||
return self._db.get_state('collector_{}'.format(collector))
|
||||
|
||||
@wsme_pecan.wsexpose(bool, wtypes.text, body=bool)
|
||||
def post_state(self, collector, state):
|
||||
"""Set the enable state of a collector.
|
||||
|
||||
:param collector: Name of the collector.
|
||||
:param state: New state for the collector.
|
||||
:return: State of the collector.
|
||||
"""
|
||||
return self._db.set_state('collector_{}'.format(collector), state)
|
61
cloudkitty/api/v1/controllers/report.py
Normal file
61
cloudkitty/api/v1/controllers/report.py
Normal file
@ -0,0 +1,61 @@
|
||||
# -*- 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 datetime
|
||||
import decimal
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
class ReportController(rest.RestController):
|
||||
"""REST Controller managing the reporting.
|
||||
|
||||
"""
|
||||
|
||||
_custom_actions = {
|
||||
'total': ['GET'],
|
||||
'tenants': ['GET']
|
||||
}
|
||||
|
||||
@wsme_pecan.wsexpose([wtypes.text],
|
||||
datetime.datetime,
|
||||
datetime.datetime)
|
||||
def tenants(self, begin=None, end=None):
|
||||
"""Return the list of rated tenants.
|
||||
|
||||
"""
|
||||
storage = pecan.request.storage_backend
|
||||
tenants = storage.get_tenants(begin, end)
|
||||
return tenants
|
||||
|
||||
@wsme_pecan.wsexpose(decimal.Decimal,
|
||||
datetime.datetime,
|
||||
datetime.datetime,
|
||||
wtypes.text)
|
||||
def total(self, begin=None, end=None, tenant_id=None):
|
||||
"""Return the amount to pay for a given period.
|
||||
|
||||
"""
|
||||
storage = pecan.request.storage_backend
|
||||
# FIXME(sheeprine): We should filter on user id.
|
||||
# Use keystone token information by default but make it overridable and
|
||||
# enforce it by policy engine
|
||||
total = storage.get_total(begin, end, tenant_id)
|
||||
return total
|
74
cloudkitty/api/v1/controllers/storage.py
Normal file
74
cloudkitty/api/v1/controllers/storage.py
Normal file
@ -0,0 +1,74 @@
|
||||
# -*- 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 datetime
|
||||
import decimal
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from cloudkitty.api.v1.datamodels import storage as storage_models
|
||||
from cloudkitty import storage as ck_storage
|
||||
from cloudkitty import utils as ck_utils
|
||||
|
||||
|
||||
class StorageController(rest.RestController):
|
||||
"""REST Controller to access stored data frames."""
|
||||
|
||||
@wsme_pecan.wsexpose([storage_models.DataFrame],
|
||||
datetime.datetime,
|
||||
datetime.datetime,
|
||||
wtypes.text)
|
||||
def get_all(self, begin, end, tenant_id=None):
|
||||
"""Return a list of rated resources for a time period and a tenant.
|
||||
|
||||
:param begin: Start of the period
|
||||
:param end: End of the period
|
||||
:return: List of RatedResource objects.
|
||||
"""
|
||||
|
||||
begin_ts = ck_utils.dt2ts(begin)
|
||||
end_ts = ck_utils.dt2ts(end)
|
||||
backend = pecan.request.storage_backend
|
||||
try:
|
||||
frames = backend.get_time_frame(begin_ts, end_ts,
|
||||
tenant_id=tenant_id)
|
||||
except ck_storage.NoTimeFrame:
|
||||
return []
|
||||
|
||||
ret = []
|
||||
for frame in frames:
|
||||
for service, data_list in frame['usage'].items():
|
||||
resources = []
|
||||
for data in data_list:
|
||||
desc = data['desc'] if data['desc'] else {}
|
||||
price = decimal.Decimal(data['billing']['price'])
|
||||
resource = storage_models.RatedResource(
|
||||
service=service,
|
||||
desc=desc,
|
||||
volume=data['vol']['qty'],
|
||||
billing=price)
|
||||
resources.append(resource)
|
||||
data_frame = storage_models.DataFrame(
|
||||
begin=ck_utils.iso2dt(frame['period']['begin']),
|
||||
end=ck_utils.iso2dt(frame['period']['end']),
|
||||
tenant_id=tenant_id, # FIXME
|
||||
resources=resources)
|
||||
ret.append(data_frame)
|
||||
return ret
|
0
cloudkitty/api/v1/datamodels/__init__.py
Normal file
0
cloudkitty/api/v1/datamodels/__init__.py
Normal file
101
cloudkitty/api/v1/datamodels/billing.py
Normal file
101
cloudkitty/api/v1/datamodels/billing.py
Normal file
@ -0,0 +1,101 @@
|
||||
|
||||
# -*- 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 decimal
|
||||
|
||||
from oslo.config import cfg
|
||||
from wsme import types as wtypes
|
||||
|
||||
from cloudkitty.api.v1 import types as cktypes
|
||||
from cloudkitty import config # noqa
|
||||
|
||||
CONF = cfg.CONF
|
||||
CLOUDKITTY_SERVICES = wtypes.Enum(wtypes.text,
|
||||
*CONF.collect.services)
|
||||
|
||||
|
||||
class CloudkittyResource(wtypes.Base):
|
||||
"""Type describing a resource in CloudKitty.
|
||||
|
||||
"""
|
||||
|
||||
service = CLOUDKITTY_SERVICES
|
||||
"""Name of the service."""
|
||||
|
||||
# FIXME(sheeprine): values should be dynamic
|
||||
# Testing with ironic dynamic type
|
||||
desc = {wtypes.text: cktypes.MultiType(wtypes.text, int, float, dict)}
|
||||
"""Description of the resources parameters."""
|
||||
|
||||
volume = decimal.Decimal
|
||||
"""Volume of resources."""
|
||||
|
||||
def to_json(self):
|
||||
res_dict = {}
|
||||
res_dict[self.service] = [{'desc': self.desc,
|
||||
'vol': {'qty': self.volume,
|
||||
'unit': 'undef'}
|
||||
}]
|
||||
return res_dict
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(service='compute',
|
||||
desc={
|
||||
'image_id': 'a41fba37-2429-4f15-aa00-b5bc4bf557bf'
|
||||
},
|
||||
volume=decimal.Decimal(1))
|
||||
return sample
|
||||
|
||||
|
||||
class CloudkittyResourceCollection(wtypes.Base):
|
||||
"""A list of CloudKittyResources."""
|
||||
|
||||
resources = [CloudkittyResource]
|
||||
|
||||
|
||||
class CloudkittyModule(wtypes.Base):
|
||||
"""A billing extension summary
|
||||
|
||||
"""
|
||||
|
||||
module_id = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||
"""Name of the extension."""
|
||||
|
||||
description = wtypes.wsattr(wtypes.text, mandatory=False)
|
||||
"""Short description of the extension."""
|
||||
|
||||
enabled = wtypes.wsattr(bool, default=False)
|
||||
"""Extension status."""
|
||||
|
||||
hot_config = wtypes.wsattr(bool, default=False, name='hot-config')
|
||||
"""On-the-fly configuration support."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(name='example',
|
||||
description='Sample extension.',
|
||||
enabled=True,
|
||||
hot_config=False)
|
||||
return sample
|
||||
|
||||
|
||||
class CloudkittyModuleCollection(wtypes.Base):
|
||||
"""A list of billing extensions."""
|
||||
|
||||
modules = [CloudkittyModule]
|
41
cloudkitty/api/v1/datamodels/collector.py
Normal file
41
cloudkitty/api/v1/datamodels/collector.py
Normal file
@ -0,0 +1,41 @@
|
||||
# -*- 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
|
||||
#
|
||||
from wsme import types as wtypes
|
||||
|
||||
|
||||
class ServiceToCollectorMapping(wtypes.Base):
|
||||
"""Type describing a service to collector mapping.
|
||||
|
||||
"""
|
||||
|
||||
service = wtypes.text
|
||||
"""Name of the service."""
|
||||
|
||||
collector = wtypes.text
|
||||
"""Name of the collector."""
|
||||
|
||||
def to_json(self):
|
||||
res_dict = {}
|
||||
res_dict[self.service] = self.collector
|
||||
return res_dict
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(service='compute',
|
||||
collector='ceilometer')
|
||||
return sample
|
56
cloudkitty/api/v1/datamodels/storage.py
Normal file
56
cloudkitty/api/v1/datamodels/storage.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- 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 datetime
|
||||
import decimal
|
||||
|
||||
from wsme import types as wtypes
|
||||
|
||||
from cloudkitty.api.v1.datamodels import billing as billing_resources
|
||||
|
||||
|
||||
class RatedResource(billing_resources.CloudkittyResource):
|
||||
"""Represents a rated CloudKitty resource."""
|
||||
|
||||
billing = decimal.Decimal
|
||||
|
||||
def to_json(self):
|
||||
res_dict = super(RatedResource, self).to_json()
|
||||
res_dict['billing'] = self.billing
|
||||
return res_dict
|
||||
|
||||
|
||||
class DataFrame(wtypes.Base):
|
||||
"""Type describing a stored dataframe."""
|
||||
|
||||
begin = datetime.datetime
|
||||
"""Begin date for the sample."""
|
||||
|
||||
end = datetime.datetime
|
||||
"""End date for the sample."""
|
||||
|
||||
tenant_id = wtypes.text
|
||||
"""Tenant owner of the sample."""
|
||||
|
||||
resources = [RatedResource]
|
||||
"""A resource list."""
|
||||
|
||||
def to_json(self):
|
||||
return {'begin': self.begin,
|
||||
'end': self.end,
|
||||
'tenant_id': self.tenant_id,
|
||||
'resources': self.resources}
|
@ -17,148 +17,34 @@
|
||||
#
|
||||
import abc
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
import six
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from cloudkitty.db import api as db_api
|
||||
|
||||
|
||||
class BillingModuleNotConfigurable(Exception):
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
super(BillingModuleNotConfigurable, self).__init__(
|
||||
'Module %s not configurable.' % module)
|
||||
|
||||
|
||||
class ExtensionSummary(wtypes.Base):
|
||||
"""A billing extension summary
|
||||
|
||||
"""
|
||||
|
||||
name = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||
"""Name of the extension."""
|
||||
|
||||
description = wtypes.text
|
||||
"""Short description of the extension."""
|
||||
|
||||
enabled = wtypes.wsattr(bool, default=False)
|
||||
"""Extension status."""
|
||||
|
||||
hot_config = wtypes.wsattr(bool, default=False, name='hot-config')
|
||||
"""On-the-fly configuration support."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(name='example',
|
||||
description='Sample extension.',
|
||||
enabled=True,
|
||||
hot_config=False)
|
||||
return sample
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BillingEnableController(rest.RestController):
|
||||
"""REST Controller to enable or disable a billing module.
|
||||
|
||||
"""
|
||||
|
||||
@wsme_pecan.wsexpose(bool)
|
||||
def get(self):
|
||||
"""Get module status
|
||||
|
||||
"""
|
||||
api = db_api.get_instance()
|
||||
module_db = api.get_module_enable_state()
|
||||
return module_db.get_state(self.module_name) or False
|
||||
|
||||
@wsme_pecan.wsexpose(bool, body=bool)
|
||||
def put(self, state):
|
||||
"""Set module status
|
||||
|
||||
:param state: State to set.
|
||||
:return: New state set for the module.
|
||||
"""
|
||||
api = db_api.get_instance()
|
||||
module_db = api.get_module_enable_state()
|
||||
client = pecan.request.rpc_client.prepare(namespace='billing',
|
||||
fanout=True)
|
||||
if state:
|
||||
operation = 'enable_module'
|
||||
else:
|
||||
operation = 'disable_module'
|
||||
client.cast({}, operation, name=self.module_name)
|
||||
return module_db.set_state(self.module_name, state)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BillingConfigController(rest.RestController):
|
||||
"""REST Controller managing internal configuration of billing modules.
|
||||
|
||||
"""
|
||||
|
||||
def notify_reload(self):
|
||||
client = pecan.request.rpc_client.prepare(namespace='billing',
|
||||
fanout=True)
|
||||
client.cast({}, 'reload_module', name=self.module_name)
|
||||
|
||||
def _not_configurable(self):
|
||||
try:
|
||||
raise BillingModuleNotConfigurable(self.module_name)
|
||||
except BillingModuleNotConfigurable as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
@wsme_pecan.wsexpose()
|
||||
def get(self):
|
||||
"""Get current module configuration
|
||||
|
||||
"""
|
||||
self._not_configurable()
|
||||
|
||||
@wsme_pecan.wsexpose()
|
||||
def put(self):
|
||||
"""Set current module configuration
|
||||
|
||||
"""
|
||||
self._not_configurable()
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BillingController(rest.RestController):
|
||||
"""REST Controller used to manage billing system.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
if not hasattr(self, 'config'):
|
||||
self.config = BillingConfigController()
|
||||
if not hasattr(self, 'enabled'):
|
||||
self.enabled = BillingEnableController()
|
||||
if hasattr(self, 'module_name'):
|
||||
self.config.module_name = self.module_name
|
||||
self.enabled.module_name = self.module_name
|
||||
|
||||
@wsme_pecan.wsexpose(ExtensionSummary)
|
||||
def get_all(self):
|
||||
"""Get extension summary.
|
||||
|
||||
"""
|
||||
extension_summary = ExtensionSummary(**self.get_module_info())
|
||||
return extension_summary
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_module_info(self):
|
||||
"""Get module informations
|
||||
|
||||
"""
|
||||
from cloudkitty import rpc
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BillingProcessorBase(object):
|
||||
"""Provides the Cloudkitty integration code to the billing processors.
|
||||
|
||||
controller = BillingController
|
||||
Every billing processor shoud sublclass this and override at least
|
||||
module_name, description.
|
||||
|
||||
config_controller can be left at None to use the default one.
|
||||
"""
|
||||
|
||||
module_name = None
|
||||
description = None
|
||||
config_controller = None
|
||||
hot_config = False
|
||||
|
||||
@property
|
||||
def module_info(self):
|
||||
return {
|
||||
'name': self.module_name,
|
||||
'description': self.description,
|
||||
'hot_config': self.hot_config,
|
||||
'enabled': self.enabled, }
|
||||
|
||||
def __init__(self, tenant_id=None):
|
||||
self._tenant_id = tenant_id
|
||||
@ -170,11 +56,22 @@ class BillingProcessorBase(object):
|
||||
:returns: bool if module is enabled
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def reload_config(self):
|
||||
"""Trigger configuration reload
|
||||
def set_state(self, enabled):
|
||||
"""Enable or disable a module
|
||||
|
||||
:param enabled: (bool) The state to put the module in.
|
||||
:return: bool
|
||||
"""
|
||||
api = db_api.get_instance()
|
||||
module_db = api.get_module_enable_state()
|
||||
client = rpc.get_client().prepare(namespace='billing',
|
||||
fanout=True)
|
||||
if enabled:
|
||||
operation = 'enable_module'
|
||||
else:
|
||||
operation = 'disable_module'
|
||||
client.cast({}, operation, name=self.module_name)
|
||||
return module_db.set_state(self.module_name, enabled)
|
||||
|
||||
@abc.abstractmethod
|
||||
def process(self, data):
|
||||
@ -184,3 +81,14 @@ class BillingProcessorBase(object):
|
||||
resources.
|
||||
:type data: dict(str:?)
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def reload_config(self):
|
||||
"""Trigger configuration reload
|
||||
|
||||
"""
|
||||
|
||||
def notify_reload(self):
|
||||
client = rpc.get_rpc_client().prepare(namespace='billing',
|
||||
fanout=True)
|
||||
client.cast({}, 'reload_module', name=self.module_name)
|
||||
|
@ -16,6 +16,7 @@
|
||||
# @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
|
||||
@ -44,7 +45,19 @@ class Mapping(wtypes.Base):
|
||||
return sample
|
||||
|
||||
|
||||
class BasicHashMapConfigController(billing.BillingConfigController):
|
||||
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):
|
||||
@ -194,37 +207,13 @@ class BasicHashMapConfigController(billing.BillingConfigController):
|
||||
pecan.response.status = 204
|
||||
|
||||
|
||||
class BasicHashMapController(billing.BillingController):
|
||||
|
||||
module_name = 'hashmap'
|
||||
|
||||
_custom_actions = {
|
||||
'types': ['GET']
|
||||
}
|
||||
|
||||
config = BasicHashMapConfigController()
|
||||
|
||||
def get_module_info(self):
|
||||
module = BasicHashMap()
|
||||
infos = {
|
||||
'name': self.module_name,
|
||||
'description': 'Basic hashmap billing module.',
|
||||
'enabled': module.enabled,
|
||||
'hot_config': True,
|
||||
}
|
||||
return infos
|
||||
|
||||
@wsme_pecan.wsexpose([wtypes.text])
|
||||
def get_types(self):
|
||||
"""Return the list of every mapping type available.
|
||||
|
||||
"""
|
||||
return MAP_TYPE.values
|
||||
|
||||
|
||||
class BasicHashMap(billing.BillingProcessorBase):
|
||||
|
||||
controller = BasicHashMapController
|
||||
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):
|
||||
|
@ -18,27 +18,10 @@
|
||||
from cloudkitty import billing
|
||||
|
||||
|
||||
class NoopController(billing.BillingController):
|
||||
|
||||
module_name = 'noop'
|
||||
|
||||
def get_module_info(self):
|
||||
module = Noop()
|
||||
infos = {
|
||||
'name': self.module_name,
|
||||
'description': 'Dummy test module.',
|
||||
'enabled': module.enabled,
|
||||
'hot_config': False,
|
||||
}
|
||||
return infos
|
||||
|
||||
|
||||
class Noop(billing.BillingProcessorBase):
|
||||
|
||||
controller = NoopController
|
||||
|
||||
def __init__(self, tenant_id=None):
|
||||
super(Noop, self).__init__(tenant_id)
|
||||
module_name = "noop"
|
||||
description = 'Dummy test module.'
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
|
38
cloudkitty/rpc.py
Normal file
38
cloudkitty/rpc.py
Normal file
@ -0,0 +1,38 @@
|
||||
# -*- 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: Guillaume Espanel
|
||||
#
|
||||
from oslo import messaging
|
||||
|
||||
from cloudkitty.common import rpc
|
||||
|
||||
|
||||
_RPC_CLIENT = None
|
||||
_RPC_TARGET = None
|
||||
|
||||
|
||||
def get_target():
|
||||
global _RPC_TARGET
|
||||
if _RPC_TARGET is None:
|
||||
_RPC_TARGET = messaging.Target(topic='cloudkitty', version=1.0)
|
||||
return _RPC_TARGET
|
||||
|
||||
|
||||
def get_client():
|
||||
global _RPC_CLIENT
|
||||
if _RPC_CLIENT is None:
|
||||
_RPC_CLIENT = rpc.get_client(get_target())
|
||||
return _RPC_CLIENT
|
@ -2,11 +2,8 @@
|
||||
HashMap Module REST API
|
||||
=======================
|
||||
|
||||
.. rest-controller:: cloudkitty.billing.hash:BasicHashMapController
|
||||
:webprefix: /v1/billing/modules/hashmap
|
||||
|
||||
.. rest-controller:: cloudkitty.billing.hash:BasicHashMapConfigController
|
||||
:webprefix: /v1/billing/modules/hashmap/config
|
||||
:webprefix: /v1/billing/module_config/hashmap
|
||||
|
||||
.. http:get:: /v1/billing/hashmap/modules/config/(service)/(field)/(key)
|
||||
|
||||
|
@ -2,15 +2,15 @@
|
||||
CloudKitty REST API (root)
|
||||
==========================
|
||||
|
||||
.. rest-controller:: cloudkitty.api.controllers.root:RootController
|
||||
.. rest-controller:: cloudkitty.api.root:RootController
|
||||
:webprefix: / /
|
||||
.. Dirty hack till the bug is fixed so we can specify root path
|
||||
|
||||
.. autotype:: cloudkitty.api.controllers.root.APILink
|
||||
.. autotype:: cloudkitty.api.root.APILink
|
||||
:members:
|
||||
|
||||
.. autotype:: cloudkitty.api.controllers.root.APIMediaType
|
||||
.. autotype:: cloudkitty.api.root.APIMediaType
|
||||
:members:
|
||||
|
||||
.. autotype:: cloudkitty.api.controllers.root.APIVersion
|
||||
.. autotype:: cloudkitty.api.root.APIVersion
|
||||
:members:
|
||||
|
@ -5,53 +5,50 @@ CloudKitty REST API (v1)
|
||||
Collector
|
||||
=========
|
||||
|
||||
.. rest-controller:: cloudkitty.api.controllers.v1:CollectorController
|
||||
.. rest-controller:: cloudkitty.api.v1.controllers.collector:CollectorController
|
||||
:webprefix: /v1/collector
|
||||
|
||||
.. rest-controller:: cloudkitty.api.controllers.v1:MappingController
|
||||
.. rest-controller:: cloudkitty.api.v1.controllers.collector:MappingController
|
||||
:webprefix: /v1/collector/mapping
|
||||
|
||||
.. autotype:: cloudkitty.api.controllers.v1.MetricToCollectorMapping
|
||||
:members:
|
||||
|
||||
|
||||
Billing
|
||||
=======
|
||||
|
||||
.. rest-controller:: cloudkitty.billing:BillingEnableController
|
||||
:webprefix: /v1/billing/modules/(module)/enabled
|
||||
|
||||
.. rest-controller:: cloudkitty.billing:BillingConfigController
|
||||
:webprefix: /v1/billing/modules/(module)/config
|
||||
|
||||
.. rest-controller:: cloudkitty.api.controllers.v1:ModulesController
|
||||
.. rest-controller:: cloudkitty.api.v1.controllers.billing:ModulesController
|
||||
:webprefix: /v1/billing/modules
|
||||
|
||||
.. rest-controller:: cloudkitty.api.controllers.v1:BillingController
|
||||
.. rest-controller:: cloudkitty.api.v1.controllers.billing:BillingController
|
||||
:webprefix: /v1/billing
|
||||
|
||||
.. autotype:: cloudkitty.billing.ExtensionSummary
|
||||
.. autotype:: cloudkitty.api.v1.datamodels.billing.CloudkittyModule
|
||||
:members:
|
||||
|
||||
.. autotype:: cloudkitty.api.controllers.v1.ResourceDescriptor
|
||||
.. autotype:: cloudkitty.api.v1.datamodels.billing.CloudkittyModuleCollection
|
||||
:members:
|
||||
|
||||
.. autotype:: cloudkitty.api.v1.datamodels.billing.CloudkittyResource
|
||||
:members:
|
||||
|
||||
.. autotype:: cloudkitty.api.v1.datamodels.billing.CloudkittyResourceCollection
|
||||
:members:
|
||||
|
||||
|
||||
Report
|
||||
======
|
||||
|
||||
.. rest-controller:: cloudkitty.api.controllers.v1:ReportController
|
||||
.. rest-controller:: cloudkitty.api.v1.controllers.report:ReportController
|
||||
:webprefix: /v1/report
|
||||
|
||||
|
||||
Storage
|
||||
=======
|
||||
|
||||
.. rest-controller:: cloudkitty.api.controllers.v1:StorageController
|
||||
.. rest-controller:: cloudkitty.api.v1.controllers.storage:StorageController
|
||||
:webprefix: /v1/storage
|
||||
|
||||
.. autotype:: cloudkitty.api.controllers.v1.DataFrame
|
||||
.. autotype:: cloudkitty.api.v1.datamodels.storage.DataFrame
|
||||
:members:
|
||||
|
||||
.. autotype:: cloudkitty.api.controllers.v1.RatedResource
|
||||
.. autotype:: cloudkitty.api.v1.datamodels.storage.RatedResource
|
||||
:members:
|
||||
|
Loading…
Reference in New Issue
Block a user