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:
Stéphane Albert 2015-03-02 16:53:42 +01:00 committed by Guillaume Espanel
parent 987ac72a7a
commit 1f21760064
22 changed files with 745 additions and 593 deletions

View File

@ -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()

View File

@ -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',

View File

@ -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()

View File

@ -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',

View 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()

View 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

View 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)

View 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

View 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

View File

View 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]

View 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

View 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}

View File

@ -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)

View File

@ -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):

View File

@ -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
View 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

View File

@ -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)

View File

@ -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:

View File

@ -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: