Added API service

Change-Id: I073d586120e5970f150a45db77ab85409c7087fc
This commit is contained in:
Stéphane Albert 2014-08-08 15:39:45 +02:00
parent 57f0c65d6c
commit 2efb97e9f0
14 changed files with 578 additions and 1 deletions

View File

99
cloudkitty/api/app.py Normal file
View File

@ -0,0 +1,99 @@
# -*- 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 os
from wsgiref import simple_server
from oslo.config import cfg
from paste import deploy
import pecan
from cloudkitty.api import config as api_config
from cloudkitty import config # noqa
from cloudkitty.openstack.common import log
LOG = log.getLogger(__name__)
auth_opts = [
cfg.StrOpt('api_paste_config',
default="api_paste.ini",
help="Configuration file for WSGI definition of API."
),
]
api_opts = [
cfg.StrOpt('host_ip',
default="0.0.0.0",
help="Host serving the API."
),
cfg.IntOpt('port',
default=8888,
help="Host port serving the API."
),
]
CONF = cfg.CONF
CONF.register_opts(auth_opts)
CONF.register_opts(api_opts, group='api')
def get_pecan_config():
# Set up the pecan configuration
filename = api_config.__file__.replace('.pyc', '.py')
return pecan.configuration.conf_from_file(filename)
def setup_app(pecan_config=None, extra_hooks=None):
app_conf = get_pecan_config()
return pecan.make_app(
app_conf.app.root,
static_root=app_conf.app.static_root,
template_path=app_conf.app.template_path,
debug=CONF.debug,
force_canonical=getattr(app_conf.app, 'force_canonical', True),
guess_content_type_from_ext=False
)
def setup_wsgi():
cfg_file = cfg.CONF.api_paste_config
if not os.path.exists(cfg_file):
raise Exception('api_paste_config file not found')
return deploy.loadapp("config:" + cfg_file)
def build_server():
# Create the WSGI server and start it
host = CONF.api.host_ip
port = CONF.api.port
server_cls = simple_server.WSGIServer
handler_cls = simple_server.WSGIRequestHandler
app = setup_app()
srv = simple_server.make_server(
host,
port,
app,
server_cls,
handler_cls)
return srv

30
cloudkitty/api/config.py Normal file
View File

@ -0,0 +1,30 @@
# 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.
from oslo.config import cfg
from cloudkitty import config # noqa
# Pecan Application Configurations
app = {
'root': 'cloudkitty.api.controllers.root.RootController',
'modules': ['cloudkitty.api'],
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/templates',
'debug': False,
'enable_acl': True,
'acl_public_routes': ['/', '/v1'],
}
wsme = {
'debug': cfg.CONF.debug,
}

View File

View File

@ -0,0 +1,84 @@
# -*- 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.controllers import v1
from cloudkitty.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class APILink(wtypes.Base):
type = wtypes.text
rel = wtypes.text
href = wtypes.text
class APIMediaType(wtypes.Base):
base = wtypes.text
type = wtypes.text
class APIVersion(wtypes.Base):
id = wtypes.text
status = wtypes.text
links = [APILink]
media_types = [APIMediaType]
class RootController(rest.RestController):
v1 = v1.V1Controller()
@wsme_pecan.wsexpose([APIVersion])
def get(self):
# TODO(sheeprine): Maybe we should store all the API version
# informations in every API modules
ver1 = APIVersion(
id='v1',
status='EXPERIMENTAL',
updated='2014-06-02T00:00:00Z',
links=[
APILink(
rel='self',
href='{scheme}://{host}/v1'.format(
scheme=pecan.request.scheme,
host=pecan.request.host
)
)
],
media_types=[]
)
versions = []
versions.append(ver1)
return versions

View File

@ -0,0 +1,47 @@
# -*- 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
from cloudkitty.i18n import _LE # noqa
# Code taken from ironic types
class MultiType(wtypes.UserType):
"""A complex type that represents one or more types.
Used for validating that a value is an instance of one of the types.
:param *types: Variable-length list of types.
"""
def __init__(self, *types):
self.types = types
def __str__(self):
return ' | '.join(map(str, self.types))
def validate(self, value):
for t in self.types:
if t is wtypes.text and isinstance(value, wtypes.bytes):
value = value.decode()
if isinstance(value, t):
return value
else:
raise ValueError(
_LE("Wrong type. Expected '%(type)s', got '%(value)s'")
% {'type': self.types, 'value': type(value)})

View File

@ -0,0 +1,132 @@
# -*- 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
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.openstack.common import log as logging
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
CLOUDKITTY_SERVICES = wtypes.Enum(wtypes.text,
*CONF.collect.services)
class ResourceDescriptor(wtypes.Base):
service = CLOUDKITTY_SERVICES
# FIXME(sheeprine): values should be dynamic
# Testing with ironic dynamic type
desc = {wtypes.text: cktypes.MultiType(wtypes.text, int, float, dict)}
volume = int
def to_json(self):
res_dict = {}
res_dict[self.service] = [{'desc': self.desc,
'vol': {'qty': self.volume,
'unit': 'undef'}
}]
return res_dict
class ModulesController(rest.RestController):
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:
if not hasattr(self, ext.name):
setattr(self, ext.name, ext.obj.controller())
@wsme_pecan.wsexpose([wtypes.text])
def get(self):
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):
# TODO(sheeprine): Send RPC request for quote
from cloudkitty import extension_manager
b_processors = {}
processors = extension_manager.EnabledExtensionManager(
'cloudkitty.billing.processors',
)
for processor in processors:
b_name = processor.name
b_obj = processor.obj
b_processors[b_name] = b_obj
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])
for processor in b_processors.values():
processor.process([{'usage': res_dict}])
price = 0.0
for res in res_dict.values():
for data in res:
price += data.get('billing', {}).get('price', 0.0)
return price
class ReportController(rest.RestController):
_custom_actions = {
'total': ['GET']
}
@wsme_pecan.wsexpose(float)
def total(self):
# TODO(sheeprine): Get current total from DB
return 10.0
class V1Controller(rest.RestController):
billing = BillingController()
report = ReportController()

View File

@ -17,12 +17,92 @@
#
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)
description = wtypes.text
enabled = wtypes.wsattr(bool, default=False)
hot_config = wtypes.wsattr(bool, default=False, name='hot-config')
@six.add_metaclass(abc.ABCMeta)
class BillingEnableController(rest.RestController):
@wsme_pecan.wsexpose(bool)
def get(self):
api = db_api.get_instance()
module = pecan.request.path.rsplit('/', 2)[-2]
module_db = api.get_module_enable_state()
return module_db.get_state(module) or False
@wsme_pecan.wsexpose(bool, body=bool)
def put(self, state):
api = db_api.get_instance()
module = pecan.request.path.rsplit('/', 2)[-2]
module_db = api.get_module_enable_state()
return module_db.set_state(module, state)
@six.add_metaclass(abc.ABCMeta)
class BillingConfigController(rest.RestController):
@wsme_pecan.wsexpose()
def get(self):
try:
module = pecan.request.path.rsplit('/', 1)[-1]
raise BillingModuleNotConfigurable(module)
except BillingModuleNotConfigurable as e:
pecan.abort(400, str(e))
@six.add_metaclass(abc.ABCMeta)
class BillingController(rest.RestController):
config = BillingConfigController()
enabled = BillingEnableController()
@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
"""
@six.add_metaclass(abc.ABCMeta)
class BillingProcessorBase(object):
controller = BillingController
def __init__(self):
pass
@ -33,6 +113,12 @@ class BillingProcessorBase(object):
:returns: bool if module is enabled
"""
@abc.abstractmethod
def reload_config(self):
"""Trigger configuration reload
"""
@abc.abstractmethod
def process(self, data):
"""Add billing informations to data

View File

@ -20,7 +20,23 @@ import json
from cloudkitty import billing
class BasicHashMapController(billing.BillingController):
def get_module_info(self):
module = BasicHashMap()
infos = {
'name': 'hashmap',
'description': 'Basic hashmap billing module.',
'enabled': module.enabled,
'hot_config': True,
}
return infos
class BasicHashMap(billing.BillingProcessorBase):
controller = BasicHashMapController
def __init__(self):
self._billing_info = {}
self._load_billing_rates()

View File

@ -18,19 +18,42 @@
from cloudkitty import billing
class NoopController(billing.BillingController):
def get_module_info(self):
module = Noop()
infos = {
'name': 'noop',
'description': 'Dummy test module.',
'enabled': module.enabled,
'hot_config': False,
}
return infos
class Noop(billing.BillingProcessorBase):
controller = NoopController
def __init__(self):
pass
@property
def enabled(self):
"""Check if the module is enabled
:returns: bool if module is enabled
"""
return True
def reload_config(self):
pass
def process(self, data):
for cur_data in data:
cur_usage = cur_data['usage']
for service in cur_usage:
for entry in cur_usage[service]:
if 'billing' not in entry:
entry['billing'] = {}
entry['billing'] = {'price': 0}
return data

View File

37
cloudkitty/cli/api.py Normal file
View File

@ -0,0 +1,37 @@
# -*- 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 sys
from oslo.config import cfg
from cloudkitty.api import app
from cloudkitty.openstack.common import log as logging
def main():
cfg.CONF(sys.argv[1:], project='cloudkitty')
logging.setup('cloudkitty')
server = app.build_server()
try:
server.serve_forever()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
main()

View File

@ -194,6 +194,15 @@
#control_exchange=openstack
#
# Options defined in cloudkitty.api.app
#
# Configuration file for WSGI definition of API. (string
# value)
#api_paste_config=api_paste.ini
#
# Options defined in cloudkitty.openstack.common.log
#
@ -288,6 +297,19 @@
#syslog_log_facility=LOG_USER
[api]
#
# Options defined in cloudkitty.api.app
#
# Host serving the API. (string value)
#host_ip=0.0.0.0
# Host port serving the API. (integer value)
#port=8888
[auth]
#

View File

@ -20,6 +20,7 @@ packages =
[entry_points]
console_scripts =
cloudkitty-api = cloudkitty.cli.api:main
cloudkitty-processor = cloudkitty.orchestrator:main
cloudkitty.collector.backends =