Improved documentation (docstrings and sphinx)

Added docstrings to every controllers and types.
Added samples for every types.
Improved CloudKitty's developper documentation.

Change-Id: Idcd25777f67c61c096fbc52962bc173a86d614ba
This commit is contained in:
Stéphane Albert
2014-08-11 16:54:57 +02:00
parent 1467304878
commit 254b13af5e
11 changed files with 352 additions and 19 deletions

View File

@@ -28,50 +28,111 @@ LOG = logging.getLogger(__name__)
class APILink(wtypes.Base): class APILink(wtypes.Base):
"""API link description.
"""
type = wtypes.text type = wtypes.text
"""Type of link."""
rel = wtypes.text rel = wtypes.text
"""Relationship with this link."""
href = wtypes.text 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): class APIMediaType(wtypes.Base):
"""Media type description.
"""
base = wtypes.text base = wtypes.text
"""Base type of this media type."""
type = wtypes.text 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): class APIVersion(wtypes.Base):
"""API Version description.
"""
id = wtypes.text 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] links = [APILink]
"""List of links to API resources."""
media_types = [APIMediaType] 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): class RootController(rest.RestController):
"""Root REST Controller exposing versions of the API.
"""
v1 = v1.V1Controller() v1 = v1.V1Controller()
@wsme_pecan.wsexpose([APIVersion]) @wsme_pecan.wsexpose([APIVersion])
def get(self): def get(self):
"""Return the version list
"""
# TODO(sheeprine): Maybe we should store all the API version # TODO(sheeprine): Maybe we should store all the API version
# informations in every API modules # informations in every API modules
ver1 = APIVersion( ver1 = APIVersion(
id='v1', id='v1',
status='EXPERIMENTAL', status='EXPERIMENTAL',
updated='2014-06-02T00:00:00Z', updated='2014-08-11T16:00:00Z',
links=[ links=[
APILink( APILink(
rel='self', rel='self',
href='{scheme}://{host}/v1'.format( href='{scheme}://{host}:{port}/v1'.format(
scheme=pecan.request.scheme, scheme=pecan.request.scheme,
host=pecan.request.host host=pecan.request.host,
port=pecan.request.port
) )
) )
], ],

View File

@@ -33,14 +33,20 @@ CLOUDKITTY_SERVICES = wtypes.Enum(wtypes.text,
class ResourceDescriptor(wtypes.Base): class ResourceDescriptor(wtypes.Base):
"""Type describing a resource in CloudKitty.
"""
service = CLOUDKITTY_SERVICES service = CLOUDKITTY_SERVICES
"""Name of the service."""
# FIXME(sheeprine): values should be dynamic # FIXME(sheeprine): values should be dynamic
# Testing with ironic dynamic type # Testing with ironic dynamic type
desc = {wtypes.text: cktypes.MultiType(wtypes.text, int, float, dict)} desc = {wtypes.text: cktypes.MultiType(wtypes.text, int, float, dict)}
"""Description of the resources parameters."""
volume = int volume = int
"""Number of resources."""
def to_json(self): def to_json(self):
res_dict = {} res_dict = {}
@@ -50,8 +56,20 @@ class ResourceDescriptor(wtypes.Base):
}] }]
return res_dict 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): class ModulesController(rest.RestController):
"""REST Controller managing billing modules.
"""
def __init__(self): def __init__(self):
self.extensions = extension.ExtensionManager( self.extensions = extension.ExtensionManager(
@@ -72,6 +90,10 @@ class ModulesController(rest.RestController):
@wsme_pecan.wsexpose([wtypes.text]) @wsme_pecan.wsexpose([wtypes.text])
def get(self): def get(self):
"""Return the list of loaded modules.
:return: Name of every loaded modules.
"""
return [ext for ext in self.extensions.names()] return [ext for ext in self.extensions.names()]
@@ -85,6 +107,12 @@ class BillingController(rest.RestController):
@wsme_pecan.wsexpose(float, body=[ResourceDescriptor]) @wsme_pecan.wsexpose(float, body=[ResourceDescriptor])
def quote(self, res_data): 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 # TODO(sheeprine): Send RPC request for quote
from cloudkitty import extension_manager from cloudkitty import extension_manager
b_processors = {} b_processors = {}
@@ -115,6 +143,9 @@ class BillingController(rest.RestController):
class ReportController(rest.RestController): class ReportController(rest.RestController):
"""REST Controller managing the reporting.
"""
_custom_actions = { _custom_actions = {
'total': ['GET'] 'total': ['GET']
@@ -122,11 +153,17 @@ class ReportController(rest.RestController):
@wsme_pecan.wsexpose(float) @wsme_pecan.wsexpose(float)
def total(self): def total(self):
"""Return the amount to pay for the current month.
"""
# TODO(sheeprine): Get current total from DB # TODO(sheeprine): Get current total from DB
return 10.0 return 10.0
class V1Controller(rest.RestController): class V1Controller(rest.RestController):
"""API version 1 controller.
"""
billing = BillingController() billing = BillingController()
report = ReportController() report = ReportController()

View File

@@ -39,19 +39,37 @@ class ExtensionSummary(wtypes.Base):
""" """
name = wtypes.wsattr(wtypes.text, mandatory=True) name = wtypes.wsattr(wtypes.text, mandatory=True)
"""Name of the extension."""
description = wtypes.text description = wtypes.text
"""Short description of the extension."""
enabled = wtypes.wsattr(bool, default=False) enabled = wtypes.wsattr(bool, default=False)
"""Extension status."""
hot_config = wtypes.wsattr(bool, default=False, name='hot-config') 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) @six.add_metaclass(abc.ABCMeta)
class BillingEnableController(rest.RestController): class BillingEnableController(rest.RestController):
"""REST Controller to enable or disable a billing module.
"""
@wsme_pecan.wsexpose(bool) @wsme_pecan.wsexpose(bool)
def get(self): def get(self):
"""Get module status
"""
api = db_api.get_instance() api = db_api.get_instance()
module = pecan.request.path.rsplit('/', 2)[-2] module = pecan.request.path.rsplit('/', 2)[-2]
module_db = api.get_module_enable_state() module_db = api.get_module_enable_state()
@@ -59,6 +77,11 @@ class BillingEnableController(rest.RestController):
@wsme_pecan.wsexpose(bool, body=bool) @wsme_pecan.wsexpose(bool, body=bool)
def put(self, state): def put(self, state):
"""Set module status
:param state: State to set.
:return: New state set for the module.
"""
api = db_api.get_instance() api = db_api.get_instance()
module = pecan.request.path.rsplit('/', 2)[-2] module = pecan.request.path.rsplit('/', 2)[-2]
module_db = api.get_module_enable_state() module_db = api.get_module_enable_state()
@@ -67,18 +90,37 @@ class BillingEnableController(rest.RestController):
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BillingConfigController(rest.RestController): class BillingConfigController(rest.RestController):
"""REST Controller managing internal configuration of billing modules.
@wsme_pecan.wsexpose() """
def get(self):
def _not_configurable(self):
try: try:
module = pecan.request.path.rsplit('/', 1)[-1] module = pecan.request.path.rsplit('/', 1)[-1]
raise BillingModuleNotConfigurable(module) raise BillingModuleNotConfigurable(module)
except BillingModuleNotConfigurable as e: except BillingModuleNotConfigurable as e:
pecan.abort(400, str(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) @six.add_metaclass(abc.ABCMeta)
class BillingController(rest.RestController): class BillingController(rest.RestController):
"""REST Controller used to manage billing system.
"""
config = BillingConfigController() config = BillingConfigController()
enabled = BillingEnableController() enabled = BillingEnableController()

View File

@@ -34,8 +34,15 @@ MAP_TYPE = wtypes.Enum(wtypes.text, 'flat', 'rate')
class Mapping(wtypes.Base): class Mapping(wtypes.Base):
map_type = wtypes.wsattr(MAP_TYPE, default='rate', name='type') map_type = wtypes.wsattr(MAP_TYPE, default='rate', name='type')
"""Type of the mapping."""
value = wtypes.wsattr(float, mandatory=True) 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): class BasicHashMapConfigController(billing.BillingConfigController):
@@ -56,7 +63,7 @@ class BasicHashMapConfigController(billing.BillingConfigController):
@wsme_pecan.wsexpose(Mapping, wtypes.text, wtypes.text, wtypes.text) @wsme_pecan.wsexpose(Mapping, wtypes.text, wtypes.text, wtypes.text)
def get_mapping(self, service, field, key): def get_mapping(self, service, field, key):
"""Return the list of every mappings. """Get a mapping from full path.
""" """
hashmap = api.get_instance() hashmap = api.get_instance()
@@ -67,6 +74,10 @@ class BasicHashMapConfigController(billing.BillingConfigController):
@wsme_pecan.wsexpose([wtypes.text]) @wsme_pecan.wsexpose([wtypes.text])
def get(self): def get(self):
"""Get the service list
:return: List of every services' name.
"""
hashmap = api.get_instance() hashmap = api.get_instance()
return [service.name for service in hashmap.list_services()] 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): def get_one(self, service=None, field=None):
"""Return the list of every sub keys. """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() hashmap = api.get_instance()
if field: if field:
@@ -95,6 +108,13 @@ class BasicHashMapConfigController(billing.BillingConfigController):
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text, @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text,
body=Mapping) body=Mapping)
def post(self, service, field=None, key=None, mapping=None): 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() hashmap = api.get_instance()
if field: if field:
if key: if key:
@@ -131,6 +151,13 @@ class BasicHashMapConfigController(billing.BillingConfigController):
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text, @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text,
body=Mapping) body=Mapping)
def put(self, service, field, key, 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() hashmap = api.get_instance()
try: try:
hashmap.update_mapping( hashmap.update_mapping(
@@ -149,6 +176,9 @@ class BasicHashMapConfigController(billing.BillingConfigController):
def delete(self, service, field=None, key=None): def delete(self, service, field=None, key=None):
"""Delete the parent and all the sub keys recursively. """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() hashmap = api.get_instance()
try: try:

47
doc/source/arch.rst Normal file
View File

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

View File

@@ -12,8 +12,8 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys #import sys
import os #import os
# If extensions (or modules to document with autodoc) are in another directory, # 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 # 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 # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # 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. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@@ -74,11 +85,11 @@ exclude_patterns = []
#default_role = None #default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text. # 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 # If true, the current module name will be prepended to all description
# unit titles (such as .. function::). # unit titles (such as .. function::).
#add_module_names = True add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the # If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default. # output. They are ignored by default.
@@ -88,7 +99,7 @@ exclude_patterns = []
pygments_style = 'sphinx' pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting. # 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. # If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False #keep_warnings = False

View File

@@ -3,14 +3,44 @@
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. 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:: .. 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 Indices and tables
@@ -19,4 +49,3 @@ Indices and tables
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :ref:`modindex`
* :ref:`search` * :ref:`search`

View File

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

View File

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

31
doc/source/webapi/v1.rst Normal file
View File

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

View File

@@ -6,7 +6,6 @@ mock>=1.0
sphinx>=1.1.2,<1.2 sphinx>=1.1.2,<1.2
oslosphinx oslosphinx
oslotest oslotest
sphinxcontrib-docbookrestapi sphinxcontrib-docbookrestapi
sphinxcontrib-httpdomain sphinxcontrib-httpdomain
sphinxcontrib-pecanwsme>=0.8 sphinxcontrib-pecanwsme>=0.8