diff --git a/magnum/api/controllers/common_types.py b/magnum/api/controllers/common_types.py new file mode 100644 index 0000000000..887b4fd3a1 --- /dev/null +++ b/magnum/api/controllers/common_types.py @@ -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') diff --git a/magnum/api/controllers/root.py b/magnum/api/controllers/root.py index d0a85c674c..89113951cf 100644 --- a/magnum/api/controllers/root.py +++ b/magnum/api/controllers/root.py @@ -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)) diff --git a/magnum/api/controllers/v1/__init__.py b/magnum/api/controllers/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/magnum/api/controllers/v1/bay.py b/magnum/api/controllers/v1/bay.py new file mode 100644 index 0000000000..76f1253339 --- /dev/null +++ b/magnum/api/controllers/v1/bay.py @@ -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 '' % (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')) diff --git a/magnum/api/controllers/v1.py b/magnum/api/controllers/v1/container.py similarity index 88% rename from magnum/api/controllers/v1.py rename to magnum/api/controllers/v1/container.py index 70cd0ff47d..e4ad30de19 100644 --- a/magnum/api/controllers/v1.py +++ b/magnum/api/controllers/v1/container.py @@ -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 diff --git a/magnum/api/controllers/v1/datamodel/__init__.py b/magnum/api/controllers/v1/datamodel/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/magnum/api/controllers/v1/datamodel/types.py b/magnum/api/controllers/v1/datamodel/types.py new file mode 100644 index 0000000000..11747ed53c --- /dev/null +++ b/magnum/api/controllers/v1/datamodel/types.py @@ -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)}) diff --git a/magnum/api/controllers/v1/pod.py b/magnum/api/controllers/v1/pod.py new file mode 100644 index 0000000000..06479522fa --- /dev/null +++ b/magnum/api/controllers/v1/pod.py @@ -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 '' % (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')) diff --git a/magnum/api/controllers/v1/root.py b/magnum/api/controllers/v1/root.py new file mode 100644 index 0000000000..a9d926c124 --- /dev/null +++ b/magnum/api/controllers/v1/root.py @@ -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) diff --git a/magnum/api/controllers/v1/service.py b/magnum/api/controllers/v1/service.py new file mode 100644 index 0000000000..fb65447edb --- /dev/null +++ b/magnum/api/controllers/v1/service.py @@ -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 '' % (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')) diff --git a/magnum/common/yamlutils.py b/magnum/common/yamlutils.py new file mode 100644 index 0000000000..cd8850e523 --- /dev/null +++ b/magnum/common/yamlutils.py @@ -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) diff --git a/magnum/tests/base.py b/magnum/tests/base.py index 1c30cdb56e..80013764d9 100644 --- a/magnum/tests/base.py +++ b/magnum/tests/base.py @@ -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): diff --git a/magnum/tests/common/__init__.py b/magnum/tests/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/magnum/tests/common/test_yamlutils.py b/magnum/tests/common/test_yamlutils.py new file mode 100644 index 0000000000..d06790f0f6 --- /dev/null +++ b/magnum/tests/common/test_yamlutils.py @@ -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) diff --git a/magnum/tests/test_functional.py b/magnum/tests/test_functional.py index 794d82b2a2..4147305deb 100644 --- a/magnum/tests/test_functional.py +++ b/magnum/tests/test_functional.py @@ -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