diff --git a/cloudkitty/api/controllers/root.py b/cloudkitty/api/controllers/root.py index 2269f631..45d4ee25 100644 --- a/cloudkitty/api/controllers/root.py +++ b/cloudkitty/api/controllers/root.py @@ -28,50 +28,111 @@ LOG = logging.getLogger(__name__) class APILink(wtypes.Base): + """API link description. + + """ type = wtypes.text + """Type of link.""" rel = wtypes.text + """Relationship with this link.""" href = wtypes.text + """URL of the link.""" + + @classmethod + def sample(cls): + version = 'v1' + sample = cls( + rel='self', + type='text/html', + href='http://127.0.0.1:8888/{id}'.format( + id=version)) + return sample class APIMediaType(wtypes.Base): + """Media type description. + + """ base = wtypes.text + """Base type of this media type.""" type = wtypes.text + """Type of this media type.""" + + @classmethod + def sample(cls): + sample = cls( + base='application/json', + type='application/vnd.openstack.cloudkitty-v1+json') + return sample + + +VERSION_STATUS = wtypes.Enum(wtypes.text, 'EXPERIMENTAL', 'STABLE') class APIVersion(wtypes.Base): + """API Version description. + + """ id = wtypes.text + """ID of the version.""" - status = wtypes.text + status = VERSION_STATUS + """Status of the version.""" + + updated = wtypes.text + "Last update in iso8601 format." links = [APILink] + """List of links to API resources.""" media_types = [APIMediaType] + """Types accepted by this API.""" + + @classmethod + def sample(cls): + version = 'v1' + updated = '2014-08-11T16:00:00Z' + links = [APILink.sample()] + media_types = [APIMediaType.sample()] + sample = cls(id=version, + status='STABLE', + updated=updated, + links=links, + media_types=media_types) + return sample class RootController(rest.RestController): + """Root REST Controller exposing versions of the API. + + """ v1 = v1.V1Controller() @wsme_pecan.wsexpose([APIVersion]) def get(self): + """Return the version list + + """ # 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', + updated='2014-08-11T16:00:00Z', links=[ APILink( rel='self', - href='{scheme}://{host}/v1'.format( + href='{scheme}://{host}:{port}/v1'.format( scheme=pecan.request.scheme, - host=pecan.request.host + host=pecan.request.host, + port=pecan.request.port ) ) ], diff --git a/cloudkitty/api/controllers/v1.py b/cloudkitty/api/controllers/v1.py index 0b2fb6e8..f1b14130 100644 --- a/cloudkitty/api/controllers/v1.py +++ b/cloudkitty/api/controllers/v1.py @@ -33,14 +33,20 @@ CLOUDKITTY_SERVICES = wtypes.Enum(wtypes.text, 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 = int + """Number of resources.""" def to_json(self): res_dict = {} @@ -50,8 +56,20 @@ class ResourceDescriptor(wtypes.Base): }] return res_dict + @classmethod + def sample(cls): + sample = cls(service='compute', + desc={ + 'image_id': 'a41fba37-2429-4f15-aa00-b5bc4bf557bf' + }, + volume=1) + return sample + class ModulesController(rest.RestController): + """REST Controller managing billing modules. + + """ def __init__(self): self.extensions = extension.ExtensionManager( @@ -72,6 +90,10 @@ class ModulesController(rest.RestController): @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()] @@ -85,6 +107,12 @@ class BillingController(rest.RestController): @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. + """ + # TODO(sheeprine): Send RPC request for quote from cloudkitty import extension_manager b_processors = {} @@ -115,6 +143,9 @@ class BillingController(rest.RestController): class ReportController(rest.RestController): + """REST Controller managing the reporting. + + """ _custom_actions = { 'total': ['GET'] @@ -122,11 +153,17 @@ class ReportController(rest.RestController): @wsme_pecan.wsexpose(float) def total(self): + """Return the amount to pay for the current month. + + """ # TODO(sheeprine): Get current total from DB return 10.0 class V1Controller(rest.RestController): + """API version 1 controller. + + """ billing = BillingController() report = ReportController() diff --git a/cloudkitty/billing/__init__.py b/cloudkitty/billing/__init__.py index 37bd21f0..6d3dafd8 100644 --- a/cloudkitty/billing/__init__.py +++ b/cloudkitty/billing/__init__.py @@ -39,19 +39,37 @@ class ExtensionSummary(wtypes.Base): """ 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 = pecan.request.path.rsplit('/', 2)[-2] module_db = api.get_module_enable_state() @@ -59,6 +77,11 @@ class BillingEnableController(rest.RestController): @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 = pecan.request.path.rsplit('/', 2)[-2] module_db = api.get_module_enable_state() @@ -67,18 +90,37 @@ class BillingEnableController(rest.RestController): @six.add_metaclass(abc.ABCMeta) class BillingConfigController(rest.RestController): + """REST Controller managing internal configuration of billing modules. - @wsme_pecan.wsexpose() - def get(self): + """ + + def _not_configurable(self): try: module = pecan.request.path.rsplit('/', 1)[-1] raise BillingModuleNotConfigurable(module) 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. + + """ config = BillingConfigController() enabled = BillingEnableController() diff --git a/cloudkitty/billing/hash/__init__.py b/cloudkitty/billing/hash/__init__.py index 61fdab34..b50433d9 100644 --- a/cloudkitty/billing/hash/__init__.py +++ b/cloudkitty/billing/hash/__init__.py @@ -34,8 +34,15 @@ MAP_TYPE = wtypes.Enum(wtypes.text, 'flat', 'rate') class Mapping(wtypes.Base): map_type = wtypes.wsattr(MAP_TYPE, default='rate', name='type') + """Type of the mapping.""" value = wtypes.wsattr(float, mandatory=True) + """Value of the mapping.""" + + @classmethod + def sample(cls): + sample = cls(value=4.2) + return sample class BasicHashMapConfigController(billing.BillingConfigController): @@ -56,7 +63,7 @@ class BasicHashMapConfigController(billing.BillingConfigController): @wsme_pecan.wsexpose(Mapping, wtypes.text, wtypes.text, wtypes.text) def get_mapping(self, service, field, key): - """Return the list of every mappings. + """Get a mapping from full path. """ hashmap = api.get_instance() @@ -67,6 +74,10 @@ class BasicHashMapConfigController(billing.BillingConfigController): @wsme_pecan.wsexpose([wtypes.text]) def get(self): + """Get the service list + + :return: List of every services' name. + """ hashmap = api.get_instance() return [service.name for service in hashmap.list_services()] @@ -74,6 +85,8 @@ class BasicHashMapConfigController(billing.BillingConfigController): def get_one(self, service=None, field=None): """Return the list of every sub keys. + :param service: (Optional) Filter on this service. + :param field: (Optional) Filter on this field. """ hashmap = api.get_instance() if field: @@ -95,6 +108,13 @@ class BasicHashMapConfigController(billing.BillingConfigController): @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text, body=Mapping) def post(self, service, field=None, key=None, mapping=None): + """Create hashmap fields. + + :param service: Name of the service to create. + :param field: (Optional) Name of the field to create. + :param key: (Optional) Name of the key to create. + :param mapping: (Optional) Mapping object to create. + """ hashmap = api.get_instance() if field: if key: @@ -131,6 +151,13 @@ class BasicHashMapConfigController(billing.BillingConfigController): @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text, body=Mapping) def put(self, service, field, key, mapping): + """Modify hashmap fields + + :param service: Filter on this service. + :param field: Filter on this field. + :param key: Modify the content of this key. + :param mapping: Mapping object to update. + """ hashmap = api.get_instance() try: hashmap.update_mapping( @@ -149,6 +176,9 @@ class BasicHashMapConfigController(billing.BillingConfigController): def delete(self, service, field=None, key=None): """Delete the parent and all the sub keys recursively. + :param service: Name of the service to delete. + :param field: (Optional) Name of the field to delete. + :param key: (Optional) Name of the key to delete. """ hashmap = api.get_instance() try: diff --git a/doc/source/arch.rst b/doc/source/arch.rst new file mode 100644 index 00000000..0e077e88 --- /dev/null +++ b/doc/source/arch.rst @@ -0,0 +1,47 @@ +========================= +CloudKitty's Architecture +========================= + +CloudKitty can be cut in four big parts: + +* API +* collector +* billing processor +* writer pipeline + + +Module loading and extensions +============================= + +Nearly every part of CloudKitty makes use of stevedore to load extensions +dynamically. + +Every billing module is loaded at runtime and can be enabled/disabled directly +via CloudKitty's API. The billing module is responsible of its own API to ease +the management of its configuration. + +Collectors and writers are loaded with stevedore but configured in CloudKitty's +configuration file. + + +Collector +========= + +This part is responsible of the information gathering. It consists of a python +module that load data from a backend and return them in a format that +CloudKitty can handle. + +Processor +========= + +This is where every pricing calculations is done. The data gathered by +the collector is pushed in a pipeline of billing processors. Every +processor does its calculations and updates the data. + + +Writer +====== + +In the same way as the processor pipeline, the writing is handled with a +pipeline. The data is pushed to every writer in the pipeline which is +responsible of the writing. diff --git a/doc/source/conf.py b/doc/source/conf.py index b7ae6e3b..8ea55d8c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -12,8 +12,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys -import os +#import sys +#import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -28,7 +28,18 @@ import os # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', + 'wsmeext.sphinxext', + 'sphinxcontrib.docbookrestapi.setup', + 'sphinxcontrib.pecanwsme.rest', + 'sphinxcontrib.httpdomain', + 'oslosphinx', +] + +wsme_protocols = ['restjson', 'restxml'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -74,11 +85,11 @@ exclude_patterns = [] #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. @@ -88,7 +99,7 @@ exclude_patterns = [] pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +modindex_common_prefix = ['cloudkitty.'] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False diff --git a/doc/source/index.rst b/doc/source/index.rst index 8c82fb96..fd12f996 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,14 +3,44 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to cloudkitty's documentation! -====================================== +================================================= +Welcome to CloudKitty's developper documentation! +================================================= -Contents: +Introduction +============ + +CloudKitty is a PricingAsAService project aimed at translating Ceilometer +metrics to prices. + + +Architecture +============ .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + arch + + +API References +============== + +.. toctree:: + :maxdepth: 1 + + webapi/root + webapi/v1 + + +Modules +======= + +.. toctree:: + :maxdepth: 1 + :glob: + + webapi/billing/* Indices and tables @@ -19,4 +49,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/doc/source/webapi/billing/hashmap.rst b/doc/source/webapi/billing/hashmap.rst new file mode 100644 index 00000000..4c84c50c --- /dev/null +++ b/doc/source/webapi/billing/hashmap.rst @@ -0,0 +1,30 @@ +======================= +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 + +.. http:get:: /v1/billing/hashmap/modules/config/(service)/(field)/(key) + + Get a mapping from full path + + :param service: Filter on this service. + :param field: Filter on this field. + :param key: Filter on this key. + :type service: :class:`unicode` + :type field: :class:`unicode` + :type key: :class:`unicode` + :type mapping: :class:`Mapping` + :return: A mapping + + :return type: :class:`Mapping` + + +.. autotype:: cloudkitty.billing.hash.Mapping + :members: + + diff --git a/doc/source/webapi/root.rst b/doc/source/webapi/root.rst new file mode 100644 index 00000000..d08a2e44 --- /dev/null +++ b/doc/source/webapi/root.rst @@ -0,0 +1,16 @@ +========================== +CloudKitty REST API (root) +========================== + +.. rest-controller:: cloudkitty.api.controllers.root:RootController + :webprefix: / / +.. Dirty hack till the bug is fixed so we can specify root path + +.. autotype:: cloudkitty.api.controllers.root.APILink + :members: + +.. autotype:: cloudkitty.api.controllers.root.APIMediaType + :members: + +.. autotype:: cloudkitty.api.controllers.root.APIVersion + :members: diff --git a/doc/source/webapi/v1.rst b/doc/source/webapi/v1.rst new file mode 100644 index 00000000..34bf4fef --- /dev/null +++ b/doc/source/webapi/v1.rst @@ -0,0 +1,31 @@ +======================== +CloudKitty REST API (v1) +======================== + +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 + :webprefix: /v1/billing/modules + +.. rest-controller:: cloudkitty.api.controllers.v1:BillingController + :webprefix: /v1/billing + +.. autotype:: cloudkitty.billing.ExtensionSummary + :members: + +.. autotype:: cloudkitty.api.controllers.v1.ResourceDescriptor + :members: + + +Report +====== + +.. rest-controller:: cloudkitty.api.controllers.v1:ReportController + :webprefix: /v1/report diff --git a/test-requirements.txt b/test-requirements.txt index 74649497..6de07dd0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,6 @@ mock>=1.0 sphinx>=1.1.2,<1.2 oslosphinx oslotest - sphinxcontrib-docbookrestapi sphinxcontrib-httpdomain sphinxcontrib-pecanwsme>=0.8