Cloudpulse API handling code
Change-Id: I2b50cf9bd59d96274a96561337d0174ab0aabbdf Implements: blueprint cloudpulse-api-handlers
This commit is contained in:
115
cloudpulse/api/controllers/v1/__init__.py
Normal file
115
cloudpulse/api/controllers/v1/__init__.py
Normal 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)
|
48
cloudpulse/api/controllers/v1/collection.py
Normal file
48
cloudpulse/api/controllers/v1/collection.py
Normal 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
|
231
cloudpulse/api/controllers/v1/cpulse.py
Normal file
231
cloudpulse/api/controllers/v1/cpulse.py
Normal 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)
|
181
cloudpulse/api/controllers/v1/types.py
Normal file
181
cloudpulse/api/controllers/v1/types.py
Normal 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
|
70
cloudpulse/api/controllers/v1/utils.py
Normal file
70
cloudpulse/api/controllers/v1/utils.py
Normal 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)
|
Reference in New Issue
Block a user