Tidy up the ReST API
The ReST API needs 4 object types. Create these as separate files. Change-Id: I108fd6b8c81f4f94c5782b5eebe8c6aa998bec96
This commit is contained in:
parent
1dfe0d6200
commit
5036e84c5a
33
magnum/api/controllers/common_types.py
Normal file
33
magnum/api/controllers/common_types.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Copyright 2013 - Red Hat, Inc.
|
||||
#
|
||||
# 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 wsme import types as wtypes
|
||||
|
||||
|
||||
Uri = wtypes.text
|
||||
|
||||
|
||||
class Link(wtypes.Base):
|
||||
"""A link representation."""
|
||||
|
||||
href = Uri
|
||||
"The link URI."
|
||||
|
||||
target_name = wtypes.text
|
||||
"Textual name of the target link."
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(href=('http://example.com:9777/v1'),
|
||||
target_name='v1')
|
@ -1,17 +1,54 @@
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
# 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 magnum.api.controllers import v1
|
||||
|
||||
import pecan
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from magnum.api.controllers import common_types
|
||||
from magnum.api.controllers.v1 import root as v1_root
|
||||
|
||||
STATUS_KIND = wtypes.Enum(str, 'SUPPORTED', 'CURRENT', 'DEPRECATED')
|
||||
|
||||
|
||||
class Version(wtypes.Base):
|
||||
"""Version representation."""
|
||||
|
||||
id = wtypes.text
|
||||
"The version identifier."
|
||||
|
||||
status = STATUS_KIND
|
||||
"The status of the API (SUPPORTED, CURRENT or DEPRECATED)."
|
||||
|
||||
link = common_types.Link
|
||||
"The link to the versioned API."
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(id='v1.0',
|
||||
status='CURRENT',
|
||||
link=common_types.Link(target_name='v1',
|
||||
href='http://example.com:9511/v1'))
|
||||
|
||||
|
||||
class RootController(object):
|
||||
v1 = v1.ContainerController()
|
||||
|
||||
v1 = v1_root.Controller()
|
||||
|
||||
@wsme_pecan.wsexpose([Version])
|
||||
def index(self):
|
||||
host_url = '%s/%s' % (pecan.request.host_url, 'v1')
|
||||
Version(id='v1.0',
|
||||
status='CURRENT',
|
||||
link=common_types.Link(target_name='v1',
|
||||
href=host_url))
|
||||
|
0
magnum/api/controllers/v1/__init__.py
Normal file
0
magnum/api/controllers/v1/__init__.py
Normal file
198
magnum/api/controllers/v1/bay.py
Normal file
198
magnum/api/controllers/v1/bay.py
Normal file
@ -0,0 +1,198 @@
|
||||
# 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 ast
|
||||
import functools
|
||||
import inspect
|
||||
import uuid
|
||||
|
||||
from oslo.utils import strutils
|
||||
from oslo.utils import timeutils
|
||||
import pecan
|
||||
from pecan import rest
|
||||
import six
|
||||
import wsme
|
||||
from wsme import exc
|
||||
from wsme import types as wtypes
|
||||
|
||||
from magnum.common import exception
|
||||
from magnum.common import yamlutils
|
||||
|
||||
# NOTE(dims): We don't depend on oslo*i18n yet
|
||||
_ = _LI = _LW = _LE = _LC = lambda x: x
|
||||
|
||||
state_kind = ["ok", "bays", "insufficient data"]
|
||||
state_kind_enum = wtypes.Enum(str, *state_kind)
|
||||
operation_kind = ('lt', 'le', 'eq', 'ne', 'ge', 'gt')
|
||||
operation_kind_enum = wtypes.Enum(str, *operation_kind)
|
||||
|
||||
|
||||
class _Base(wtypes.Base):
|
||||
|
||||
@classmethod
|
||||
def from_db_model(cls, m):
|
||||
return cls(**(m.as_dict()))
|
||||
|
||||
@classmethod
|
||||
def from_db_and_links(cls, m, links):
|
||||
return cls(links=links, **(m.as_dict()))
|
||||
|
||||
def as_dict(self, db_model):
|
||||
valid_keys = inspect.getargspec(db_model.__init__)[0]
|
||||
if 'self' in valid_keys:
|
||||
valid_keys.remove('self')
|
||||
return self.as_dict_from_keys(valid_keys)
|
||||
|
||||
def as_dict_from_keys(self, keys):
|
||||
return dict((k, getattr(self, k))
|
||||
for k in keys
|
||||
if hasattr(self, k) and
|
||||
getattr(self, k) != wsme.Unset)
|
||||
|
||||
|
||||
class Query(_Base):
|
||||
|
||||
"""Query filter."""
|
||||
|
||||
# The data types supported by the query.
|
||||
_supported_types = ['integer', 'float', 'string', 'boolean']
|
||||
|
||||
# Functions to convert the data field to the correct type.
|
||||
_type_converters = {'integer': int,
|
||||
'float': float,
|
||||
'boolean': functools.partial(
|
||||
strutils.bool_from_string, strict=True),
|
||||
'string': six.text_type,
|
||||
'datetime': timeutils.parse_isotime}
|
||||
|
||||
_op = None # provide a default
|
||||
|
||||
def get_op(self):
|
||||
return self._op or 'eq'
|
||||
|
||||
def set_op(self, value):
|
||||
self._op = value
|
||||
|
||||
field = wtypes.text
|
||||
"The name of the field to test"
|
||||
|
||||
# op = wsme.wsattr(operation_kind, default='eq')
|
||||
# this ^ doesn't seem to work.
|
||||
op = wsme.wsproperty(operation_kind_enum, get_op, set_op)
|
||||
"The comparison operator. Defaults to 'eq'."
|
||||
|
||||
value = wtypes.text
|
||||
"The value to compare against the stored data"
|
||||
|
||||
type = wtypes.text
|
||||
"The data type of value to compare against the stored data"
|
||||
|
||||
def __repr__(self):
|
||||
# for logging calls
|
||||
return '<Query %r %s %r %s>' % (self.field,
|
||||
self.op,
|
||||
self.value,
|
||||
self.type)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(field='resource_id',
|
||||
op='eq',
|
||||
value='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
|
||||
type='string'
|
||||
)
|
||||
|
||||
def as_dict(self):
|
||||
return self.as_dict_from_keys(['field', 'op', 'type', 'value'])
|
||||
|
||||
def _get_value_as_type(self, forced_type=None):
|
||||
"""Convert metadata value to the specified data type.
|
||||
"""
|
||||
type = forced_type or self.type
|
||||
try:
|
||||
converted_value = self.value
|
||||
if not type:
|
||||
try:
|
||||
converted_value = ast.literal_eval(self.value)
|
||||
except (ValueError, SyntaxError):
|
||||
# Unable to convert the metadata value automatically
|
||||
# let it default to self.value
|
||||
pass
|
||||
else:
|
||||
if type not in self._supported_types:
|
||||
# Types must be explicitly declared so the
|
||||
# correct type converter may be used. Subclasses
|
||||
# of Query may define _supported_types and
|
||||
# _type_converters to define their own types.
|
||||
raise TypeError()
|
||||
converted_value = self._type_converters[type](self.value)
|
||||
except ValueError:
|
||||
msg = (_('Unable to convert the value %(value)s'
|
||||
' to the expected data type %(type)s.') %
|
||||
{'value': self.value, 'type': type})
|
||||
raise exc.ClientSideError(msg)
|
||||
except TypeError:
|
||||
msg = (_('The data type %(type)s is not supported. The supported'
|
||||
' data type list is: %(supported)s') %
|
||||
{'type': type, 'supported': self._supported_types})
|
||||
raise exc.ClientSideError(msg)
|
||||
except Exception:
|
||||
msg = (_('Unexpected exception converting %(value)s to'
|
||||
' the expected data type %(type)s.') %
|
||||
{'value': self.value, 'type': type})
|
||||
raise exc.ClientSideError(msg)
|
||||
return converted_value
|
||||
|
||||
|
||||
class BayController(rest.RestController):
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def get(self):
|
||||
"""Retrieve a bay by UUID."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def put(self):
|
||||
"""Create a new bay."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def delete(self):
|
||||
"""Delete an existing bay."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
|
||||
class Bay(_Base):
|
||||
bay_id = wtypes.text
|
||||
""" The ID of the bays."""
|
||||
|
||||
name = wsme.wsattr(wtypes.text, mandatory=True)
|
||||
""" The name of the bay."""
|
||||
|
||||
desc = wsme.wsattr(wtypes.text, mandatory=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Bay, self).__init__(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(id=str(uuid.uuid1(),
|
||||
name="Docker",
|
||||
desc='Docker Bays'))
|
@ -23,7 +23,9 @@ import six
|
||||
import wsme
|
||||
from wsme import exc
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from magnum.common import exception
|
||||
from magnum.common import yamlutils
|
||||
|
||||
|
||||
# NOTE(dims): We don't depend on oslo*i18n yet
|
||||
@ -152,6 +154,32 @@ class Query(_Base):
|
||||
return converted_value
|
||||
|
||||
|
||||
class ContainerController(rest.RestController):
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def get(self):
|
||||
"""Retrieve a container by UUID."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def put(self):
|
||||
"""Create a new container."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def delete(self):
|
||||
"""Delete an existing container."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
|
||||
class Container(_Base):
|
||||
"""Controller Model."""
|
||||
|
||||
@ -171,30 +199,3 @@ class Container(_Base):
|
||||
return cls(id=str(uuid.uuid1(),
|
||||
name="Docker",
|
||||
desc='Docker Containers'))
|
||||
|
||||
|
||||
class ContainerController(rest.RestController):
|
||||
|
||||
@wsme_pecan.wsexpose([Container], [Query], int)
|
||||
def get_all(self, q=None, limit=None):
|
||||
# TODO(dims): Returns all the containers
|
||||
pecan.response.status = 200
|
||||
return
|
||||
|
||||
@wsme_pecan.wsexpose(Container, wtypes.text)
|
||||
def get_one(self, container_id):
|
||||
# TODO(dims): Returns specified container
|
||||
pecan.response.status = 200
|
||||
return
|
||||
|
||||
@wsme_pecan.wsexpose([Container], body=[Container])
|
||||
def post(self, data):
|
||||
# TODO(dims): Create a new container
|
||||
pecan.response.status = 201
|
||||
return
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
# TODO(dims): DELETE this container
|
||||
pecan.response.status = 204
|
||||
return
|
0
magnum/api/controllers/v1/datamodel/__init__.py
Normal file
0
magnum/api/controllers/v1/datamodel/__init__.py
Normal file
108
magnum/api/controllers/v1/datamodel/types.py
Normal file
108
magnum/api/controllers/v1/datamodel/types.py
Normal file
@ -0,0 +1,108 @@
|
||||
# Copyright 2013 - Red Hat, Inc.
|
||||
#
|
||||
# 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 string
|
||||
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
|
||||
from magnum.api.controllers import common_types
|
||||
from magnum.openstack.common._i18n import _
|
||||
|
||||
|
||||
class Base(wtypes.Base):
|
||||
"""Base class for all API types."""
|
||||
|
||||
uri = common_types.Uri
|
||||
"URI to the resource."
|
||||
|
||||
uuid = wtypes.text
|
||||
"Unique Identifier of the resource"
|
||||
|
||||
def get_name(self):
|
||||
return self.__name
|
||||
|
||||
def set_name(self, value):
|
||||
allowed_chars = string.letters + string.digits + '-_'
|
||||
for ch in value:
|
||||
if ch not in allowed_chars:
|
||||
raise ValueError(_('Names must only contain a-z,A-Z,0-9,-,_'))
|
||||
self.__name = value
|
||||
|
||||
name = wtypes.wsproperty(str, get_name, set_name, mandatory=True)
|
||||
"Name of the resource."
|
||||
|
||||
type = wtypes.text
|
||||
"The resource type."
|
||||
|
||||
description = wtypes.text
|
||||
"Textual description of the resource."
|
||||
|
||||
tags = [wtypes.text]
|
||||
"Tags for the resource."
|
||||
|
||||
project_id = wtypes.text
|
||||
"The project that this resource belongs in."
|
||||
|
||||
user_id = wtypes.text
|
||||
"The user that owns this resource."
|
||||
|
||||
def __init__(self, **kwds):
|
||||
self.__name = wsme.Unset
|
||||
super(Base, self).__init__(**kwds)
|
||||
|
||||
@classmethod
|
||||
def from_db_model(cls, m, host_url):
|
||||
json = m.as_dict()
|
||||
json['type'] = m.__tablename__
|
||||
json['uri'] = '%s/v1/%s/%s' % (host_url, m.__resource__, m.uuid)
|
||||
del json['id']
|
||||
return cls(**(json))
|
||||
|
||||
def as_dict(self, db_model):
|
||||
valid_keys = (attr for attr in db_model.__dict__.keys()
|
||||
if attr[:2] != '__' and attr != 'as_dict')
|
||||
return self.as_dict_from_keys(valid_keys)
|
||||
|
||||
def as_dict_from_keys(self, keys):
|
||||
return dict((k, getattr(self, k))
|
||||
for k in keys
|
||||
if hasattr(self, k) and
|
||||
getattr(self, k) != wsme.Unset)
|
||||
|
||||
|
||||
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 wsme.types.text and isinstance(value, wsme.types.bytes):
|
||||
value = value.decode()
|
||||
if isinstance(value, t):
|
||||
return value
|
||||
else:
|
||||
raise ValueError(
|
||||
_("Wrong type. Expected '%(type)s', got '%(value)s'")
|
||||
% {'type': self.types, 'value': type(value)})
|
199
magnum/api/controllers/v1/pod.py
Normal file
199
magnum/api/controllers/v1/pod.py
Normal file
@ -0,0 +1,199 @@
|
||||
# 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 ast
|
||||
import functools
|
||||
import inspect
|
||||
import uuid
|
||||
|
||||
from oslo.utils import strutils
|
||||
from oslo.utils import timeutils
|
||||
import pecan
|
||||
from pecan import rest
|
||||
import six
|
||||
import wsme
|
||||
from wsme import exc
|
||||
from wsme import types as wtypes
|
||||
|
||||
from magnum.common import exception
|
||||
from magnum.common import yamlutils
|
||||
|
||||
|
||||
# NOTE(dims): We don't depend on oslo*i18n yet
|
||||
_ = _LI = _LW = _LE = _LC = lambda x: x
|
||||
|
||||
state_kind = ["ok", "pods", "insufficient data"]
|
||||
state_kind_enum = wtypes.Enum(str, *state_kind)
|
||||
operation_kind = ('lt', 'le', 'eq', 'ne', 'ge', 'gt')
|
||||
operation_kind_enum = wtypes.Enum(str, *operation_kind)
|
||||
|
||||
|
||||
class _Base(wtypes.Base):
|
||||
|
||||
@classmethod
|
||||
def from_db_model(cls, m):
|
||||
return cls(**(m.as_dict()))
|
||||
|
||||
@classmethod
|
||||
def from_db_and_links(cls, m, links):
|
||||
return cls(links=links, **(m.as_dict()))
|
||||
|
||||
def as_dict(self, db_model):
|
||||
valid_keys = inspect.getargspec(db_model.__init__)[0]
|
||||
if 'self' in valid_keys:
|
||||
valid_keys.remove('self')
|
||||
return self.as_dict_from_keys(valid_keys)
|
||||
|
||||
def as_dict_from_keys(self, keys):
|
||||
return dict((k, getattr(self, k))
|
||||
for k in keys
|
||||
if hasattr(self, k) and
|
||||
getattr(self, k) != wsme.Unset)
|
||||
|
||||
|
||||
class Query(_Base):
|
||||
|
||||
"""Query filter."""
|
||||
|
||||
# The data types supported by the query.
|
||||
_supported_types = ['integer', 'float', 'string', 'boolean']
|
||||
|
||||
# Functions to convert the data field to the correct type.
|
||||
_type_converters = {'integer': int,
|
||||
'float': float,
|
||||
'boolean': functools.partial(
|
||||
strutils.bool_from_string, strict=True),
|
||||
'string': six.text_type,
|
||||
'datetime': timeutils.parse_isotime}
|
||||
|
||||
_op = None # provide a default
|
||||
|
||||
def get_op(self):
|
||||
return self._op or 'eq'
|
||||
|
||||
def set_op(self, value):
|
||||
self._op = value
|
||||
|
||||
field = wtypes.text
|
||||
"The name of the field to test"
|
||||
|
||||
# op = wsme.wsattr(operation_kind, default='eq')
|
||||
# this ^ doesn't seem to work.
|
||||
op = wsme.wsproperty(operation_kind_enum, get_op, set_op)
|
||||
"The comparison operator. Defaults to 'eq'."
|
||||
|
||||
value = wtypes.text
|
||||
"The value to compare against the stored data"
|
||||
|
||||
type = wtypes.text
|
||||
"The data type of value to compare against the stored data"
|
||||
|
||||
def __repr__(self):
|
||||
# for logging calls
|
||||
return '<Query %r %s %r %s>' % (self.field,
|
||||
self.op,
|
||||
self.value,
|
||||
self.type)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(field='resource_id',
|
||||
op='eq',
|
||||
value='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
|
||||
type='string'
|
||||
)
|
||||
|
||||
def as_dict(self):
|
||||
return self.as_dict_from_keys(['field', 'op', 'type', 'value'])
|
||||
|
||||
def _get_value_as_type(self, forced_type=None):
|
||||
"""Convert metadata value to the specified data type.
|
||||
"""
|
||||
type = forced_type or self.type
|
||||
try:
|
||||
converted_value = self.value
|
||||
if not type:
|
||||
try:
|
||||
converted_value = ast.literal_eval(self.value)
|
||||
except (ValueError, SyntaxError):
|
||||
# Unable to convert the metadata value automatically
|
||||
# let it default to self.value
|
||||
pass
|
||||
else:
|
||||
if type not in self._supported_types:
|
||||
# Types must be explicitly declared so the
|
||||
# correct type converter may be used. Subclasses
|
||||
# of Query may define _supported_types and
|
||||
# _type_converters to define their own types.
|
||||
raise TypeError()
|
||||
converted_value = self._type_converters[type](self.value)
|
||||
except ValueError:
|
||||
msg = (_('Unable to convert the value %(value)s'
|
||||
' to the expected data type %(type)s.') %
|
||||
{'value': self.value, 'type': type})
|
||||
raise exc.ClientSideError(msg)
|
||||
except TypeError:
|
||||
msg = (_('The data type %(type)s is not supported. The supported'
|
||||
' data type list is: %(supported)s') %
|
||||
{'type': type, 'supported': self._supported_types})
|
||||
raise exc.ClientSideError(msg)
|
||||
except Exception:
|
||||
msg = (_('Unexpected exception converting %(value)s to'
|
||||
' the expected data type %(type)s.') %
|
||||
{'value': self.value, 'type': type})
|
||||
raise exc.ClientSideError(msg)
|
||||
return converted_value
|
||||
|
||||
|
||||
class PodController(rest.RestController):
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def get(self):
|
||||
"""Retrieve a pod by UUID."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def put(self):
|
||||
"""Create a new pod."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def delete(self):
|
||||
"""Delete an existing pod."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
|
||||
class Pod(_Base):
|
||||
pod_id = wtypes.text
|
||||
""" The ID of the pods."""
|
||||
|
||||
name = wsme.wsattr(wtypes.text, mandatory=True)
|
||||
""" The name of the pod."""
|
||||
|
||||
desc = wsme.wsattr(wtypes.text, mandatory=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Pod, self).__init__(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(id=str(uuid.uuid1(),
|
||||
name="Docker",
|
||||
desc='Docker Pods'))
|
72
magnum/api/controllers/v1/root.py
Normal file
72
magnum/api/controllers/v1/root.py
Normal file
@ -0,0 +1,72 @@
|
||||
# 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
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from magnum.api.controllers import common_types
|
||||
from magnum.api.controllers.v1 import bay
|
||||
from magnum.api.controllers.v1 import container
|
||||
from magnum.api.controllers.v1.datamodel import types as api_types
|
||||
from magnum.api.controllers.v1 import pod
|
||||
from magnum.api.controllers.v1 import service
|
||||
from magnum.common import exception
|
||||
from magnum import version
|
||||
|
||||
|
||||
class Platform(api_types.Base):
|
||||
bays_uri = common_types.Uri
|
||||
"URI to Bays"
|
||||
|
||||
pods_uri = common_types.Uri
|
||||
"URI to Pods"
|
||||
|
||||
services_uri = common_types.Uri
|
||||
"URI to Services"
|
||||
|
||||
containers_uri = common_types.Uri
|
||||
"URI to Services"
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(uri='http://example.com/v1',
|
||||
name='magnum',
|
||||
type='platform',
|
||||
description='magnum native implementation',
|
||||
bays_uri='http://example.com:9511/v1/bays',
|
||||
pods_uri='http://example.com:9511/v1/pods',
|
||||
services_uri='http://example.com:9511/v1/services',
|
||||
containers_uri='http://example.com:9511/v1/containers')
|
||||
|
||||
|
||||
class Controller(object):
|
||||
"""Version 1 API Controller Root."""
|
||||
|
||||
bays = bay.BayController()
|
||||
pods = pod.PodController()
|
||||
services = service.ServiceController()
|
||||
containers = container.ContainerController()
|
||||
|
||||
@exception.wrap_wsme_controller_exception
|
||||
@wsme_pecan.wsexpose(Platform)
|
||||
def index(self):
|
||||
host_url = '%s/%s' % (pecan.request.host_url, 'v1')
|
||||
return Platform(uri=host_url,
|
||||
name='magnum',
|
||||
type='platform',
|
||||
description='magnum native implementation',
|
||||
implementation_version=version.version_string(),
|
||||
bays_uri='%s/bays' % host_url,
|
||||
pods_uri='%s/pods' % host_url,
|
||||
services_uri='%s/services' % host_url,
|
||||
containers_uri='%s/containers' % host_url)
|
199
magnum/api/controllers/v1/service.py
Normal file
199
magnum/api/controllers/v1/service.py
Normal file
@ -0,0 +1,199 @@
|
||||
# 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 ast
|
||||
import functools
|
||||
import inspect
|
||||
import uuid
|
||||
|
||||
from oslo.utils import strutils
|
||||
from oslo.utils import timeutils
|
||||
import pecan
|
||||
from pecan import rest
|
||||
import six
|
||||
import wsme
|
||||
from wsme import exc
|
||||
from wsme import types as wtypes
|
||||
|
||||
from magnum.common import exception
|
||||
from magnum.common import yamlutils
|
||||
|
||||
|
||||
# NOTE(dims): We don't depend on oslo*i18n yet
|
||||
_ = _LI = _LW = _LE = _LC = lambda x: x
|
||||
|
||||
state_kind = ["ok", "services", "insufficient data"]
|
||||
state_kind_enum = wtypes.Enum(str, *state_kind)
|
||||
operation_kind = ('lt', 'le', 'eq', 'ne', 'ge', 'gt')
|
||||
operation_kind_enum = wtypes.Enum(str, *operation_kind)
|
||||
|
||||
|
||||
class _Base(wtypes.Base):
|
||||
|
||||
@classmethod
|
||||
def from_db_model(cls, m):
|
||||
return cls(**(m.as_dict()))
|
||||
|
||||
@classmethod
|
||||
def from_db_and_links(cls, m, links):
|
||||
return cls(links=links, **(m.as_dict()))
|
||||
|
||||
def as_dict(self, db_model):
|
||||
valid_keys = inspect.getargspec(db_model.__init__)[0]
|
||||
if 'self' in valid_keys:
|
||||
valid_keys.remove('self')
|
||||
return self.as_dict_from_keys(valid_keys)
|
||||
|
||||
def as_dict_from_keys(self, keys):
|
||||
return dict((k, getattr(self, k))
|
||||
for k in keys
|
||||
if hasattr(self, k) and
|
||||
getattr(self, k) != wsme.Unset)
|
||||
|
||||
|
||||
class Query(_Base):
|
||||
|
||||
"""Query filter."""
|
||||
|
||||
# The data types supported by the query.
|
||||
_supported_types = ['integer', 'float', 'string', 'boolean']
|
||||
|
||||
# Functions to convert the data field to the correct type.
|
||||
_type_converters = {'integer': int,
|
||||
'float': float,
|
||||
'boolean': functools.partial(
|
||||
strutils.bool_from_string, strict=True),
|
||||
'string': six.text_type,
|
||||
'datetime': timeutils.parse_isotime}
|
||||
|
||||
_op = None # provide a default
|
||||
|
||||
def get_op(self):
|
||||
return self._op or 'eq'
|
||||
|
||||
def set_op(self, value):
|
||||
self._op = value
|
||||
|
||||
field = wtypes.text
|
||||
"The name of the field to test"
|
||||
|
||||
# op = wsme.wsattr(operation_kind, default='eq')
|
||||
# this ^ doesn't seem to work.
|
||||
op = wsme.wsproperty(operation_kind_enum, get_op, set_op)
|
||||
"The comparison operator. Defaults to 'eq'."
|
||||
|
||||
value = wtypes.text
|
||||
"The value to compare against the stored data"
|
||||
|
||||
type = wtypes.text
|
||||
"The data type of value to compare against the stored data"
|
||||
|
||||
def __repr__(self):
|
||||
# for logging calls
|
||||
return '<Query %r %s %r %s>' % (self.field,
|
||||
self.op,
|
||||
self.value,
|
||||
self.type)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(field='resource_id',
|
||||
op='eq',
|
||||
value='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
|
||||
type='string'
|
||||
)
|
||||
|
||||
def as_dict(self):
|
||||
return self.as_dict_from_keys(['field', 'op', 'type', 'value'])
|
||||
|
||||
def _get_value_as_type(self, forced_type=None):
|
||||
"""Convert metadata value to the specified data type.
|
||||
"""
|
||||
type = forced_type or self.type
|
||||
try:
|
||||
converted_value = self.value
|
||||
if not type:
|
||||
try:
|
||||
converted_value = ast.literal_eval(self.value)
|
||||
except (ValueError, SyntaxError):
|
||||
# Unable to convert the metadata value automatically
|
||||
# let it default to self.value
|
||||
pass
|
||||
else:
|
||||
if type not in self._supported_types:
|
||||
# Types must be explicitly declared so the
|
||||
# correct type converter may be used. Subclasses
|
||||
# of Query may define _supported_types and
|
||||
# _type_converters to define their own types.
|
||||
raise TypeError()
|
||||
converted_value = self._type_converters[type](self.value)
|
||||
except ValueError:
|
||||
msg = (_('Unable to convert the value %(value)s'
|
||||
' to the expected data type %(type)s.') %
|
||||
{'value': self.value, 'type': type})
|
||||
raise exc.ClientSideError(msg)
|
||||
except TypeError:
|
||||
msg = (_('The data type %(type)s is not supported. The supported'
|
||||
' data type list is: %(supported)s') %
|
||||
{'type': type, 'supported': self._supported_types})
|
||||
raise exc.ClientSideError(msg)
|
||||
except Exception:
|
||||
msg = (_('Unexpected exception converting %(value)s to'
|
||||
' the expected data type %(type)s.') %
|
||||
{'value': self.value, 'type': type})
|
||||
raise exc.ClientSideError(msg)
|
||||
return converted_value
|
||||
|
||||
|
||||
class ServiceController(rest.RestController):
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def get(self):
|
||||
"""Retrieve a service by UUID."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def put(self):
|
||||
"""Create a new service."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@pecan.expose(content_type='application/x-yaml')
|
||||
def delete(self):
|
||||
"""Delete an existing service."""
|
||||
res_yaml = yamlutils.dump({'dummy_data'})
|
||||
pecan.response.status = 200
|
||||
return res_yaml
|
||||
|
||||
|
||||
class Service(_Base):
|
||||
service_id = wtypes.text
|
||||
""" The ID of the services."""
|
||||
|
||||
name = wsme.wsattr(wtypes.text, mandatory=True)
|
||||
""" The name of the service."""
|
||||
|
||||
desc = wsme.wsattr(wtypes.text, mandatory=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Service, self).__init__(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(id=str(uuid.uuid1(),
|
||||
name="Docker",
|
||||
desc='Docker Services'))
|
45
magnum/common/yamlutils.py
Normal file
45
magnum/common/yamlutils.py
Normal file
@ -0,0 +1,45 @@
|
||||
# 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 yaml
|
||||
|
||||
|
||||
if hasattr(yaml, 'CSafeLoader'):
|
||||
yaml_loader = yaml.CSafeLoader
|
||||
else:
|
||||
yaml_loader = yaml.SafeLoader
|
||||
|
||||
|
||||
if hasattr(yaml, 'CSafeDumper'):
|
||||
yaml_dumper = yaml.CSafeDumper
|
||||
else:
|
||||
yaml_dumper = yaml.SafeDumper
|
||||
|
||||
|
||||
def load(s):
|
||||
try:
|
||||
yml_dict = yaml.load(s, yaml_loader)
|
||||
except yaml.YAMLError as exc:
|
||||
msg = 'An error occurred during YAML parsing.'
|
||||
if hasattr(exc, 'problem_mark'):
|
||||
msg += ' Error position: (%s:%s)' % (exc.problem_mark.line + 1,
|
||||
exc.problem_mark.column + 1)
|
||||
raise ValueError(msg)
|
||||
if not isinstance(yml_dict, dict) and not isinstance(yml_dict, list):
|
||||
raise ValueError('The source is not a YAML mapping or list.')
|
||||
if isinstance(yml_dict, dict) and len(yml_dict) < 1:
|
||||
raise ValueError('Could not find any element in your YAML mapping.')
|
||||
return yml_dict
|
||||
|
||||
|
||||
def dump(s):
|
||||
return yaml.dump(s, Dumper=yaml_dumper)
|
@ -15,7 +15,17 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo.config import cfg
|
||||
from oslotest import base
|
||||
import testscenarios
|
||||
|
||||
|
||||
class BaseTestCase(testscenarios.WithScenarios, base.BaseTestCase):
|
||||
"""Test base class."""
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTestCase, self).setUp()
|
||||
self.addCleanup(cfg.CONF.reset)
|
||||
|
||||
|
||||
class TestCase(base.BaseTestCase):
|
||||
|
0
magnum/tests/common/__init__.py
Normal file
0
magnum/tests/common/__init__.py
Normal file
48
magnum/tests/common/test_yamlutils.py
Normal file
48
magnum/tests/common/test_yamlutils.py
Normal file
@ -0,0 +1,48 @@
|
||||
# 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 mock
|
||||
import yaml
|
||||
|
||||
from magnum.common import yamlutils
|
||||
from magnum.tests import base
|
||||
|
||||
|
||||
class TestYamlUtils(base.BaseTestCase):
|
||||
def setUp(self):
|
||||
super(TestYamlUtils, self).setUp()
|
||||
|
||||
def test_load_yaml(self):
|
||||
yml_dict = yamlutils.load('a: x\nb: y\n')
|
||||
self.assertEqual(yml_dict, {'a': 'x', 'b': 'y'})
|
||||
|
||||
def test_load_empty_yaml(self):
|
||||
self.assertRaises(ValueError, yamlutils.load, '{}')
|
||||
|
||||
def test_load_empty_list(self):
|
||||
yml_dict = yamlutils.load('[]')
|
||||
self.assertEqual(yml_dict, [])
|
||||
|
||||
def test_load_invalid_yaml_syntax(self):
|
||||
self.assertRaises(ValueError, yamlutils.load, "}invalid: y'm'l3!")
|
||||
|
||||
def test_load_invalid_yaml_type(self):
|
||||
self.assertRaises(ValueError, yamlutils.load, 'invalid yaml type')
|
||||
|
||||
@mock.patch('magnum.common.yamlutils.yaml.dump')
|
||||
def test_dump_yaml(self, dump):
|
||||
if hasattr(yaml, 'CSafeDumper'):
|
||||
yaml_dumper = yaml.CSafeDumper
|
||||
else:
|
||||
yaml_dumper = yaml.SafeDumper
|
||||
yamlutils.dump('version: 1')
|
||||
dump.assert_called_with('version: 1', Dumper=yaml_dumper)
|
@ -15,10 +15,6 @@ from magnum import tests
|
||||
|
||||
class TestRootController(tests.FunctionalTest):
|
||||
|
||||
def test_get_all(self):
|
||||
response = self.app.get('/v1/containers')
|
||||
assert response.status_int == 200
|
||||
|
||||
def test_get_not_found(self):
|
||||
response = self.app.get('/a/bogus/url', expect_errors=True)
|
||||
assert response.status_int == 404
|
||||
|
Loading…
Reference in New Issue
Block a user