diff --git a/.gitignore b/.gitignore index 69f0c8d0..d5030952 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ pip-log.txt .tox nosetests.xml tests/cover +tests/logs # Translations *.mo diff --git a/doc/source/index.rst b/doc/source/index.rst index fa175b4d..9c97964f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,8 +29,6 @@ This documentation is generated by the Sphinx toolkit and lives in the source tree. Additional draft and project documentation on Poppy and other components of OpenStack can be found on the `OpenStack wiki`_. Cloud administrators, refer to `docs.openstack.org`_. -.. _`OpenStack wiki`: http://wiki.openstack.org -.. _`docs.openstack.org`: http://docs.openstack.org Concepts ======== @@ -76,4 +74,5 @@ Using Poppy's API .. toctree:: :maxdepth: 1 - api +.. _`OpenStack wiki`: http://wiki.openstack.org +.. _`docs.openstack.org`: http://docs.openstack.org diff --git a/poppy/common/uri.py b/poppy/common/uri.py new file mode 100644 index 00000000..8fd1ce32 --- /dev/null +++ b/poppy/common/uri.py @@ -0,0 +1,136 @@ +"""Defines URI utilities + +Copyright 2014 by Rackspace Hosting, 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 six + + +# NOTE(kgriffs): See also RFC 3986 +_UNRESERVED = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789' + '-._~') + +# NOTE(kgriffs): See also RFC 3986 +_DELIMITERS = ":/?#[]@!$&'()*+,;=" + +_ALL_ALLOWED = _UNRESERVED + _DELIMITERS + + +def _create_char_encoder(allowed_chars): + + lookup = {} + + for code_point in range(256): + if chr(code_point) in allowed_chars: + encoded_char = chr(code_point) + else: + encoded_char = '%{0:02X}'.format(code_point) + + # NOTE(kgriffs): PY2 returns str from uri.encode, while + # PY3 returns a byte array. + key = chr(code_point) if six.PY2 else code_point + lookup[key] = encoded_char + + return lookup.__getitem__ + + +def _create_str_encoder(is_value): + + allowed_chars = _UNRESERVED if is_value else _ALL_ALLOWED + encode_char = _create_char_encoder(allowed_chars) + + def encoder(uri): + # PERF(kgriffs): Very fast way to check, learned from urlib.quote + if not uri.rstrip(allowed_chars): + return uri + + # Convert to a byte array if it is not one already + # + # NOTE(kgriffs): Code coverage disabled since in Py3K the uri + # is always a text type, so we get a failure for that tox env. + if isinstance(uri, six.text_type): # pragma no cover + uri = uri.encode('utf-8') + + # Use our map to encode each char and join the result into a new uri + # + # PERF(kgriffs): map is faster than list comp on py27, but a tiny bit + # slower on py33. Since we are already much faster than urllib on + # py33, let's optimize for py27. + return ''.join(map(encode_char, uri)) + + return encoder + + +encode = _create_str_encoder(False) +encode.__name__ = 'encode' +encode.__doc__ = """Encodes a full or relative URI according to RFC 3986. + +Escapes disallowed characters by percent-encoding them according +to RFC 3986. + +This function is faster in the average case than the similar +`quote` function found in urlib. It also strives to be easier +to use by assuming a sensible default of allowed characters. + +RFC 3986 defines a set of "unreserved" characters as well as a +set of "reserved" characters used as delimiters. + +Args: + uri: URI or part of a URI to encode. If this is a wide + string (i.e., six.text_type), it will be encoded to + a UTF-8 byte array and any multibyte sequences will + be percent-encoded as-is. + +Returns: + An escaped version of `uri`, where all disallowed characters + have been percent-encoded. + +""" + + +encode_value = _create_str_encoder(True) +encode_value.name = 'encode_value' +encode_value.__doc__ = """Encodes a value string according to RFC 3986. + +Escapes disallowed characters by percent-encoding them according +to RFC 3986. + +This function is faster in the average case than the similar +`quote` function found in urlib. It also strives to be easier +to use by assuming a sensible default of allowed characters. + +RFC 3986 defines a set of "unreserved" characters as well as a +set of "reserved" characters used as delimiters. + +This function keeps things simply by lumping all reserved +characters into a single set of "delimiters", and everything in +that set is escaped. + +Args: + uri: Value to encode. It is assumed not to cross delimiter + boundaries, and so any reserved URI delimiter characters + included in it will be escaped. If `value` is a wide + string (i.e., six.text_type), it will be encoded to + a UTF-8 byte array and any multibyte sequences will + be percent-encoded as-is. + +Returns: + An escaped version of `value`, where all disallowed characters + have been percent-encoded. + +""" diff --git a/poppy/common/util.py b/poppy/common/util.py new file mode 100644 index 00000000..5cec09e7 --- /dev/null +++ b/poppy/common/util.py @@ -0,0 +1,45 @@ +# Copyright (c) 2014 Rackspace, 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 pprint + + +class dict2obj(object): + + """Creates objects that behave much like a dictionaries.""" + def __init__(self, d): + for k in d: + if isinstance(d[k], dict): + self.__dict__[k] = dict2obj(d[k]) + elif isinstance(d[k], (list, tuple)): + l = [] + for v in d[k]: + if isinstance(v, dict): + l.append(dict2obj(v)) + else: + l.append(v) + self.__dict__[k] = l + else: + self.__dict__[k] = d[k] + + def __getitem__(self, name): + if name in self.__dict__: + return self.__dict__[name] + + def __iter__(self): + return iter(self.__dict__.keys()) + + def __repr__(self): + return pprint.pformat(self.__dict__) diff --git a/poppy/manager/base/__init__.py b/poppy/manager/base/__init__.py index a1616638..c0f0433a 100644 --- a/poppy/manager/base/__init__.py +++ b/poppy/manager/base/__init__.py @@ -14,11 +14,13 @@ # limitations under the License. from poppy.manager.base import driver +from poppy.manager.base import flavors +from poppy.manager.base import home from poppy.manager.base import services -from poppy.manager.base import v1 Driver = driver.ManagerDriverBase +FlavorsController = flavors.FlavorsControllerBase ServicesController = services.ServicesControllerBase -V1Controller = v1.V1ControllerBase +HomeController = home.HomeControllerBase diff --git a/poppy/manager/base/controller.py b/poppy/manager/base/controller.py index d9ff03e7..cd8fad18 100644 --- a/poppy/manager/base/controller.py +++ b/poppy/manager/base/controller.py @@ -29,3 +29,7 @@ class ManagerControllerBase(object): def __init__(self, driver): self._driver = driver + + @property + def driver(self): + return self._driver diff --git a/poppy/manager/base/driver.py b/poppy/manager/base/driver.py index 7600ea57..912cc22a 100644 --- a/poppy/manager/base/driver.py +++ b/poppy/manager/base/driver.py @@ -37,3 +37,8 @@ class ManagerDriverBase(object): def services_controller(self): """Returns the driver's services controller.""" raise NotImplementedError + + @abc.abstractproperty + def flavors_controller(self): + """Returns the driver's flavors controller.""" + raise NotImplementedError diff --git a/poppy/manager/base/flavors.py b/poppy/manager/base/flavors.py new file mode 100644 index 00000000..b7629609 --- /dev/null +++ b/poppy/manager/base/flavors.py @@ -0,0 +1,49 @@ +# Copyright (c) 2014 Rackspace, 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 abc + +import six + +from poppy.manager.base import controller + + +@six.add_metaclass(abc.ABCMeta) +class FlavorsControllerBase(controller.ManagerControllerBase): + + def __init__(self, manager): + super(FlavorsControllerBase, self).__init__(manager) + + self._storage = self.driver.storage.flavors_controller + + @property + def storage(self): + return self._storage + + @abc.abstractmethod + def list(self): + raise NotImplementedError + + @abc.abstractmethod + def get(self, flavor_id): + raise NotImplementedError + + @abc.abstractmethod + def add(self, flavor): + raise NotImplementedError + + @abc.abstractmethod + def delete(self, flavor_id, provider_id): + raise NotImplementedError diff --git a/poppy/manager/base/v1.py b/poppy/manager/base/home.py similarity index 87% rename from poppy/manager/base/v1.py rename to poppy/manager/base/home.py index 65af59be..a999af84 100644 --- a/poppy/manager/base/v1.py +++ b/poppy/manager/base/home.py @@ -21,9 +21,9 @@ from poppy.manager.base import controller @six.add_metaclass(abc.ABCMeta) -class V1ControllerBase(controller.ManagerControllerBase): +class HomeControllerBase(controller.ManagerControllerBase): def __init__(self, manager): - super(V1ControllerBase, self).__init__(manager) + super(HomeControllerBase, self).__init__(manager) @abc.abstractmethod def get(self): diff --git a/poppy/manager/default/controllers.py b/poppy/manager/default/controllers.py index 729f2132..107e4092 100644 --- a/poppy/manager/default/controllers.py +++ b/poppy/manager/default/controllers.py @@ -13,9 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from poppy.manager.default import flavors +from poppy.manager.default import home from poppy.manager.default import services -from poppy.manager.default import v1 +Home = home.DefaultHomeController +Flavors = flavors.DefaultFlavorsController Services = services.DefaultServicesController -V1 = v1.DefaultV1Controller diff --git a/poppy/manager/default/driver.py b/poppy/manager/default/driver.py index b76f3186..2a696d2c 100644 --- a/poppy/manager/default/driver.py +++ b/poppy/manager/default/driver.py @@ -30,5 +30,9 @@ class DefaultManagerDriver(base.Driver): return controllers.Services(self) @decorators.lazy_property(write=False) - def v1_controller(self): - return controllers.V1(self) + def home_controller(self): + return controllers.Home(self) + + @decorators.lazy_property(write=False) + def flavors_controller(self): + return controllers.Flavors(self) diff --git a/poppy/manager/default/flavors.py b/poppy/manager/default/flavors.py new file mode 100644 index 00000000..308f08d7 --- /dev/null +++ b/poppy/manager/default/flavors.py @@ -0,0 +1,33 @@ +# Copyright (c) 2014 Rackspace, 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 poppy.manager import base + + +class DefaultFlavorsController(base.FlavorsController): + def __init__(self, manager): + super(DefaultFlavorsController, self).__init__(manager) + + def list(self): + return self.storage.list() + + def get(self, flavor_id): + return self.storage.get(flavor_id) + + def add(self, new_flavor): + return self.storage.add(new_flavor) + + def delete(self, flavor_id): + return self.storage.delete(flavor_id) diff --git a/poppy/manager/default/v1.py b/poppy/manager/default/home.py similarity index 91% rename from poppy/manager/default/v1.py rename to poppy/manager/default/home.py index 05aa288f..76b06a2c 100644 --- a/poppy/manager/default/v1.py +++ b/poppy/manager/default/home.py @@ -36,9 +36,9 @@ JSON_HOME = { } -class DefaultV1Controller(base.V1Controller): +class DefaultHomeController(base.HomeController): def __init__(self, manager): - super(DefaultV1Controller, self).__init__(manager) + super(DefaultHomeController, self).__init__(manager) self.JSON_HOME = JSON_HOME diff --git a/poppy/manager/default/services.py b/poppy/manager/default/services.py index 9a468cdd..3d21cb42 100644 --- a/poppy/manager/default/services.py +++ b/poppy/manager/default/services.py @@ -21,7 +21,7 @@ class DefaultServicesController(base.ServicesController): def __init__(self, manager): super(DefaultServicesController, self).__init__(manager) - self.storage = self._driver.storage.service_controller + self.storage = self._driver.storage.services_controller def list(self, project_id, marker=None, limit=None): return self.storage.list(project_id, marker, limit) diff --git a/poppy/model/flavor.py b/poppy/model/flavor.py new file mode 100644 index 00000000..df584dd9 --- /dev/null +++ b/poppy/model/flavor.py @@ -0,0 +1,48 @@ +# Copyright (c) 2014 Rackspace, 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. + + +class Flavor(object): + + def __init__(self, + flavor_id, providers=[]): + + self._flavor_id = flavor_id + self._providers = providers + + @property + def flavor_id(self): + return self._flavor_id + + @property + def providers(self): + return self._providers + + +class Provider(object): + + def __init__(self, + provider_id, + provider_url): + self._provider_id = provider_id + self._provider_url = provider_url + + @property + def provider_id(self): + return self._provider_id + + @property + def provider_url(self): + return self._provider_url diff --git a/poppy/model/service.py b/poppy/model/service.py index aeabc31a..f794a846 100644 --- a/poppy/model/service.py +++ b/poppy/model/service.py @@ -21,7 +21,12 @@ VALID_STATUSES = [u'unknown', u'in_progress', u'deployed', u'failed'] class Service(common.DictSerializableModel): - def __init__(self, name, domains, origins, caching=[], restrictions=[]): + def __init__(self, + name, + domains, + origins, + caching=[], + restrictions=[]): self._name = name self._domains = domains self._origins = origins diff --git a/poppy/storage/base/__init__.py b/poppy/storage/base/__init__.py index 554589b7..3277a8c2 100644 --- a/poppy/storage/base/__init__.py +++ b/poppy/storage/base/__init__.py @@ -14,9 +14,11 @@ # limitations under the License. from poppy.storage.base import driver +from poppy.storage.base import flavors from poppy.storage.base import services Driver = driver.StorageDriverBase +FlavorsController = flavors.FlavorsControllerBase ServicesController = services.ServicesControllerBase diff --git a/poppy/storage/base/driver.py b/poppy/storage/base/driver.py index daadb298..e80a5b0a 100644 --- a/poppy/storage/base/driver.py +++ b/poppy/storage/base/driver.py @@ -52,6 +52,11 @@ class StorageDriverBase(object): raise NotImplementedError @abc.abstractproperty - def service_controller(self): + def services_controller(self): + """Returns the driver's hostname controller.""" + raise NotImplementedError + + @abc.abstractproperty + def flavors_controller(self): """Returns the driver's hostname controller.""" raise NotImplementedError diff --git a/poppy/storage/base/flavors.py b/poppy/storage/base/flavors.py new file mode 100644 index 00000000..e04d2d2d --- /dev/null +++ b/poppy/storage/base/flavors.py @@ -0,0 +1,43 @@ +# Copyright (c) 2014 Rackspace, 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 abc + +import six + +from poppy.storage.base import controller + + +@six.add_metaclass(abc.ABCMeta) +class FlavorsControllerBase(controller.StorageControllerBase): + + def __init__(self, driver): + super(FlavorsControllerBase, self).__init__(driver) + + @abc.abstractmethod + def list(self): + raise NotImplementedError + + @abc.abstractmethod + def get(self, flavor_id): + raise NotImplementedError + + @abc.abstractmethod + def add(self, flavor): + raise NotImplementedError + + @abc.abstractmethod + def delete(self, flavor_id): + raise NotImplementedError diff --git a/poppy/storage/cassandra/controllers.py b/poppy/storage/cassandra/controllers.py index 84211831..ed0f48be 100644 --- a/poppy/storage/cassandra/controllers.py +++ b/poppy/storage/cassandra/controllers.py @@ -23,6 +23,8 @@ Field Mappings: updated and documented in each controller class. """ +from poppy.storage.cassandra import flavors from poppy.storage.cassandra import services ServicesController = services.ServicesController +FlavorsController = flavors.FlavorsController diff --git a/poppy/storage/cassandra/driver.py b/poppy/storage/cassandra/driver.py index a9a9ea85..d02e395f 100644 --- a/poppy/storage/cassandra/driver.py +++ b/poppy/storage/cassandra/driver.py @@ -16,8 +16,8 @@ """Cassandra storage driver implementation.""" from cassandra import cluster +from cassandra import query -from poppy.common import decorators from poppy.openstack.common import log as logging from poppy.storage import base from poppy.storage.cassandra import controllers @@ -38,6 +38,7 @@ CASSANDRA_GROUP = 'drivers:storage:cassandra' def _connection(conf): cassandra_cluster = cluster.Cluster(conf.cluster) session = cassandra_cluster.connect(conf.keyspace) + session.row_factory = query.dict_factory return session @@ -54,15 +55,19 @@ class CassandraStorageDriver(base.Driver): def is_alive(self): return True - @decorators.lazy_property(write=False) + @property def connection(self): """Cassandra connection instance.""" return _connection(self.cassandra_conf) - @decorators.lazy_property(write=False) - def service_controller(self): + @property + def services_controller(self): return controllers.ServicesController(self) - @decorators.lazy_property(write=False) - def service_database(self): + @property + def flavors_controller(self): + return controllers.FlavorsController(self) + + @property + def database(self): return self.connection diff --git a/poppy/storage/cassandra/flavors.py b/poppy/storage/cassandra/flavors.py new file mode 100644 index 00000000..28380e15 --- /dev/null +++ b/poppy/storage/cassandra/flavors.py @@ -0,0 +1,107 @@ +# Copyright (c) 2014 Rackspace, 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 poppy.model import flavor +from poppy.storage import base + + +CQL_GET_ALL = ''' + SELECT flavor_id, + providers + FROM flavors +''' + +CQL_GET = ''' + SELECT flavor_id, + providers + FROM flavors + WHERE flavor_id = %(flavor_id)s +''' + +CQL_DELETE = ''' + DELETE FROM flavors + WHERE flavor_id = %(flavor_id)s +''' + +CQL_CREATE = ''' + INSERT INTO flavors (flavor_id, + providers) + VALUES (%(flavor_id)s, + %(providers)s) +''' + + +class FlavorsController(base.FlavorsController): + + @property + def session(self): + return self._driver.database + + def list(self): + """List the supported flavors.""" + + # get all + result = self.session.execute(CQL_GET_ALL) + + flavors = [ + flavor.Flavor( + f['flavor_id'], + [flavor.Provider(p_id, p_url) + for p_id, p_url in f['providers'].items()]) + for f in result] + + return flavors + + def get(self, flavor_id): + """Get the specified Flavor.""" + + args = { + 'flavor_id': flavor_id + } + result = self.session.execute(CQL_GET, args) + + flavors = [ + flavor.Flavor( + f['flavor_id'], + [flavor.Provider(p_id, p_url) + for p_id, p_url in f['providers'].items()] + ) + for f in result] + + if (len(flavors) == 1): + return flavors[0] + else: + raise LookupError("More than one flavor was retrieved.") + + def add(self, flavor): + """Add a new flavor.""" + + providers = dict((p.provider_id, p.provider_url) + for p in flavor.providers) + + args = { + 'flavor_id': flavor.flavor_id, + 'providers': providers + } + + self.session.execute(CQL_CREATE, args) + + def delete(self, flavor_id): + """Delete a flavor.""" + + args = { + 'flavor_id': flavor_id + } + self.session.execute(CQL_DELETE, args) diff --git a/poppy/storage/cassandra/schema.cql b/poppy/storage/cassandra/schema.cql index 96f224a7..a42a26bb 100644 --- a/poppy/storage/cassandra/schema.cql +++ b/poppy/storage/cassandra/schema.cql @@ -4,6 +4,7 @@ USE poppy; CREATE TABLE services ( project_id VARCHAR, service_name VARCHAR, + flavor_id VARCHAR, domains LIST, origins LIST, caching_rules LIST, @@ -11,3 +12,9 @@ CREATE TABLE services ( provider_details MAP, PRIMARY KEY (project_id, service_name) ); + +CREATE TABLE flavors ( + flavor_id VARCHAR, + providers MAP, + PRIMARY KEY (flavor_id) +); diff --git a/poppy/storage/cassandra/services.py b/poppy/storage/cassandra/services.py index aa250ded..c5652108 100644 --- a/poppy/storage/cassandra/services.py +++ b/poppy/storage/cassandra/services.py @@ -99,7 +99,7 @@ class ServicesController(base.ServicesController): @property def session(self): - return self._driver.service_database + return self._driver.database def list(self, project_id, marker=None, limit=None): diff --git a/poppy/storage/mockdb/controllers.py b/poppy/storage/mockdb/controllers.py index cc489f11..fbfeab97 100644 --- a/poppy/storage/mockdb/controllers.py +++ b/poppy/storage/mockdb/controllers.py @@ -23,6 +23,8 @@ Field Mappings: updated and documented in each controller class. """ +from poppy.storage.mockdb import flavors from poppy.storage.mockdb import services ServicesController = services.ServicesController +FlavorsController = flavors.FlavorsController diff --git a/poppy/storage/mockdb/driver.py b/poppy/storage/mockdb/driver.py index 33c49489..83e8d316 100644 --- a/poppy/storage/mockdb/driver.py +++ b/poppy/storage/mockdb/driver.py @@ -15,7 +15,6 @@ """Storage driver implementation.""" -from poppy.common import decorators from poppy.openstack.common import log as logging from poppy.storage import base from poppy.storage.mockdb import controllers @@ -48,15 +47,19 @@ class MockDBStorageDriver(base.Driver): def is_alive(self): return True - @decorators.lazy_property(write=False) + @property def connection(self): """Connection instance.""" return _connection() - @decorators.lazy_property(write=False) - def service_controller(self): + @property + def services_controller(self): return controllers.ServicesController(self) - @decorators.lazy_property(write=False) - def service_database(self): + @property + def flavors_controller(self): + return controllers.FlavorsController(self) + + @property + def database(self): return self.connection diff --git a/poppy/model/helpers/link.py b/poppy/storage/mockdb/flavors.py similarity index 65% rename from poppy/model/helpers/link.py rename to poppy/storage/mockdb/flavors.py index 7a52f703..ce23a7f2 100644 --- a/poppy/model/helpers/link.py +++ b/poppy/storage/mockdb/flavors.py @@ -13,19 +13,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -from poppy.model import common +from poppy.storage import base -class Link(common.DictSerializableModel): - - def __init__(self, href, rel): - self._href = href - self._rel = rel +class FlavorsController(base.FlavorsController): @property - def href(self): - return self._href + def session(self): + return self._driver.database - @property - def rel(self): - return self._rel + def list(self): + return [] + + def get(self, flavor_id): + return None + + def add(self, flavor): + pass + + def delete(self, flavor_id): + pass diff --git a/poppy/storage/mockdb/services.py b/poppy/storage/mockdb/services.py index bf379486..400d9e24 100644 --- a/poppy/storage/mockdb/services.py +++ b/poppy/storage/mockdb/services.py @@ -24,7 +24,7 @@ class ServicesController(base.ServicesController): @property def session(self): - return self._driver.service_database + return self._driver.database def list(self, project_id, marker=None, limit=None): services = [ diff --git a/poppy/storage/mongodb/controllers.py b/poppy/storage/mongodb/controllers.py index e63f13f8..d8716c73 100644 --- a/poppy/storage/mongodb/controllers.py +++ b/poppy/storage/mongodb/controllers.py @@ -22,6 +22,8 @@ Field Mappings: updated and documented in each controller class. """ +from poppy.storage.mongodb import flavors from poppy.storage.mongodb import services ServicesController = services.ServicesController +FlavorsController = flavors.FlavorsController diff --git a/poppy/storage/mongodb/driver.py b/poppy/storage/mongodb/driver.py index c58a21b2..fbe669aa 100644 --- a/poppy/storage/mongodb/driver.py +++ b/poppy/storage/mongodb/driver.py @@ -85,8 +85,12 @@ class MongoDBStorageDriver(base.Driver): return _connection(self.mongodb_conf) @decorators.lazy_property(write=False) - def service_controller(self): - return controllers.ServicesController(self.providers) + def services_controller(self): + return controllers.ServicesController(self) + + @decorators.lazy_property(write=False) + def flavors_controller(self): + return controllers.FlavorsController(self) @decorators.lazy_property(write=False) def service_database(self): @@ -97,3 +101,13 @@ class MongoDBStorageDriver(base.Driver): name = self.mongodb_conf.database + '_services' return self.connection[name] + + @decorators.lazy_property(write=False) + def flavor_database(self): + """Database dedicated to the "services" collection. + + The services collection is separated out into its own database. + """ + + name = self.mongodb_conf.database + '_flavors' + return self.connection[name] diff --git a/poppy/storage/mongodb/flavors.py b/poppy/storage/mongodb/flavors.py new file mode 100644 index 00000000..da303a98 --- /dev/null +++ b/poppy/storage/mongodb/flavors.py @@ -0,0 +1,34 @@ +# Copyright (c) 2014 Rackspace, 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 poppy.storage import base + + +class FlavorsController(base.FlavorsController): + @property + def session(self): + return self._driver.flavor_database + + def list(self): + return "" + + def get(self, flavor_id): + return "" + + def add(self, flavor): + return "" + + def delete(self, flavor_id, provider_id): + return "" diff --git a/poppy/transport/pecan/controllers/__init__.py b/poppy/transport/pecan/controllers/__init__.py index 64280c7e..18dea9f9 100644 --- a/poppy/transport/pecan/controllers/__init__.py +++ b/poppy/transport/pecan/controllers/__init__.py @@ -15,14 +15,8 @@ """Pecan Controllers""" -from poppy.transport.pecan.controllers import ping from poppy.transport.pecan.controllers import root -from poppy.transport.pecan.controllers import services -from poppy.transport.pecan.controllers import v1 # Hoist into package namespace Root = root.RootController -Ping = ping.PingController -Services = services.ServicesController -V1 = v1.ControllerV1 diff --git a/poppy/transport/pecan/controllers/base.py b/poppy/transport/pecan/controllers/base.py index 89307247..c19a1469 100644 --- a/poppy/transport/pecan/controllers/base.py +++ b/poppy/transport/pecan/controllers/base.py @@ -24,6 +24,10 @@ class Controller(rest.RestController): def __init__(self, driver): self._driver = driver + @property + def driver(self): + return self._driver + def add_controller(self, path, controller): setattr(self, path, controller) diff --git a/poppy/transport/pecan/controllers/v1/__init__.py b/poppy/transport/pecan/controllers/v1/__init__.py new file mode 100644 index 00000000..7c992fcd --- /dev/null +++ b/poppy/transport/pecan/controllers/v1/__init__.py @@ -0,0 +1,28 @@ +# Copyright (c) 2014 Rackspace, 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. + +"""Pecan v1.0 Controllers""" + +from poppy.transport.pecan.controllers.v1 import flavors +from poppy.transport.pecan.controllers.v1 import home +from poppy.transport.pecan.controllers.v1 import ping +from poppy.transport.pecan.controllers.v1 import services + + +# Hoist into package namespace +Home = home.HomeController +Services = services.ServicesController +Flavors = flavors.FlavorsController +Ping = ping.PingController diff --git a/poppy/transport/pecan/controllers/v1/flavors.py b/poppy/transport/pecan/controllers/v1/flavors.py new file mode 100644 index 00000000..02ce5a26 --- /dev/null +++ b/poppy/transport/pecan/controllers/v1/flavors.py @@ -0,0 +1,84 @@ +# Copyright (c) 2014 Rackspace, 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 json + +import pecan + +from poppy.common import uri +from poppy.transport.pecan.controllers import base +from poppy.transport.pecan.models.request import flavor as flavor_request +from poppy.transport.pecan.models.response import flavor as flavor_response +from poppy.transport.validators import helpers +from poppy.transport.validators.schemas import flavor as schema +from poppy.transport.validators.stoplight import decorators +from poppy.transport.validators.stoplight import helpers as stoplight_helpers +from poppy.transport.validators.stoplight import rule + + +class FlavorsController(base.Controller): + + @pecan.expose('json') + def get_all(self): + flavors_controller = self.driver.manager.flavors_controller + result = flavors_controller.list() + + flavor_list = [ + flavor_response.Model(item, pecan.request) for item in result] + + return flavor_list + + @pecan.expose('json') + def get_one(self, flavor_id): + flavors_controller = self.driver.manager.flavors_controller + result = flavors_controller.get(flavor_id) + + if result is not None: + print (result) + print('done') + return flavor_response.Model(result, pecan.request) + else: + pecan.response.status = 404 + + @pecan.expose('json') + @decorators.validate( + request=rule.Rule( + helpers.json_matches_schema( + schema.FlavorSchema.get_schema("flavor", "POST")), + helpers.abort_with_message, + stoplight_helpers.pecan_getter)) + def post(self): + flavors_controller = self.driver.manager.flavors_controller + flavor_json = json.loads(pecan.request.body.decode('utf-8')) + try: + new_flavor = flavor_request.load_from_json(flavor_json) + flavors_controller.add(new_flavor) + + # form the success response + flavor_url = str( + uri.encode(u'{0}/v1.0/flavors/{1}'.format( + pecan.request.host_url, + new_flavor.flavor_id))) + pecan.response.status = 204 + pecan.response.headers["Location"] = flavor_url + + except Exception: + pecan.response.status = 400 + + @pecan.expose('json') + def delete(self, flavor_id): + flavors_controller = self.driver.manager.flavors_controller + flavors_controller.delete(flavor_id) + pecan.response.status = 204 diff --git a/poppy/transport/pecan/controllers/v1.py b/poppy/transport/pecan/controllers/v1/home.py similarity index 83% rename from poppy/transport/pecan/controllers/v1.py rename to poppy/transport/pecan/controllers/v1/home.py index 61cf15ff..1f87f293 100644 --- a/poppy/transport/pecan/controllers/v1.py +++ b/poppy/transport/pecan/controllers/v1/home.py @@ -18,9 +18,9 @@ import pecan from poppy.transport.pecan.controllers import base -class ControllerV1(base.Controller): +class HomeController(base.Controller): @pecan.expose('json') def get(self): - v1_controller = self._driver.manager.v1_controller - return v1_controller.get() + home_controller = self._driver.manager.home_controller + return home_controller.get() diff --git a/poppy/transport/pecan/controllers/ping.py b/poppy/transport/pecan/controllers/v1/ping.py similarity index 100% rename from poppy/transport/pecan/controllers/ping.py rename to poppy/transport/pecan/controllers/v1/ping.py diff --git a/poppy/transport/pecan/controllers/services.py b/poppy/transport/pecan/controllers/v1/services.py similarity index 100% rename from poppy/transport/pecan/controllers/services.py rename to poppy/transport/pecan/controllers/v1/services.py diff --git a/poppy/transport/pecan/driver.py b/poppy/transport/pecan/driver.py index cecd5c69..77d00db6 100644 --- a/poppy/transport/pecan/driver.py +++ b/poppy/transport/pecan/driver.py @@ -21,6 +21,7 @@ import pecan from poppy.openstack.common import log from poppy import transport from poppy.transport.pecan import controllers +from poppy.transport.pecan.controllers import v1 from poppy.transport.pecan import hooks @@ -49,16 +50,17 @@ class PecanTransportDriver(transport.Driver): def _setup_app(self): root_controller = controllers.Root(self) + home_controller = v1.Home(self) + + root_controller.add_controller('v1.0', home_controller) + + home_controller.add_controller('ping', v1.Ping(self)) + home_controller.add_controller('services', v1.Services(self)) + home_controller.add_controller('flavors', v1.Flavors(self)) + pecan_hooks = [hooks.Context()] - self._app = pecan.make_app(root_controller, hooks=pecan_hooks) - controller_v1 = controllers.V1(self) - root_controller.add_controller('v1.0', controller_v1) - - controller_v1.add_controller('ping', controllers.Ping(self)) - controller_v1.add_controller('services', controllers.Services(self)) - def listen(self): LOG.info( 'Serving on host %(bind)s:%(port)s', diff --git a/poppy/transport/pecan/models/request/flavor.py b/poppy/transport/pecan/models/request/flavor.py new file mode 100644 index 00000000..95638a25 --- /dev/null +++ b/poppy/transport/pecan/models/request/flavor.py @@ -0,0 +1,35 @@ +# Copyright (c) 2014 Rackspace, 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 poppy.model import flavor + + +def load_from_json(json_data): + + flavor_id = json_data['id'] + providers = [] + + for p in json_data['providers']: + provider_id = p['provider'] + provider_url = [item['href'] + for item in p['links'] + if item['rel'] == 'provider_url'][0] + + provider = flavor.Provider(provider_id, provider_url) + providers.append(provider) + + new_flavor = flavor.Flavor(flavor_id, providers) + + return new_flavor diff --git a/poppy/transport/pecan/models/response/flavor.py b/poppy/transport/pecan/models/response/flavor.py new file mode 100644 index 00000000..a844bb16 --- /dev/null +++ b/poppy/transport/pecan/models/response/flavor.py @@ -0,0 +1,46 @@ +# Copyright (c) 2014 Rackspace, 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. + +try: + import ordereddict as collections +except ImportError: + import collections + +from poppy.transport.pecan.models.response import link + + +class Model(collections.OrderedDict): + + def __init__(self, flavor, request): + super(Model, self).__init__() + + self['id'] = flavor.flavor_id + self['providers'] = [] + + for x in flavor.providers: + provider = collections.OrderedDict() + provider['provider'] = x.provider_id + provider['links'] = [] + provider['links'].append( + link.Model(x.provider_url, 'provider_url')) + + self['providers'].append(provider) + + self['links'] = [] + self['links'].append( + link.Model( + u'{0}/v1.0/flavors/{1}'.format(request.host_url, + flavor.flavor_id), + 'self')) diff --git a/poppy/transport/pecan/models/response/link.py b/poppy/transport/pecan/models/response/link.py index c30fd3bb..be0548a1 100644 --- a/poppy/transport/pecan/models/response/link.py +++ b/poppy/transport/pecan/models/response/link.py @@ -24,4 +24,4 @@ class Model(collections.OrderedDict): def __init__(self, href, rel): super(Model, self).__init__() self['href'] = href - self['rel'] = rel \ No newline at end of file + self['rel'] = rel diff --git a/poppy/transport/validators/helpers.py b/poppy/transport/validators/helpers.py index c4b1c8ae..555553a6 100644 --- a/poppy/transport/validators/helpers.py +++ b/poppy/transport/validators/helpers.py @@ -196,6 +196,11 @@ def is_valid_service_name(service_name): pass +@decorators.validation_function +def is_valid_flavor_id(flavor_id): + pass + + def abort_with_message(error_info): pecan.abort(400, detail=getattr(error_info, "message", ""), headers={'Content-Type': "application/json"}) diff --git a/poppy/transport/validators/schemas/flavor.py b/poppy/transport/validators/schemas/flavor.py new file mode 100644 index 00000000..4e7c46d7 --- /dev/null +++ b/poppy/transport/validators/schemas/flavor.py @@ -0,0 +1,69 @@ +# Copyright (c) 2014 Rackspace, 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 poppy.transport.validators import schema_base + + +class FlavorSchema(schema_base.SchemaBase): + + """JSON Schmema validation for /flavor.""" + + schema = { + "flavor": { + "POST": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 3, + "maxLength": 64, + "required": True + }, + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "required": True + }, + "links": { + "type": "array", + "items": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri", + "required": True + }, + "rel": { + "type": "string", + "enum": ["provider_url"], + "required": True + } + } + }, + "minItems": 1 + } + } + }, + "minItems": 0 + } + } + } + } + } diff --git a/requirements/common.txt b/requirements/common.txt index 0a02423f..4c9ccbc4 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -5,6 +5,7 @@ netaddr>=0.7.6 jsonschema>=1.3.0,!=1.4.0 iso8601>=0.1.8 msgpack-python +ordereddict python-keystoneclient>=0.4.1 WebOb>=1.2.3,<1.3 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index c0565acd..81a9b945 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,7 @@ -r common.txt -r docs.txt --r storage/cassandra.txt -r transport/pecan.txt +-r storage/cassandra.txt +-r provider/cloudfront.txt -r provider/fastly.txt -r provider/maxcdn.txt diff --git a/tests/functional/transport/pecan/base.py b/tests/functional/transport/pecan/base.py index 810f912d..0936f7fc 100644 --- a/tests/functional/transport/pecan/base.py +++ b/tests/functional/transport/pecan/base.py @@ -35,6 +35,6 @@ class BaseFunctionalTest(base.TestCase): cfg.CONF(args=[], default_config_files=[conf_path]) poppy_wsgi = bootstrap.Bootstrap(cfg.CONF).transport.app - self.app = webtest.TestApp(poppy_wsgi) + self.app = webtest.app.TestApp(poppy_wsgi) FunctionalTest = BaseFunctionalTest diff --git a/tests/functional/transport/pecan/controllers/data_create_flavor.json b/tests/functional/transport/pecan/controllers/data_create_flavor.json new file mode 100644 index 00000000..d98bedaa --- /dev/null +++ b/tests/functional/transport/pecan/controllers/data_create_flavor.json @@ -0,0 +1,57 @@ +{ + "one_provider": { + "id" : "asia", + "providers" : [ + { + "provider" : "ChinaCache", + "links": [ + { + "href": "http://www.chinacache.com", + "rel": "provider_url" + } + ] + } + ] + }, + "many_providers": { + "id" : "asia", + "providers" : [ + { + "provider" : "ChinaCache", + "links": [ + { + "href": "http://www.chinacache.com", + "rel": "provider_url" + } + ] + }, + { + "provider" : "KiwiCache", + "links": [ + { + "href": "http://www.cdn.co.nz", + "rel": "provider_url" + } + ] + } + ] + }, + "unicode_provider": { + "id" : "위키백과대문", + "providers" : [ + { + "provider" : "KoreanCache", + "links": [ + { + "href": "http://www.위키백과대문.com", + "rel": "provider_url" + } + ] + } + ] + }, + "no_providers": { + "id" : "asia", + "providers" : [] + } +} \ No newline at end of file diff --git a/tests/functional/transport/pecan/controllers/data_create_flavor_bad.json b/tests/functional/transport/pecan/controllers/data_create_flavor_bad.json new file mode 100644 index 00000000..1e227e68 --- /dev/null +++ b/tests/functional/transport/pecan/controllers/data_create_flavor_bad.json @@ -0,0 +1,21 @@ +{ + "no_body": { + }, + "no_flavor_id": { + "x_id" : "asia" + }, + "invalid_fields": { + "id" : "asia", + "providers" : [ + { + "invalid_field" : "ChinaCache", + "links": [ + { + "href": "http://www.chinacache.com", + "rel": "invalid_url" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/functional/transport/pecan/controllers/test_flavors.py b/tests/functional/transport/pecan/controllers/test_flavors.py new file mode 100644 index 00000000..47ba772a --- /dev/null +++ b/tests/functional/transport/pecan/controllers/test_flavors.py @@ -0,0 +1,93 @@ +# Copyright (c) 2014 Rackspace, 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 json +import uuid + +import ddt +import mock + +from poppy.common import uri +from poppy.manager.default import flavors as manager +from poppy.transport.pecan.models.request import flavor +from tests.functional.transport.pecan import base + + +@ddt.ddt +class FlavorControllerTest(base.FunctionalTest): + + def test_get_all(self): + response = self.app.get('/v1.0/flavors') + self.assertEqual(200, response.status_code) + + @ddt.file_data('data_create_flavor.json') + @mock.patch.object(manager.DefaultFlavorsController, 'storage') + def test_get_one(self, value, mock_manager): + + return_flavor = flavor.load_from_json(value) + + # mock the storage response + mock_response = return_flavor + mock_manager.get.return_value = mock_response + + url = u'/v1.0/flavors/{0}'.format(uri.encode(value['id'])) + response = self.app.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_not_found(self): + response = self.app.get('/v1.0/flavors/{0}'.format(uuid.uuid1()), + status=404, + expect_errors=True) + + self.assertEqual(404, response.status_code) + + @ddt.file_data('data_create_flavor_bad.json') + def test_create_bad_data(self, value): + + response = self.app.post('/v1.0/flavors', + params=json.dumps(value), + headers={"Content-Type": "application/json"}, + status=400, + expect_errors=True) + + self.assertEqual(400, response.status_code) + + @ddt.file_data('data_create_flavor.json') + @mock.patch.object(manager.DefaultFlavorsController, 'storage') + def test_create_exception(self, value, mock_storage): + mock_storage.add.side_effect = Exception() + + # create with good data + response = self.app.post('/v1.0/flavors', + params=json.dumps(value), + headers={"Content-Type": "application/json"}, + expect_errors=True) + + self.assertEqual(400, response.status_code) + + @ddt.file_data('data_create_flavor.json') + def test_create(self, value): + + # create with good data + response = self.app.post('/v1.0/flavors', + params=json.dumps(value), + headers={"Content-Type": "application/json"}) + self.assertEqual(204, response.status_code) + + def test_delete(self): + response = self.app.delete('/v1.0/flavors/{0}'.format(uuid.uuid1())) + + self.assertEqual(204, response.status_code) diff --git a/tests/functional/transport/pecan/controllers/test_v1_controller.py b/tests/functional/transport/pecan/controllers/test_home_controller.py similarity index 85% rename from tests/functional/transport/pecan/controllers/test_v1_controller.py rename to tests/functional/transport/pecan/controllers/test_home_controller.py index 72443959..3f46256f 100644 --- a/tests/functional/transport/pecan/controllers/test_v1_controller.py +++ b/tests/functional/transport/pecan/controllers/test_home_controller.py @@ -13,15 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from poppy.manager.default import v1 +from poppy.manager.default import home from tests.functional.transport.pecan import base -class V1ControllerTest(base.FunctionalTest): +class HomeControllerTest(base.FunctionalTest): def test_get_all(self): response = self.app.get('/v1.0/00001') self.assertEqual(200, response.status_code) # Temporary until actual implementation - self.assertEqual(v1.JSON_HOME, response.json) + self.assertEqual(home.JSON_HOME, response.json) diff --git a/tests/functional/transport/pecan/hooks/test_context.py b/tests/functional/transport/pecan/hooks/test_context.py index 82639a8a..113194ac 100644 --- a/tests/functional/transport/pecan/hooks/test_context.py +++ b/tests/functional/transport/pecan/hooks/test_context.py @@ -15,7 +15,7 @@ import uuid -from poppy.manager.default import v1 +from poppy.manager.default import home from tests.functional.transport.pecan import base @@ -33,11 +33,11 @@ class ContextHookTest(base.FunctionalTest): self.assertEqual(200, response.status_code) # Temporary until actual implementation - self.assertEqual(v1.JSON_HOME, response.json) + self.assertEqual(home.JSON_HOME, response.json) def test_project_id_in_url(self): response = self.app.get('/v1.0/000001', headers=self.headers) self.assertEqual(200, response.status_code) # Temporary until actual implementation - self.assertEqual(v1.JSON_HOME, response.json) + self.assertEqual(home.JSON_HOME, response.json) diff --git a/tests/unit/common/test_uri.py b/tests/unit/common/test_uri.py new file mode 100644 index 00000000..28294919 --- /dev/null +++ b/tests/unit/common/test_uri.py @@ -0,0 +1,44 @@ +# Copyright (c) 2014 Rackspace, 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 poppy.common import uri +from tests.unit import base + + +class URITest(base.TestCase): + + def test_uri_encode(self): + url = 'http://example.com/v1/fizbit/messages?limit=3&echo=true' + self.assertEqual(uri.encode(url), url) + + url = 'http://example.com/v1/fiz bit/messages' + expected = 'http://example.com/v1/fiz%20bit/messages' + self.assertEqual(uri.encode(url), expected) + + url = u'http://example.com/v1/fizbit/messages?limit=3&e\u00e7ho=true' + expected = ('http://example.com/v1/fizbit/messages' + '?limit=3&e%C3%A7ho=true') + self.assertEqual(uri.encode(url), expected) + + def test_uri_encode_value(self): + self.assertEqual(uri.encode_value('abcd'), 'abcd') + self.assertEqual(uri.encode_value(u'abcd'), u'abcd') + self.assertEqual(uri.encode_value(u'ab cd'), u'ab%20cd') + self.assertEqual(uri.encode_value(u'\u00e7'), '%C3%A7') + self.assertEqual(uri.encode_value(u'\u00e7\u20ac'), + '%C3%A7%E2%82%AC') + self.assertEqual(uri.encode_value('ab/cd'), 'ab%2Fcd') + self.assertEqual(uri.encode_value('ab+cd=42,9'), + 'ab%2Bcd%3D42%2C9') diff --git a/tests/unit/manager/default/test_driver.py b/tests/unit/manager/default/test_driver.py index 12bb655e..65dae0c7 100644 --- a/tests/unit/manager/default/test_driver.py +++ b/tests/unit/manager/default/test_driver.py @@ -17,6 +17,7 @@ import mock from oslo.config import cfg from poppy.manager.default import driver +from poppy.manager.default import flavors from poppy.manager.default import services from tests.unit import base @@ -36,3 +37,8 @@ class DefaultManagerDriverTests(base.TestCase): sc = self.driver.services_controller self.assertIsInstance(sc, services.DefaultServicesController) + + def test_flavors_controller(self): + sc = self.driver.flavors_controller + + self.assertIsInstance(sc, flavors.DefaultFlavorsController) diff --git a/tests/unit/manager/default/test_flavors.py b/tests/unit/manager/default/test_flavors.py new file mode 100644 index 00000000..541e4e7e --- /dev/null +++ b/tests/unit/manager/default/test_flavors.py @@ -0,0 +1,77 @@ +# Copyright (c) 2014 Rackspace, 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 uuid + +import mock +from oslo.config import cfg + +from poppy.manager.default import driver +from poppy.manager.default import flavors +from poppy.model import flavor +from tests.unit import base + + +class DefaultManagerFlavorTests(base.TestCase): + @mock.patch('poppy.storage.base.driver.StorageDriverBase') + @mock.patch('poppy.provider.base.driver.ProviderDriverBase') + def setUp(self, mock_driver, mock_provider): + super(DefaultManagerFlavorTests, self).setUp() + + # create mocked config and driver + conf = cfg.ConfigOpts() + manager_driver = driver.DefaultManagerDriver(conf, + mock_driver, + mock_provider) + + # stubbed driver + self.fc = flavors.DefaultFlavorsController(manager_driver) + + def test_list(self): + + results = self.fc.list() + + # ensure the manager calls the storage driver with the appropriate data + self.fc.storage.list.assert_called_once() + + # and that a list of flavors objects are returned + [self.assertIsInstance(x, flavor.Flavor) for x in results] + + def test_get(self): + flavor_id = uuid.uuid1() + self.fc.get(flavor_id) + + # ensure the manager calls the storage driver with the appropriate data + self.fc.storage.get.assert_called_once_with(flavor_id) + + def test_add(self): + flavor_id = uuid.uuid1() + providers = [] + + providers.append(flavor.Provider(uuid.uuid1(), uuid.uuid1())) + providers.append(flavor.Provider(uuid.uuid1(), uuid.uuid1())) + providers.append(flavor.Provider(uuid.uuid1(), uuid.uuid1())) + + new_flavor = flavor.Flavor(flavor_id, providers) + + self.fc.add(new_flavor) + self.fc.storage.add.assert_called_once_with(new_flavor) + + def test_delete(self): + flavor_id = uuid.uuid1() + self.fc.delete(flavor_id) + + # ensure the manager calls the storage driver with the appropriate data + self.fc.storage.delete.assert_called_once_with(flavor_id) diff --git a/tests/unit/model/helpers/test_link.py b/tests/unit/model/helpers/test_link.py deleted file mode 100644 index 4982b214..00000000 --- a/tests/unit/model/helpers/test_link.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2014 Rackspace, 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 ddt - -from poppy.model.helpers import link -from tests.unit import base - - -@ddt.ddt -class TestLink(base.TestCase): - - def test_link(self): - - href = 'http://www.mywebsite.com/' - rel = 'nofollow' - mylink = link.Link(href, rel) - - # test all properties - # href - self.assertEqual(mylink.href, href) - self.assertRaises(AttributeError, setattr, mylink, 'href', href) - - # rel - self.assertEqual(mylink.rel, rel) - self.assertRaises(AttributeError, setattr, mylink, 'href', rel) diff --git a/tests/unit/model/test_flavors.py b/tests/unit/model/test_flavors.py new file mode 100644 index 00000000..12015944 --- /dev/null +++ b/tests/unit/model/test_flavors.py @@ -0,0 +1,60 @@ +# Copyright (c) 2014 Rackspace, 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 uuid + +import ddt + +from poppy.model import flavor +from tests.unit import base + + +@ddt.ddt +class TestFlavorModel(base.TestCase): + + def setUp(self): + super(TestFlavorModel, self).setUp() + + def test_create_with_providers(self): + self.flavor_id = uuid.uuid1() + self.providers = [] + + self.providers.append(flavor.Provider(uuid.uuid1(), uuid.uuid1())) + self.providers.append(flavor.Provider(uuid.uuid1(), uuid.uuid1())) + self.providers.append(flavor.Provider(uuid.uuid1(), uuid.uuid1())) + + my_flavor = flavor.Flavor(self.flavor_id, self.providers) + + # test all properties + self.assertEqual(my_flavor.flavor_id, self.flavor_id) + self.assertEqual(len(my_flavor.providers), len(self.providers)) + + def test_create_no_providers(self): + self.flavor_id = uuid.uuid1() + + my_flavor = flavor.Flavor(self.flavor_id) + + # test all properties + self.assertEqual(my_flavor.flavor_id, self.flavor_id) + self.assertEqual(len(my_flavor.providers), 0) + + def test_provider(self): + provider_id = uuid.uuid1() + provider_url = uuid.uuid1() + + new_provider = flavor.Provider(provider_id, provider_url) + + self.assertEqual(new_provider.provider_id, provider_id) + self.assertEqual(new_provider.provider_url, provider_url) diff --git a/tests/unit/model/test_service.py b/tests/unit/model/test_service.py index 4837173f..558ae199 100644 --- a/tests/unit/model/test_service.py +++ b/tests/unit/model/test_service.py @@ -32,6 +32,7 @@ class TestServiceModel(base.TestCase): super(TestServiceModel, self).setUp() self.service_name = uuid.uuid1() + self.flavorRef = "strawberry" self.myorigins = [] self.mydomains = [] @@ -62,6 +63,9 @@ class TestServiceModel(base.TestCase): myservice.name = changed_service_name self.assertEqual(myservice.name, changed_service_name) + # flavorRef + # self.assertEqual(myservice.flavorRef, self.flavorRef) + # domains self.assertEqual(myservice.domains, self.mydomains) myservice.domains = [] diff --git a/tests/unit/storage/cassandra/data_get_flavor.json b/tests/unit/storage/cassandra/data_get_flavor.json new file mode 100644 index 00000000..543ae725 --- /dev/null +++ b/tests/unit/storage/cassandra/data_get_flavor.json @@ -0,0 +1,22 @@ +{ + "one_provider": [{ + "flavor_id" : "europe", + "providers" : { + "fastly" : "www.fastly.net" + } + }], + "multiple_providers": [{ + "flavor_id" : "europe", + "providers" : { + "fastly" : "www.fastly.net", + "maxcdn" : "www.maxcdn.net" + } + }], + "no_providers": [{ + "flavor_id" : "europe", + "providers" : {} + }] +} + + + diff --git a/tests/unit/storage/cassandra/data_get_flavor_bad.json b/tests/unit/storage/cassandra/data_get_flavor_bad.json new file mode 100644 index 00000000..0de0d609 --- /dev/null +++ b/tests/unit/storage/cassandra/data_get_flavor_bad.json @@ -0,0 +1,20 @@ +{ + "multiple_flavors": [ + { + "flavor_id" : "europe", + "providers" : { + "fastly" : "www.fastly.net" + } + }, + { + "flavor_id" : "europe", + "providers" : { + "fastly" : "www.fastly.net", + "maxcdn" : "www.maxcdn.net" + } + } + ] +} + + + diff --git a/tests/unit/storage/cassandra/data_list_flavors.json b/tests/unit/storage/cassandra/data_list_flavors.json new file mode 100644 index 00000000..ec3c7d83 --- /dev/null +++ b/tests/unit/storage/cassandra/data_list_flavors.json @@ -0,0 +1,28 @@ +{ + "one_flavor": [ + { + "flavor_id" : "europe", + "providers" : { + "fastly" : "www.fastly.net" + } + } + ], + "multiple_flavors": [ + { + "flavor_id" : "europe", + "providers" : { + "fastly" : "www.fastly.net", + "maxcdn" : "www.maxcdn.net" + } + }, + { + "flavor_id" : "europe", + "providers" : { + "fastly" : "www.fastly.net" + } + } + ], + "no_flavors": [ + + ] +} diff --git a/tests/unit/storage/cassandra/test_driver.py b/tests/unit/storage/cassandra/test_driver.py index 0b602c10..88f3dfba 100644 --- a/tests/unit/storage/cassandra/test_driver.py +++ b/tests/unit/storage/cassandra/test_driver.py @@ -18,6 +18,7 @@ import mock from oslo.config import cfg from poppy.storage.cassandra import driver +from poppy.storage.cassandra import flavors from poppy.storage.cassandra import services from tests.unit import base @@ -30,11 +31,11 @@ CASSANDRA_OPTIONS = [ ] -class CassandraStorageServiceTests(base.TestCase): +class CassandraStorageDriverTests(base.TestCase): @mock.patch.object(driver, 'CASSANDRA_OPTIONS', new=CASSANDRA_OPTIONS) def setUp(self): - super(CassandraStorageServiceTests, self).setUp() + super(CassandraStorageDriverTests, self).setUp() conf = cfg.ConfigOpts() self.cassandra_driver = driver.CassandraStorageDriver(conf) @@ -55,13 +56,20 @@ class CassandraStorageServiceTests(base.TestCase): mock_cluster.assert_called_with('mock_poppy') def test_service_controller(self): - sc = self.cassandra_driver.service_controller + sc = self.cassandra_driver.services_controller self.assertEqual( isinstance(sc, services.ServicesController), True) + def test_flavor_controller(self): + sc = self.cassandra_driver.flavors_controller + + self.assertEqual( + isinstance(sc, flavors.FlavorsController), + True) + @mock.patch.object(cassandra.cluster.Cluster, 'connect') - def test_service_database(self, mock_cluster): - self.cassandra_driver.service_database + def test_database(self, mock_cluster): + self.cassandra_driver.database mock_cluster.assert_called_with('mock_poppy') diff --git a/tests/unit/storage/cassandra/test_flavors.py b/tests/unit/storage/cassandra/test_flavors.py new file mode 100644 index 00000000..c099e18d --- /dev/null +++ b/tests/unit/storage/cassandra/test_flavors.py @@ -0,0 +1,108 @@ +# Copyright (c) 2014 Rackspace, 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 uuid + +import cassandra +import ddt +import mock +from oslo.config import cfg + +from poppy.storage.cassandra import driver +from poppy.storage.cassandra import flavors +from poppy.transport.pecan.models.request import flavor +from tests.unit import base + + +@ddt.ddt +class CassandraStorageFlavorsTests(base.TestCase): + + def setUp(self): + super(CassandraStorageFlavorsTests, self).setUp() + + self.flavor_id = uuid.uuid1() + + # create mocked config and driver + conf = cfg.ConfigOpts() + cassandra_driver = driver.CassandraStorageDriver(conf) + + # stubbed cassandra driver + self.fc = flavors.FlavorsController(cassandra_driver) + + @ddt.file_data('data_get_flavor.json') + @mock.patch.object(flavors.FlavorsController, 'session') + @mock.patch.object(cassandra.cluster.Session, 'execute') + def test_get_flavor(self, value, mock_session, mock_execute): + + # mock the response from cassandra + mock_execute.execute.return_value = value + + actual_response = self.fc.get(value[0]['flavor_id']) + + self.assertEqual(actual_response.flavor_id, value[0]['flavor_id']) + self.assertEqual( + len(actual_response.providers), len(value[0]['providers'])) + + @ddt.file_data('data_get_flavor_bad.json') + @mock.patch.object(flavors.FlavorsController, 'session') + @mock.patch.object(cassandra.cluster.Session, 'execute') + def test_get_flavor_error(self, value, mock_session, mock_execute): + + # mock the response from cassandra + mock_execute.execute.return_value = value + + self.assertRaises( + LookupError, lambda: self.fc.get(value[0]['flavor_id'])) + + @ddt.file_data('../data/data_create_flavor.json') + @mock.patch.object(flavors.FlavorsController, 'session') + @mock.patch.object(cassandra.cluster.Session, 'execute') + def test_add_flavor(self, value, mock_session, mock_execute): + # mock the response from cassandra + mock_execute.execute.return_value = value + + new_flavor = flavor.load_from_json(value) + + actual_response = self.fc.add(new_flavor) + + self.assertEqual(actual_response, None) + + @ddt.file_data('data_list_flavors.json') + @mock.patch.object(flavors.FlavorsController, 'session') + @mock.patch.object(cassandra.cluster.Session, 'execute') + def test_list_flavors(self, value, mock_session, mock_execute): + # mock the response from cassandra + mock_execute.execute.return_value = value + + actual_response = self.fc.list() + + # confirm the correct number of results are returned + self.assertEqual(len(actual_response), len(value)) + + # confirm the flavor id is returned for each expectation + for i, r in enumerate(actual_response): + self.assertEqual(r.flavor_id, value[i]['flavor_id']) + + @mock.patch.object(flavors.FlavorsController, 'session') + @mock.patch.object(cassandra.cluster.Session, 'execute') + def test_delete_flavor(self, mock_session, mock_execute): + actual_response = self.fc.delete(self.flavor_id) + + self.assertEqual(actual_response, None) + + @mock.patch.object(cassandra.cluster.Cluster, 'connect') + def test_session(self, mock_flavor_database): + session = self.fc.session + self.assertNotEqual(session, None) diff --git a/tests/unit/storage/cassandra/test_services.py b/tests/unit/storage/cassandra/test_services.py index b8a9718c..6b3bd460 100644 --- a/tests/unit/storage/cassandra/test_services.py +++ b/tests/unit/storage/cassandra/test_services.py @@ -66,7 +66,7 @@ class CassandraStorageServiceTests(base.TestCase): self.assertRaises(ValueError, self.sc.get, self.project_id, self.service_name) - @ddt.file_data('data_create_service.json') + @ddt.file_data('../data/data_create_service.json') @mock.patch.object(services.ServicesController, 'session') @mock.patch.object(cassandra.cluster.Session, 'execute') def test_create_service(self, value, mock_session, mock_execute): @@ -106,7 +106,7 @@ class CassandraStorageServiceTests(base.TestCase): # into the driver to respond to this call self.assertEqual(actual_response, None) - @ddt.file_data('data_update_service.json') + @ddt.file_data('../data/data_update_service.json') @mock.patch.object(services.ServicesController, 'session') @mock.patch.object(cassandra.cluster.Session, 'execute') def test_update_service(self, value, mock_session, mock_execute): diff --git a/tests/unit/storage/data/data_create_flavor.json b/tests/unit/storage/data/data_create_flavor.json new file mode 100644 index 00000000..162b942c --- /dev/null +++ b/tests/unit/storage/data/data_create_flavor.json @@ -0,0 +1,43 @@ +{ + "one_provider": { + "id" : "asia", + "providers" : [ + { + "provider" : "ChinaCache", + "links": [ + { + "href": "http://www.chinacache.com", + "rel": "provider_url" + } + ] + } + ] + }, + "many_providers": { + "id" : "asia", + "providers" : [ + { + "provider" : "ChinaCache", + "links": [ + { + "href": "http://www.chinacache.com", + "rel": "provider_url" + } + ] + }, + { + "provider" : "KiwiCache", + "links": [ + { + "href": "http://www.cdn.co.nz", + "rel": "provider_url" + } + ] + } + ] + }, + "no_providers": { + "id" : "asia", + "providers" : [] + } +} \ No newline at end of file diff --git a/tests/unit/storage/cassandra/data_create_service.json b/tests/unit/storage/data/data_create_service.json similarity index 100% rename from tests/unit/storage/cassandra/data_create_service.json rename to tests/unit/storage/data/data_create_service.json diff --git a/tests/unit/storage/cassandra/data_update_service.json b/tests/unit/storage/data/data_update_service.json similarity index 100% rename from tests/unit/storage/cassandra/data_update_service.json rename to tests/unit/storage/data/data_update_service.json diff --git a/tests/unit/storage/mockdb/test_mockdb_driver.py b/tests/unit/storage/mockdb/test_driver.py similarity index 63% rename from tests/unit/storage/mockdb/test_mockdb_driver.py rename to tests/unit/storage/mockdb/test_driver.py index e8aa9b24..62f0fca5 100644 --- a/tests/unit/storage/mockdb/test_mockdb_driver.py +++ b/tests/unit/storage/mockdb/test_driver.py @@ -19,11 +19,24 @@ from poppy.storage.mockdb import driver from tests.unit import base -class MockDBDriverTest(base.TestCase): +class MockDBStorageDriverTests(base.TestCase): + + def setUp(self): + super(MockDBStorageDriverTests, self).setUp() - def test_mockdb_driver_working(self): self.mockdb_driver = driver.MockDBStorageDriver(cfg.CONF) + + def test_is_alive(self): self.assertTrue(self.mockdb_driver.is_alive()) - self.assertTrue(self.mockdb_driver.service_database is None) + + def test_database(self): + self.assertTrue(self.mockdb_driver.database is None) + + def test_connection(self): self.assertTrue(self.mockdb_driver.connection is None) - self.assertTrue(self.mockdb_driver.service_controller.session is None) + + def test_services_controller(self): + self.assertTrue(self.mockdb_driver.services_controller.session is None) + + def test_flavors_controller(self): + self.assertTrue(self.mockdb_driver.flavors_controller.session is None) diff --git a/tests/unit/storage/mockdb/test_flavors.py b/tests/unit/storage/mockdb/test_flavors.py new file mode 100644 index 00000000..5dc2db34 --- /dev/null +++ b/tests/unit/storage/mockdb/test_flavors.py @@ -0,0 +1,74 @@ +# Copyright (c) 2014 Rackspace, 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 uuid + +import ddt +import mock +from oslo.config import cfg + +from poppy.storage.mockdb import driver +from poppy.storage.mockdb import flavors +from poppy.transport.pecan.models.request import flavor +from tests.unit import base + + +@ddt.ddt +class MockDBStorageFlavorsTests(base.TestCase): + + def setUp(self): + super(MockDBStorageFlavorsTests, self).setUp() + + self.flavor_id = uuid.uuid1() + + # create mocked config and driver + conf = cfg.ConfigOpts() + mockdb_driver = driver.MockDBStorageDriver(conf) + + # stubbed driver + self.fc = flavors.FlavorsController(mockdb_driver) + + @mock.patch.object(flavors.FlavorsController, 'session') + def test_get_flavor(self, mock_session): + + actual_response = self.fc.get(self.flavor_id) + + self.assertEqual(actual_response, None) + + @mock.patch.object(flavors.FlavorsController, 'session') + @ddt.file_data('../data/data_create_flavor.json') + def test_add_flavor(self, mock_session, value): + new_flavor = flavor.load_from_json(value) + + actual_response = self.fc.add(new_flavor) + + self.assertEqual(actual_response, None) + + @mock.patch.object(flavors.FlavorsController, 'session') + def test_list_flavors(self, mock_session): + actual_response = self.fc.list() + + # confirm the correct number of results are returned + self.assertEqual(actual_response, []) + + @mock.patch.object(flavors.FlavorsController, 'session') + def test_delete_flavor(self, mock_session): + actual_response = self.fc.delete(self.flavor_id) + + self.assertEqual(actual_response, None) + + def test_session(self): + session = self.fc.session + self.assertEqual(session, None)