Cloudpulse API handling code

Change-Id: I2b50cf9bd59d96274a96561337d0174ab0aabbdf
Implements: blueprint cloudpulse-api-handlers
This commit is contained in:
vinod pandarinathan
2015-06-12 14:28:46 -07:00
parent 793be25ac9
commit cd2e5a7d64
5 changed files with 645 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
# All Rights Reserved.
#
# 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.
"""
Version 1 of the Cloudpulse API
NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED.
"""
import datetime
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from cloudpulse.api.controllers import link
from cloudpulse.api.controllers.v1 import cpulse
class APIBase(wtypes.Base):
created_at = wsme.wsattr(datetime.datetime, readonly=True)
"""The time in UTC at which the object is created"""
updated_at = wsme.wsattr(datetime.datetime, readonly=True)
"""The time in UTC at which the object is updated"""
def as_dict(self):
"""Render this object as a dict of its fields."""
return dict((k, getattr(self, k))
for k in self.fields
if hasattr(self, k) and
getattr(self, k) != wsme.Unset)
def unset_fields_except(self, except_list=None):
"""Unset fields so they don't appear in the message body.
:param except_list: A list of fields that won't be touched.
"""
if except_list is None:
except_list = []
for k in self.as_dict():
if k not in except_list:
setattr(self, k, wsme.Unset)
class V1(APIBase):
"""The representation of the version 1 of the API."""
id = wtypes.text
"""The ID of the version, also acts as the release number"""
cpulse = [link.Link]
"""Links to the cpulse resource"""
extcpulse = [link.Link]
"""Links to the cpulse extension resource"""
@staticmethod
def convert():
v1 = V1()
v1.id = "v1"
v1.links = [link.Link.make_link('self', pecan.request.host_url,
'v1', '', bookmark=True),
link.Link.make_link('describedby',
'http://docs.openstack.org',
'developer/cloudpulse/dev',
'api-spec-v1.html',
bookmark=True, type='text/html')
]
v1.cpulse = [link.Link.make_link('self', pecan.request.host_url,
'cpulse', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
'cpulse', '',
bookmark=True)
]
v1.cpulse_ext = [link.Link.make_link('self', pecan.request.host_url,
'cpulse_ext', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
'cpulse_ext', '',
bookmark=True)
]
return v1
class Controller(rest.RestController):
"""Version 1 API controller root."""
cpulse = cpulse.cpulseController()
@wsme_pecan.wsexpose(V1)
def get(self):
# NOTE: The reason why convert() it's being called for every
# request is because we need to get the host url from
# the request object to make the links.
return V1.convert()
__all__ = (Controller)

View File

@@ -0,0 +1,48 @@
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
import pecan
from wsme import types as wtypes
from cloudpulse.api.controllers import base
from cloudpulse.api.controllers import link
class Collection(base.APIBase):
next = wtypes.text
"""A link to retrieve the next subset of the collection"""
@property
def collection(self):
return getattr(self, self._type)
def has_next(self, limit):
"""Return whether collection has more items."""
return len(self.collection) and len(self.collection) == limit
def get_next(self, limit, url=None, **kwargs):
"""Return a link to the next subset of the collection."""
if not self.has_next(limit):
return wtypes.Unset
resource_url = url or self._type
q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs])
next_args = ('?%(args)slimit=%(limit)d&marker=%(marker)s'
% {'args': q_args, 'limit': limit,
'marker': self.collection[-1].uuid})
return link.Link.make_link('next', pecan.request.host_url,
resource_url, next_args).href

View File

@@ -0,0 +1,231 @@
# Copyright 2013 UnitedStack Inc.
# All Rights Reserved.
#
# 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.
import datetime
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from cloudpulse.api.controllers import base
from cloudpulse.api.controllers import link
from cloudpulse.api.controllers.v1 import collection
from cloudpulse.api.controllers.v1 import types
from cloudpulse.api.controllers.v1 import utils as api_utils
from cloudpulse.common import exception
from cloudpulse import objects
class CpulsePatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/uuid']
class Cpulse(base.APIBase):
"""API representation of a test.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a test.
"""
id = wtypes.IntegerType(minimum=1)
uuid = types.uuid
"""Unique UUID for this test"""
name = wtypes.StringType(min_length=1, max_length=255)
"""Name of this test"""
state = wtypes.StringType(min_length=1, max_length=255)
"""State of this test"""
cpulse_create_timeout = wtypes.IntegerType(minimum=0)
"""Timeout for creating the test in minutes. Set to 0 for no timeout."""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated test links"""
result = wtypes.StringType(min_length=1, max_length=1024)
"""Result of this test"""
def __init__(self, **kwargs):
super(Cpulse, self).__init__()
self.fields = []
for field in objects.Cpulse.fields:
# Skip fields we do not expose.
if not hasattr(self, field):
continue
self.fields.append(field)
setattr(self, field, kwargs.get(field, wtypes.Unset))
@staticmethod
def _convert_with_links(cpulse, url, expand=True):
if not expand:
cpulse.unset_fields_except(['uuid', 'name', 'state', 'id', 'result'
])
return cpulse
@classmethod
def convert_with_links(cls, rpc_test, expand=True):
test = Cpulse(**rpc_test.as_dict())
return cls._convert_with_links(test, pecan.request.host_url, expand)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='example',
state="CREATED",
result="NotYetRun",
created_at=datetime.datetime.utcnow(),
updated_at=datetime.datetime.utcnow())
return cls._convert_with_links(sample, 'http://localhost:9511', expand)
class CpulseCollection(collection.Collection):
"""API representation of a collection of tests."""
cpulses = [Cpulse]
"""A list containing tests objects"""
def __init__(self, **kwargs):
self._type = 'cpulses'
@staticmethod
def convert_with_links(rpc_tests, limit, url=None, expand=False, **kwargs):
collection = CpulseCollection()
collection.cpulses = [Cpulse.convert_with_links(p, expand)
for p in rpc_tests]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@classmethod
def sample(cls):
sample = cls()
sample.cpulse = [Cpulse.sample(expand=False)]
return sample
class cpulseController(rest.RestController):
"""REST controller for Cpulse.."""
def __init__(self):
super(cpulseController, self).__init__()
_custom_actions = {'detail': ['GET']}
def _get_tests_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Cpulse.get_by_uuid(pecan.request.context,
marker)
tests = pecan.request.rpcapi.test_list(pecan.request.context, limit,
marker_obj, sort_key=sort_key,
sort_dir=sort_dir)
return CpulseCollection.convert_with_links(tests, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(CpulseCollection, types.uuid,
types.uuid, int, wtypes.text, wtypes.text)
def get_all(self, test_uuid=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of tests.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
return self._get_tests_collection(marker, limit, sort_key,
sort_dir)
@wsme_pecan.wsexpose(CpulseCollection, types.uuid,
types.uuid, int, wtypes.text, wtypes.text)
def detail(self, test_uuid=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of tests with detail.
:param test_uuid: UUID of a test, to get only tests for that test.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "tests":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['tests', 'detail'])
return self._get_tests_collection(marker, limit,
sort_key, sort_dir, expand,
resource_url)
@wsme_pecan.wsexpose(Cpulse, types.uuid_or_name)
def get_one(self, test_ident):
"""Retrieve information about the given test.
:param test_ident: UUID of a test or logical name of the test.
"""
rpc_test = api_utils.get_rpc_resource('Cpulse', test_ident)
return Cpulse.convert_with_links(rpc_test)
@wsme_pecan.wsexpose(Cpulse, body=Cpulse, status_code=201)
def post(self, test):
"""Create a new test.
:param test: a test within the request body.
"""
test_dict = test.as_dict()
context = pecan.request.context
auth_token = context.auth_token_info['token']
test_dict['project_id'] = auth_token['project']['id']
test_dict['user_id'] = auth_token['user']['id']
ncp = objects.Cpulse(context, **test_dict)
ncp.cpulse_create_timeout = 0
ncp.result = "NotYetRun"
res_test = pecan.request.rpcapi.test_create(ncp,
ncp.cpulse_create_timeout)
return Cpulse.convert_with_links(res_test)
@wsme_pecan.wsexpose(None, types.uuid_or_name, status_code=204)
def delete(self, test_ident):
"""Delete a test.
:param test_ident: UUID of a test or logical name of the test.
"""
context = pecan.request.context
rpc_test = api_utils.get_rpc_resource('Cpulse', test_ident)
pecan.request.rpcapi.test_delete(context, rpc_test.uuid)

View File

@@ -0,0 +1,181 @@
# coding: utf-8
#
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# 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_utils import strutils
import wsme
from wsme import types as wtypes
from cloudpulse.common import exception
from cloudpulse.common import utils
from cloudpulse.openstack.common._i18n import _
class NameType(wtypes.UserType):
"""A logical name type."""
basetype = wtypes.text
name = 'name'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod
def validate(value):
if not utils.is_name_safe(value):
raise exception.InvalidName(name=value)
return value
@staticmethod
def frombasetype(value):
if value is None:
return None
return NameType.validate(value)
class UuidType(wtypes.UserType):
"""A simple UUID type."""
basetype = wtypes.text
name = 'uuid'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod
def validate(value):
if not utils.is_uuid_like(value):
raise exception.InvalidUUID(uuid=value)
return value
@staticmethod
def frombasetype(value):
if value is None:
return None
return UuidType.validate(value)
class BooleanType(wtypes.UserType):
"""A simple boolean type."""
basetype = wtypes.text
name = 'boolean'
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
# to get the name of the type by accessing it's __name__ attribute.
# Remove this __name__ attribute once it's fixed in WSME.
# https://bugs.launchpad.net/wsme/+bug/1265590
__name__ = name
@staticmethod
def validate(value):
try:
return strutils.bool_from_string(value, strict=True)
except ValueError as e:
# raise Invalid to return 400 (BadRequest) in the API
raise exception.Invalid(e)
@staticmethod
def frombasetype(value):
if value is None:
return None
return BooleanType.validate(value)
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.
"""
basetype = wtypes.text
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:
try:
return wtypes.validate_value(t, value)
except (exception.InvalidUUID, ValueError):
pass
else:
raise ValueError(_("Expected '%(type)s', got '%(value)s'")
% {'type': self.types, 'value': type(value)})
uuid = UuidType()
name = NameType()
uuid_or_name = MultiType(UuidType, NameType)
boolean = BooleanType()
class JsonPatchType(wtypes.Base):
"""A complex type that represents a single json-patch operation."""
path = wtypes.wsattr(wtypes.StringType(pattern='^(/[\w-]+)+$'),
mandatory=True)
op = wtypes.wsattr(wtypes.Enum(str, 'add', 'replace', 'remove'),
mandatory=True)
value = MultiType(wtypes.text, int)
@staticmethod
def internal_attrs():
"""Returns a list of internal attributes.
Internal attributes can't be added, replaced or removed. This
method may be overwritten by derived class.
"""
return ['/created_at', '/id', '/links', '/updated_at', '/uuid']
@staticmethod
def mandatory_attrs():
"""Retruns a list of mandatory attributes.
Mandatory attributes can't be removed from the document. This
method should be overwritten by derived class.
"""
return []
@staticmethod
def validate(patch):
if patch.path in patch.internal_attrs():
msg = _("'%s' is an internal attribute and can not be updated")
raise wsme.exc.ClientSideError(msg % patch.path)
if patch.path in patch.mandatory_attrs() and patch.op == 'remove':
msg = _("'%s' is a mandatory attribute and can not be removed")
raise wsme.exc.ClientSideError(msg % patch.path)
if patch.op != 'remove':
if not patch.value:
msg = _("'add' and 'replace' operations needs value")
raise wsme.exc.ClientSideError(msg)
ret = {'path': patch.path, 'op': patch.op}
if patch.value:
ret['value'] = patch.value
return ret

View File

@@ -0,0 +1,70 @@
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
import jsonpatch
from oslo_config import cfg
import pecan
import wsme
from cloudpulse.common import exception
from cloudpulse.common import utils
from cloudpulse import objects
from cloudpulse.openstack.common._i18n import _
CONF = cfg.CONF
JSONPATCH_EXCEPTIONS = (jsonpatch.JsonPatchException,
jsonpatch.JsonPointerException,
KeyError)
def validate_limit(limit):
if limit is not None and limit <= 0:
raise wsme.exc.ClientSideError(_("Limit must be positive"))
return min(CONF.api.max_limit, limit) or CONF.api.max_limit
def validate_sort_dir(sort_dir):
if sort_dir not in ['asc', 'desc']:
raise wsme.exc.ClientSideError(_("Invalid sort direction: %s. "
"Acceptable values are "
"'asc' or 'desc'") % sort_dir)
return sort_dir
def apply_jsonpatch(doc, patch):
return jsonpatch.apply_patch(doc, jsonpatch.JsonPatch(patch))
def get_rpc_resource(resource, resource_ident):
"""Get the RPC resource from the uuid or logical name.
:param resource: the resource type.
:param resource_ident: the UUID or logical name of the resource.
:returns: The RPC resource.
:raises: InvalidUuidOrName if the name or uuid provided is not valid.
"""
resource = getattr(objects, resource)
if utils.is_uuid_like(resource_ident):
return resource.get_by_uuid(pecan.request.context, resource_ident)
if utils.allow_logical_names():
return resource.get_by_name(pecan.request.context, resource_ident)
raise exception.InvalidUuidOrName(name=resource_ident)