Added API service
Change-Id: I073d586120e5970f150a45db77ab85409c7087fc
This commit is contained in:
parent
57f0c65d6c
commit
2efb97e9f0
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -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
|
|
@ -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)})
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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]
|
||||
|
||||
#
|
||||
|
|
Loading…
Reference in New Issue