diff --git a/test-requirements.txt b/test-requirements.txt index 771b9479a6..801af8a7f2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -23,3 +23,4 @@ pymongo>=3.0.2 # Apache-2.0 redis>=2.10.0 # MIT psycopg2>=2.5 # LGPL/ZPL cassandra-driver>=2.1.4 # Apache-2.0 +pycrypto>=2.6 # Public Domain diff --git a/trove/common/api.py b/trove/common/api.py index 8805346852..da13448fde 100644 --- a/trove/common/api.py +++ b/trove/common/api.py @@ -23,6 +23,7 @@ from trove.datastore.service import DatastoreController from trove.flavor.service import FlavorController from trove.instance.service import InstanceController from trove.limits.service import LimitsController +from trove.module.service import ModuleController from trove.versions import VersionsController @@ -39,6 +40,7 @@ class API(wsgi.Router): self._limits_router(mapper) self._backups_router(mapper) self._configurations_router(mapper) + self._modules_router(mapper) def _versions_router(self, mapper): versions_resource = VersionsController().create_resource() @@ -184,6 +186,32 @@ class API(wsgi.Router): action="delete", conditions={'method': ['DELETE']}) + def _modules_router(self, mapper): + + modules_resource = ModuleController().create_resource() + mapper.resource("modules", "/{tenant_id}/modules", + controller=modules_resource) + mapper.connect("/{tenant_id}/modules", + controller=modules_resource, + action="index", + conditions={'method': ['GET']}) + mapper.connect("/{tenant_id}/modules", + controller=modules_resource, + action="create", + conditions={'method': ['POST']}) + mapper.connect("/{tenant_id}/modules/{id}", + controller=modules_resource, + action="show", + conditions={'method': ['GET']}) + mapper.connect("/{tenant_id}/modules/{id}", + controller=modules_resource, + action="update", + conditions={'method': ['PUT']}) + mapper.connect("/{tenant_id}/modules/{id}", + controller=modules_resource, + action="delete", + conditions={'method': ['DELETE']}) + def _configurations_router(self, mapper): parameters_resource = ParametersController().create_resource() path = '/{tenant_id}/datastores/versions/{version}/parameters' diff --git a/trove/common/apischema.py b/trove/common/apischema.py index b79c8837bd..ee4a59e243 100644 --- a/trove/common/apischema.py +++ b/trove/common/apischema.py @@ -528,6 +528,75 @@ guest_log = { } } +module_non_empty_string = { + "type": "string", + "minLength": 1, + "maxLength": 65535, + "pattern": "^.*.+.*$" +} + +module = { + "create": { + "name": "module:create", + "type": "object", + "required": ["module"], + "properties": { + "module": { + "type": "object", + "required": ["name", "module_type", "contents"], + "additionalProperties": True, + "properties": { + "name": non_empty_string, + "module_type": non_empty_string, + "contents": module_non_empty_string, + "description": non_empty_string, + "datastore": { + "type": "object", + "properties": { + "type": non_empty_string, + "version": non_empty_string + } + }, + "auto_apply": boolean_string, + "all_tenants": boolean_string, + "visible": boolean_string, + "live_update": boolean_string, + } + } + } + }, + "update": { + "name": "module:update", + "type": "object", + "required": ["module"], + "properties": { + "module": { + "type": "object", + "required": [], + "additionalProperties": True, + "properties": { + "name": non_empty_string, + "type": non_empty_string, + "contents": module_non_empty_string, + "description": non_empty_string, + "datastore": { + "type": "object", + "additionalProperties": True, + "properties": { + "type": non_empty_string, + "version": non_empty_string + } + }, + "auto_apply": boolean_string, + "all_tenants": boolean_string, + "visible": boolean_string, + "live_update": boolean_string, + } + } + } + }, +} + configuration = { "create": { "name": "configuration:create", diff --git a/trove/common/cfg.py b/trove/common/cfg.py index 9d6525124a..e453e0c4e4 100644 --- a/trove/common/cfg.py +++ b/trove/common/cfg.py @@ -131,6 +131,8 @@ common_opts = [ help='Page size for listing backups.'), cfg.IntOpt('configurations_page_size', default=20, help='Page size for listing configurations.'), + cfg.IntOpt('modules_page_size', default=20, + help='Page size for listing modules.'), cfg.IntOpt('agent_call_low_timeout', default=5, help="Maximum time (in seconds) to wait for Guest Agent 'quick'" "requests (such as retrieving a list of users or " @@ -397,6 +399,10 @@ common_opts = [ cfg.IntOpt('timeout_wait_for_service', default=120, help='Maximum time (in seconds) to wait for a service to ' 'become alive.'), + cfg.StrOpt('module_aes_cbc_key', default='module_aes_cbc_key', + help='OpenSSL aes_cbc key for module encryption.'), + cfg.StrOpt('module_types', default='test, hidden_test', + help='A list of module types supported.'), cfg.StrOpt('guest_log_container_name', default='database_logs', help='Name of container that stores guest log components.'), diff --git a/trove/common/exception.py b/trove/common/exception.py index a67b2224e3..75a6774038 100644 --- a/trove/common/exception.py +++ b/trove/common/exception.py @@ -496,6 +496,32 @@ class ReplicaSourceDeleteForbidden(Forbidden): "replicas.") +class ModuleTypeNotFound(NotFound): + message = _("Module type '%(module_type)s' was not found.") + + +class ModuleAppliedToInstance(BadRequest): + + message = _("A module cannot be deleted or its contents modified if it " + "has been applied to a non-terminated instance, unless the " + "module has been marked as 'live_update.' " + "Please remove the module from all non-terminated " + "instances and try again.") + + +class ModuleAlreadyExists(BadRequest): + + message = _("A module with the name '%(name)s' already exists for " + "datastore '%(datastore)s' and datastore version " + "'%(ds_version)s'") + + +class ModuleAccessForbidden(Forbidden): + + message = _("You must be admin to %(action)s a module with these " + "options. %(options)s") + + class ClusterNotFound(NotFound): message = _("Cluster '%(cluster)s' cannot be found.") diff --git a/trove/common/utils.py b/trove/common/utils.py index f86b306db7..2aeaf88524 100644 --- a/trove/common/utils.py +++ b/trove/common/utils.py @@ -14,8 +14,12 @@ # under the License. """I totally stole most of this from melange, thx guys!!!""" +import base64 import collections +from Crypto.Cipher import AES +from Crypto import Random import datetime +import hashlib import inspect import os import shutil @@ -327,3 +331,44 @@ def is_collection(item): """ return (isinstance(item, collections.Iterable) and not isinstance(item, types.StringTypes)) + + +# Encryption/decryption handling methods +IV_BIT_COUNT = 16 + + +def encode_string(data_str): + byte_array = bytearray(data_str) + return base64.b64encode(byte_array) + + +def decode_string(data_str): + return base64.b64decode(data_str) + + +# Pad the data string to an multiple of pad_size +def pad_for_encryption(data_str, pad_size=IV_BIT_COUNT): + pad_count = pad_size - (len(data_str) % pad_size) + return data_str + chr(pad_count) * pad_count + + +# Unpad the data string by stripping off excess characters +def unpad_after_decryption(data_str): + return data_str[:len(data_str) - ord(data_str[-1])] + + +def encrypt_string(data_str, key, iv_bit_count=IV_BIT_COUNT): + md5_key = hashlib.md5(key).hexdigest() + iv = encode_string(Random.new().read(iv_bit_count))[:iv_bit_count] + aes = AES.new(md5_key, AES.MODE_CBC, iv) + data_str = pad_for_encryption(data_str, iv_bit_count) + encrypted_str = aes.encrypt(data_str) + return iv + encrypted_str + + +def decrypt_string(data_str, key, iv_bit_count=IV_BIT_COUNT): + md5_key = hashlib.md5(key).hexdigest() + iv = data_str[:iv_bit_count] + aes = AES.new(md5_key, AES.MODE_CBC, iv) + decrypted_str = aes.decrypt(data_str[iv_bit_count:]) + return unpad_after_decryption(decrypted_str) diff --git a/trove/common/wsgi.py b/trove/common/wsgi.py index 2aedf4c864..1e91a120ae 100644 --- a/trove/common/wsgi.py +++ b/trove/common/wsgi.py @@ -319,6 +319,7 @@ class Controller(object): webob.exc.HTTPForbidden: [ exception.ReplicaSourceDeleteForbidden, exception.BackupTooLarge, + exception.ModuleAccessForbidden, ], webob.exc.HTTPBadRequest: [ exception.InvalidModelError, @@ -328,6 +329,8 @@ class Controller(object): exception.DatabaseAlreadyExists, exception.UserAlreadyExists, exception.LocalStorageNotSpecified, + exception.ModuleAlreadyExists, + exception.ModuleAppliedToInstance, ], webob.exc.HTTPNotFound: [ exception.NotFound, @@ -340,6 +343,7 @@ class Controller(object): exception.ClusterNotFound, exception.DatastoreNotFound, exception.SwiftNotFound, + exception.ModuleTypeNotFound, ], webob.exc.HTTPConflict: [ exception.BackupNotCompleteError, diff --git a/trove/db/sqlalchemy/mappers.py b/trove/db/sqlalchemy/mappers.py index 9d379bc0e7..68063ba976 100644 --- a/trove/db/sqlalchemy/mappers.py +++ b/trove/db/sqlalchemy/mappers.py @@ -70,6 +70,10 @@ def map(engine, models): orm.mapper(models['datastore_configuration_parameters'], Table('datastore_configuration_parameters', meta, autoload=True)) + orm.mapper(models['modules'], + Table('modules', meta, autoload=True)) + orm.mapper(models['instance_modules'], + Table('instance_modules', meta, autoload=True)) def mapping_exists(model): diff --git a/trove/db/sqlalchemy/migrate_repo/versions/037_modules.py b/trove/db/sqlalchemy/migrate_repo/versions/037_modules.py new file mode 100644 index 0000000000..b2fc3334b9 --- /dev/null +++ b/trove/db/sqlalchemy/migrate_repo/versions/037_modules.py @@ -0,0 +1,84 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from sqlalchemy import ForeignKey +from sqlalchemy.schema import Column +from sqlalchemy.schema import MetaData +from sqlalchemy.schema import UniqueConstraint + +from trove.db.sqlalchemy.migrate_repo.schema import Boolean +from trove.db.sqlalchemy.migrate_repo.schema import create_tables +from trove.db.sqlalchemy.migrate_repo.schema import DateTime +from trove.db.sqlalchemy.migrate_repo.schema import drop_tables +from trove.db.sqlalchemy.migrate_repo.schema import String +from trove.db.sqlalchemy.migrate_repo.schema import Table +from trove.db.sqlalchemy.migrate_repo.schema import Text + + +meta = MetaData() + +modules = Table( + 'modules', + meta, + Column('id', String(length=64), primary_key=True, nullable=False), + Column('name', String(length=255), nullable=False), + Column('type', String(length=255), nullable=False), + Column('contents', Text(), nullable=False), + Column('description', String(length=255)), + Column('tenant_id', String(length=64), nullable=True), + Column('datastore_id', String(length=64), nullable=True), + Column('datastore_version_id', String(length=64), nullable=True), + Column('auto_apply', Boolean(), default=0, nullable=False), + Column('visible', Boolean(), default=1, nullable=False), + Column('live_update', Boolean(), default=0, nullable=False), + Column('md5', String(length=32), nullable=False), + Column('created', DateTime(), nullable=False), + Column('updated', DateTime(), nullable=False), + Column('deleted', Boolean(), default=0, nullable=False), + Column('deleted_at', DateTime()), + UniqueConstraint( + 'type', 'tenant_id', 'datastore_id', 'datastore_version_id', + 'name', 'deleted_at', + name='UQ_type_tenant_datastore_datastore_version_name'), +) + +instance_modules = Table( + 'instance_modules', + meta, + Column('id', String(length=64), primary_key=True, nullable=False), + Column('instance_id', String(length=64), + ForeignKey('instances.id', ondelete="CASCADE", + onupdate="CASCADE"), nullable=False), + Column('module_id', String(length=64), + ForeignKey('modules.id', ondelete="CASCADE", + onupdate="CASCADE"), nullable=False), + Column('md5', String(length=32), nullable=False), + Column('created', DateTime(), nullable=False), + Column('updated', DateTime(), nullable=False), + Column('deleted', Boolean(), default=0, nullable=False), + Column('deleted_at', DateTime()), +) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + Table('instances', meta, autoload=True) + create_tables([modules, instance_modules]) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + drop_tables([instance_modules, modules]) diff --git a/trove/db/sqlalchemy/session.py b/trove/db/sqlalchemy/session.py index cb08a378bc..e6a3f8ed27 100644 --- a/trove/db/sqlalchemy/session.py +++ b/trove/db/sqlalchemy/session.py @@ -48,6 +48,7 @@ def configure_db(options, models_mapper=None): from trove.extensions.security_group import models as secgrp_models from trove.guestagent import models as agent_models from trove.instance import models as base_models + from trove.module import models as module_models from trove.quota import models as quota_models model_modules = [ @@ -62,6 +63,7 @@ def configure_db(options, models_mapper=None): configurations_models, conductor_models, cluster_models, + module_models ] models = {} diff --git a/trove/module/__init__.py b/trove/module/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/trove/module/models.py b/trove/module/models.py new file mode 100644 index 0000000000..79882fe98a --- /dev/null +++ b/trove/module/models.py @@ -0,0 +1,273 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Model classes that form the core of Module functionality.""" + +from datetime import datetime +import hashlib + +from trove.common import cfg +from trove.common import exception +from trove.common.i18n import _ +from trove.common import utils +from trove.datastore import models as datastore_models +from trove.db import models +from trove.instance import models as instances_models + +from oslo_log import log as logging + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class Modules(object): + + DEFAULT_LIMIT = CONF.modules_page_size + ENCRYPT_KEY = CONF.module_aes_cbc_key + VALID_MODULE_TYPES = CONF.module_types + MATCH_ALL_NAME = 'all' + + @staticmethod + def load(context): + if context is None: + raise TypeError("Argument context not defined.") + elif id is None: + raise TypeError("Argument is not defined.") + + if context.is_admin: + db_info = DBModule.find_all(deleted=False) + if db_info.count() == 0: + LOG.debug("No modules found for admin user") + else: + db_info = DBModule.find_all( + tenant_id=context.tenant, visible=True, deleted=False) + if db_info.count() == 0: + LOG.debug("No modules found for tenant %s" % context.tenant) + + limit = utils.pagination_limit( + context.limit, Modules.DEFAULT_LIMIT) + data_view = DBModule.find_by_pagination( + 'modules', db_info, 'foo', limit=limit, marker=context.marker) + next_marker = data_view.next_page_marker + return data_view.collection, next_marker + + +class Module(object): + + def __init__(self, context, module_id): + self.context = context + self.module_id = module_id + + @staticmethod + def create(context, name, module_type, contents, + description, tenant_id, datastore, + datastore_version, auto_apply, visible, live_update): + if module_type not in Modules.VALID_MODULE_TYPES: + raise exception.ModuleTypeNotFound(module_type=module_type) + Module.validate_action( + context, 'create', tenant_id, auto_apply, visible) + datastore_id, datastore_version_id = Module.validate_datastore( + datastore, datastore_version) + if Module.key_exists( + name, module_type, tenant_id, + datastore_id, datastore_version_id): + datastore_str = datastore_id or Modules.MATCH_ALL_NAME + ds_version_str = datastore_version_id or Modules.MATCH_ALL_NAME + raise exception.ModuleAlreadyExists( + name=name, datastore=datastore_str, ds_version=ds_version_str) + md5, processed_contents = Module.process_contents(contents) + module = DBModule.create( + name=name, + type=module_type, + contents=processed_contents, + description=description, + tenant_id=tenant_id, + datastore_id=datastore_id, + datastore_version_id=datastore_version_id, + auto_apply=auto_apply, + visible=visible, + live_update=live_update, + md5=md5) + return module + + # Certain fields require admin access to create/change/delete + @staticmethod + def validate_action(context, action_str, tenant_id, auto_apply, visible): + error_str = None + if not context.is_admin: + option_strs = [] + if tenant_id is None: + option_strs.append(_("Tenant: %s") % Modules.MATCH_ALL_NAME) + if auto_apply: + option_strs.append(_("Auto: %s") % auto_apply) + if not visible: + option_strs.append(_("Visible: %s") % visible) + if option_strs: + error_str = "(" + " ".join(option_strs) + ")" + if error_str: + raise exception.ModuleAccessForbidden( + action=action_str, options=error_str) + + @staticmethod + def validate_datastore(datastore, datastore_version): + datastore_id = None + datastore_version_id = None + if datastore: + ds, ds_ver = datastore_models.get_datastore_version( + type=datastore, version=datastore_version) + datastore_id = ds.id + if datastore_version: + datastore_version_id = ds_ver.id + elif datastore_version: + msg = _("Cannot specify version without datastore") + raise exception.BadRequest(message=msg) + return datastore_id, datastore_version_id + + @staticmethod + def key_exists(name, module_type, tenant_id, datastore_id, + datastore_version_id): + try: + DBModule.find_by( + name=name, type=module_type, tenant_id=tenant_id, + datastore_id=datastore_id, + datastore_version_id=datastore_version_id, + deleted=False) + return True + except exception.ModelNotFoundError: + return False + + # We encrypt the contents (which should be encoded already, since it + # might be in binary format) and then encode them again so they can + # be stored in a text field in the Trove database. + @staticmethod + def process_contents(contents): + md5 = hashlib.md5(contents).hexdigest() + encrypted_contents = utils.encrypt_string( + contents, Modules.ENCRYPT_KEY) + return md5, utils.encode_string(encrypted_contents) + + @staticmethod + def delete(context, module): + Module.validate_action( + context, 'delete', + module.tenant_id, module.auto_apply, module.visible) + Module.enforce_live_update(module.id, module.live_update, module.md5) + module.deleted = True + module.deleted_at = datetime.utcnow() + module.save() + + @staticmethod + def enforce_live_update(module_id, live_update, md5): + if not live_update: + instances = DBInstanceModules.find_all( + id=module_id, md5=md5, deleted=False).all() + if instances: + raise exception.ModuleAppliedToInstance() + + @staticmethod + def load(context, module_id): + try: + if context.is_admin: + return DBModule.find_by(id=module_id, deleted=False) + else: + return DBModule.find_by( + id=module_id, tenant_id=context.tenant, visible=True, + deleted=False) + except exception.ModelNotFoundError: + # See if we have the module in the 'all' tenant section + if not context.is_admin: + try: + return DBModule.find_by( + id=module_id, tenant_id=None, visible=True, + deleted=False) + except exception.ModelNotFoundError: + pass # fall through to the raise below + msg = _("Module with ID %s could not be found.") % module_id + raise exception.ModelNotFoundError(msg) + + @staticmethod + def update(context, module, original_module): + Module.enforce_live_update( + original_module.id, original_module.live_update, + original_module.md5) + do_update = False + if module.contents != original_module.contents: + md5, processed_contents = Module.process_contents(module.contents) + do_update = (original_module.live_update and + md5 != original_module.md5) + module.md5 = md5 + module.contents = processed_contents + else: + module.contents = original_module.contents + # we don't allow any changes to 'admin'-type modules, even if + # the values changed aren't the admin ones. + access_tenant_id = (None if (original_module.tenant_id is None or + module.tenant_id is None) + else module.tenant_id) + access_auto_apply = original_module.auto_apply or module.auto_apply + access_visible = original_module.visible and module.visible + Module.validate_action( + context, 'update', + access_tenant_id, access_auto_apply, access_visible) + ds_id, ds_ver_id = Module.validate_datastore( + module.datastore_id, module.datastore_version_id) + if module.datastore_id: + module.datastore_id = ds_id + if module.datastore_version_id: + module.datastore_version_id = ds_ver_id + + module.updated = datetime.utcnow() + DBModule.save(module) + if do_update: + Module.reapply_on_all_instances(context, module) + + @staticmethod + def reapply_on_all_instances(context, module): + """Reapply a module on all its instances, if required.""" + if module.live_update: + instance_modules = DBInstanceModules.find_all( + id=module.id, deleted=False).all() + + LOG.debug( + "All instances with module '%s' applied: %s" + % (module.id, instance_modules)) + + for instance_module in instance_modules: + if instance_module.md5 != module.md5: + LOG.debug("Applying module '%s' to instance: %s" + % (module.id, instance_module.instance_id)) + instance = instances_models.Instance.load( + context, instance_module.instance_id) + instance.apply_module(module) + + +class DBModule(models.DatabaseModelBase): + _data_fields = [ + 'id', 'name', 'type', 'contents', 'description', + 'tenant_id', 'datastore_id', 'datastore_version_id', + 'auto_apply', 'visible', 'live_update', + 'md5', 'created', 'updated', 'deleted', 'deleted_at'] + + +class DBInstanceModules(models.DatabaseModelBase): + _data_fields = [ + 'id', 'instance_id', 'module_id', 'md5', 'created', + 'updated', 'deleted', 'deleted_at'] + + +def persisted_models(): + return {'modules': DBModule, 'instance_modules': DBInstanceModules} diff --git a/trove/module/service.py b/trove/module/service.py new file mode 100644 index 0000000000..91816dfa62 --- /dev/null +++ b/trove/module/service.py @@ -0,0 +1,123 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy + +from oslo_log import log as logging + +import trove.common.apischema as apischema +from trove.common import cfg +from trove.common.i18n import _ +from trove.common import pagination +from trove.common import wsgi +from trove.module import models +from trove.module import views + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class ModuleController(wsgi.Controller): + + schemas = apischema.module + + def index(self, req, tenant_id): + context = req.environ[wsgi.CONTEXT_KEY] + modules, marker = models.Modules.load(context) + view = views.ModulesView(modules) + paged = pagination.SimplePaginatedDataView(req.url, 'modules', + view, marker) + return wsgi.Result(paged.data(), 200) + + def show(self, req, tenant_id, id): + LOG.info(_("Showing module %s") % id) + + context = req.environ[wsgi.CONTEXT_KEY] + module = models.Module.load(context, id) + module.instance_count = models.DBInstanceModules.find_all( + id=module.id, md5=module.md5, + deleted=False).count() + + return wsgi.Result( + views.DetailedModuleView(module).data(), 200) + + def create(self, req, body, tenant_id): + + name = body['module']['name'] + LOG.info(_("Creating module '%s'") % name) + + context = req.environ[wsgi.CONTEXT_KEY] + module_type = body['module']['module_type'] + contents = body['module']['contents'] + + description = body['module'].get('description') + all_tenants = body['module'].get('all_tenants', 0) + module_tenant_id = None if all_tenants else tenant_id + datastore = body['module'].get('datastore', {}).get('type', None) + ds_version = body['module'].get('datastore', {}).get('version', None) + auto_apply = body['module'].get('auto_apply', 0) + visible = body['module'].get('visible', 1) + live_update = body['module'].get('live_update', 0) + + module = models.Module.create( + context, name, module_type, contents, + description, module_tenant_id, datastore, ds_version, + auto_apply, visible, live_update) + view_data = views.DetailedModuleView(module) + return wsgi.Result(view_data.data(), 200) + + def delete(self, req, tenant_id, id): + LOG.info(_("Deleting module %s") % id) + + context = req.environ[wsgi.CONTEXT_KEY] + module = models.Module.load(context, id) + models.Module.delete(context, module) + return wsgi.Result(None, 200) + + def update(self, req, body, tenant_id, id): + LOG.info(_("Updating module %s") % id) + + context = req.environ[wsgi.CONTEXT_KEY] + module = models.Module.load(context, id) + original_module = copy.deepcopy(module) + if 'name' in body['module']: + module.name = body['module']['name'] + if 'module_type' in body['module']: + module.type = body['module']['module_type'] + if 'contents' in body['module']: + module.contents = body['module']['contents'] + if 'description' in body['module']: + module.description = body['module']['description'] + if 'all_tenants' in body['module']: + module.tenant_id = (None if body['module']['all_tenants'] + else tenant_id) + if 'datastore' in body['module']: + if 'type' in body['module']['datastore']: + module.datastore_id = body['module']['datastore']['type'] + if 'version' in body['module']['datastore']: + module.datastore_version_id = ( + body['module']['datastore']['version']) + if 'auto_apply' in body['module']: + module.auto_apply = body['module']['auto_apply'] + if 'visible' in body['module']: + module.visible = body['module']['visible'] + if 'live_update' in body['module']: + module.live_update = body['module']['live_update'] + + models.Module.update(context, module, original_module) + view_data = views.DetailedModuleView(module) + return wsgi.Result(view_data.data(), 200) diff --git a/trove/module/views.py b/trove/module/views.py new file mode 100644 index 0000000000..7793528f5a --- /dev/null +++ b/trove/module/views.py @@ -0,0 +1,101 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from oslo_log import log as logging + +from trove.datastore import models as datastore_models +from trove.module import models + +LOG = logging.getLogger(__name__) + + +class ModuleView(object): + + def __init__(self, module): + self.module = module + + def data(self): + module_dict = dict( + id=self.module.id, + name=self.module.name, + type=self.module.type, + description=self.module.description, + tenant_id=self.module.tenant_id, + datastore_id=self.module.datastore_id, + datastore_version_id=self.module.datastore_version_id, + auto_apply=self.module.auto_apply, + md5=self.module.md5, + created=self.module.created, + updated=self.module.updated) + # add extra data to make results more legible + if self.module.tenant_id: + # This should be the tenant name, but until we figure out where + # to get it from, use the tenant_id + tenant = self.module.tenant_id + else: + tenant = models.Modules.MATCH_ALL_NAME + module_dict["tenant"] = tenant + datastore = self.module.datastore_id + datastore_version = self.module.datastore_version_id + if datastore: + ds, ds_ver = ( + datastore_models.get_datastore_version( + type=datastore, version=datastore_version)) + datastore = ds.name + if datastore_version: + datastore_version = ds_ver.name + else: + datastore_version = models.Modules.MATCH_ALL_NAME + else: + datastore = models.Modules.MATCH_ALL_NAME + datastore_version = models.Modules.MATCH_ALL_NAME + module_dict["datastore"] = datastore + module_dict["datastore_version"] = datastore_version + + return {"module": module_dict} + + +class ModulesView(object): + + def __init__(self, modules): + self.modules = modules + + def data(self): + data = [] + + for module in self.modules: + data.append(self.data_for_module(module)) + + return {"modules": data} + + def data_for_module(self, module): + view = ModuleView(module) + return view.data()['module'] + + +class DetailedModuleView(ModuleView): + + def __init__(self, module): + super(DetailedModuleView, self).__init__(module) + + def data(self): + return_value = super(DetailedModuleView, self).data() + module_dict = return_value["module"] + module_dict["visible"] = self.module.visible + module_dict["live_update"] = self.module.live_update + if hasattr(self.module, 'instance_count'): + module_dict["instance_count"] = self.module.instance_count + return {"module": module_dict} diff --git a/trove/tests/int_tests.py b/trove/tests/int_tests.py index 76329a4619..19869a2cb0 100644 --- a/trove/tests/int_tests.py +++ b/trove/tests/int_tests.py @@ -40,6 +40,7 @@ from trove.tests.scenario.groups import guest_log_group from trove.tests.scenario.groups import instance_actions_group from trove.tests.scenario.groups import instance_create_group from trove.tests.scenario.groups import instance_delete_group +from trove.tests.scenario.groups import module_group from trove.tests.scenario.groups import negative_cluster_actions_group from trove.tests.scenario.groups import replication_group from trove.tests.scenario.groups import root_actions_group @@ -155,6 +156,16 @@ guest_log_groups.extend([guest_log_group.GROUP]) instance_actions_groups = list(instance_create_groups) instance_actions_groups.extend([instance_actions_group.GROUP]) +instance_module_groups = list(instance_create_groups) +instance_module_groups.extend([module_group.GROUP_INSTANCE_MODULE]) + +module_groups = list(instance_create_groups) +module_groups.extend([module_group.GROUP]) + +module_create_groups = list(base_groups) +module_create_groups.extend([module_group.GROUP_MODULE, + module_group.GROUP_MODULE_DELETE]) + replication_groups = list(instance_create_groups) replication_groups.extend([replication_group.GROUP]) @@ -166,9 +177,9 @@ user_actions_groups.extend([user_actions_group.GROUP]) # groups common to all datastores common_groups = list(instance_actions_groups) -common_groups.extend([guest_log_groups]) +common_groups.extend([guest_log_groups, module_groups]) -# Register: Module based groups +# Register: Component based groups register(["backup"], backup_groups) register(["cluster"], cluster_actions_groups) register(["configuration"], configuration_groups) @@ -176,6 +187,9 @@ register(["database"], database_actions_groups) register(["guest_log"], guest_log_groups) register(["instance", "instance_actions"], instance_actions_groups) register(["instance_create"], instance_create_groups) +register(["instance_module"], instance_module_groups) +register(["module"], module_groups) +register(["module_create"], module_create_groups) register(["replication"], replication_groups) register(["root"], root_actions_groups) register(["user"], user_actions_groups) diff --git a/trove/tests/scenario/groups/instance_delete_group.py b/trove/tests/scenario/groups/instance_delete_group.py index 8ce28d06e8..00e2c93381 100644 --- a/trove/tests/scenario/groups/instance_delete_group.py +++ b/trove/tests/scenario/groups/instance_delete_group.py @@ -20,6 +20,7 @@ from trove.tests.scenario.groups import configuration_group from trove.tests.scenario.groups import database_actions_group from trove.tests.scenario.groups import instance_actions_group from trove.tests.scenario.groups import instance_create_group +from trove.tests.scenario.groups import module_group from trove.tests.scenario.groups import replication_group from trove.tests.scenario.groups import root_actions_group from trove.tests.scenario.groups.test_group import TestGroup @@ -35,6 +36,7 @@ GROUP = "scenario.instance_delete_group" configuration_group.GROUP, database_actions_group.GROUP, instance_actions_group.GROUP, + module_group.GROUP, replication_group.GROUP, root_actions_group.GROUP, user_actions_group.GROUP]) diff --git a/trove/tests/scenario/groups/module_group.py b/trove/tests/scenario/groups/module_group.py new file mode 100644 index 0000000000..bfcb8d1d60 --- /dev/null +++ b/trove/tests/scenario/groups/module_group.py @@ -0,0 +1,344 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from proboscis import test + +from trove.tests.scenario.groups import instance_create_group +from trove.tests.scenario.groups.test_group import TestGroup + + +GROUP = "scenario.module_all_group" +GROUP_MODULE = "scenario.module_group" +GROUP_MODULE_DELETE = "scenario.module_delete_group" +GROUP_INSTANCE_MODULE = "scenario.instance_module_group" + + +@test(groups=[GROUP, GROUP_MODULE]) +class ModuleGroup(TestGroup): + """Test Module functionality.""" + + def __init__(self): + super(ModuleGroup, self).__init__( + 'module_runners', 'ModuleRunner') + + @test(groups=[GROUP, GROUP_MODULE]) + def module_delete_existing(self): + """Delete all previous test modules.""" + self.test_runner.run_module_delete_existing() + + @test(groups=[GROUP, GROUP_MODULE]) + def module_create_bad_type(self): + """Ensure create module fails with invalid type.""" + self.test_runner.run_module_create_bad_type() + + @test(groups=[GROUP, GROUP_MODULE]) + def module_create_non_admin_auto(self): + """Ensure create auto_apply module fails for non-admin.""" + self.test_runner.run_module_create_non_admin_auto() + + @test(groups=[GROUP, GROUP_MODULE]) + def module_create_non_admin_all_tenant(self): + """Ensure create all tenant module fails for non-admin.""" + self.test_runner.run_module_create_non_admin_all_tenant() + + @test(groups=[GROUP, GROUP_MODULE]) + def module_create_non_admin_hidden(self): + """Ensure create hidden module fails for non-admin.""" + self.test_runner.run_module_create_non_admin_hidden() + + @test(groups=[GROUP, GROUP_MODULE]) + def module_create_bad_datastore(self): + """Ensure create module fails with invalid datastore.""" + self.test_runner.run_module_create_bad_datastore() + + @test(groups=[GROUP, GROUP_MODULE]) + def module_create_bad_datastore_version(self): + """Ensure create module fails with invalid datastore_version.""" + self.test_runner.run_module_create_bad_datastore_version() + + @test(groups=[GROUP, GROUP_MODULE]) + def module_create_missing_datastore(self): + """Ensure create module fails with missing datastore.""" + self.test_runner.run_module_create_missing_datastore() + + @test(groups=[GROUP, GROUP_MODULE], + runs_after=[module_delete_existing]) + def module_create(self): + """Check that create module works.""" + self.test_runner.run_module_create() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create]) + def module_create_dupe(self): + """Ensure create with duplicate info fails.""" + self.test_runner.run_module_create_dupe() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create]) + def module_show(self): + """Check that show module works.""" + self.test_runner.run_module_show() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create]) + def module_show_unauth_user(self): + """Ensure that show module for unauth user fails.""" + self.test_runner.run_module_show_unauth_user() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create]) + def module_list(self): + """Check that list modules works.""" + self.test_runner.run_module_list() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create]) + def module_list_unauth_user(self): + """Ensure that list module for unauth user fails.""" + self.test_runner.run_module_list_unauth_user() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create], + runs_after=[module_list]) + def module_create_admin_all(self): + """Check that create module works with all admin options.""" + self.test_runner.run_module_create_admin_all() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create], + runs_after=[module_create_admin_all]) + def module_create_admin_hidden(self): + """Check that create module works with hidden option.""" + self.test_runner.run_module_create_admin_hidden() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create], + runs_after=[module_create_admin_hidden]) + def module_create_admin_auto(self): + """Check that create module works with auto option.""" + self.test_runner.run_module_create_admin_auto() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create], + runs_after=[module_create_admin_auto]) + def module_create_admin_live_update(self): + """Check that create module works with live-update option.""" + self.test_runner.run_module_create_admin_live_update() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create], + runs_after=[module_create_admin_live_update]) + def module_create_all_tenant(self): + """Check that create 'all' tenants with datastore module works.""" + self.test_runner.run_module_create_all_tenant() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create], + runs_after=[module_create_all_tenant, module_list_unauth_user]) + def module_create_different_tenant(self): + """Check that create with same name on different tenant works.""" + self.test_runner.run_module_create_different_tenant() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create_all_tenant], + runs_after=[module_create_different_tenant]) + def module_list_again(self): + """Check that list modules skips invisible modules.""" + self.test_runner.run_module_list_again() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create_admin_hidden]) + def module_show_invisible(self): + """Ensure that show invisible module for non-admin fails.""" + self.test_runner.run_module_show_invisible() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create_all_tenant], + runs_after=[module_create_different_tenant]) + def module_list_admin(self): + """Check that list modules for admin works.""" + self.test_runner.run_module_list_admin() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_create], + runs_after=[module_show]) + def module_update(self): + """Check that update module works.""" + self.test_runner.run_module_update() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update]) + def module_update_same_contents(self): + """Check that update module with same contents works.""" + self.test_runner.run_module_update_same_contents() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_same_contents]) + def module_update_auto_toggle(self): + """Check that update module works for auto apply toggle.""" + self.test_runner.run_module_update_auto_toggle() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_auto_toggle]) + def module_update_all_tenant_toggle(self): + """Check that update module works for all tenant toggle.""" + self.test_runner.run_module_update_all_tenant_toggle() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_all_tenant_toggle]) + def module_update_invisible_toggle(self): + """Check that update module works for invisible toggle.""" + self.test_runner.run_module_update_invisible_toggle() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_unauth(self): + """Ensure update module fails for unauth user.""" + self.test_runner.run_module_update_unauth() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_non_admin_auto(self): + """Ensure update module to auto_apply fails for non-admin.""" + self.test_runner.run_module_update_non_admin_auto() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_non_admin_auto_off(self): + """Ensure update module to auto_apply off fails for non-admin.""" + self.test_runner.run_module_update_non_admin_auto_off() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_non_admin_auto_any(self): + """Ensure any update module to auto_apply fails for non-admin.""" + self.test_runner.run_module_update_non_admin_auto_any() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_non_admin_all_tenant(self): + """Ensure update module to all tenant fails for non-admin.""" + self.test_runner.run_module_update_non_admin_all_tenant() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_non_admin_all_tenant_off(self): + """Ensure update module to all tenant off fails for non-admin.""" + self.test_runner.run_module_update_non_admin_all_tenant_off() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_non_admin_all_tenant_any(self): + """Ensure any update module to all tenant fails for non-admin.""" + self.test_runner.run_module_update_non_admin_all_tenant_any() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_non_admin_invisible(self): + """Ensure update module to invisible fails for non-admin.""" + self.test_runner.run_module_update_non_admin_invisible() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_non_admin_invisible_off(self): + """Ensure update module to invisible off fails for non-admin.""" + self.test_runner.run_module_update_non_admin_invisible_off() + + @test(groups=[GROUP, GROUP_MODULE], + depends_on=[module_update], + runs_after=[module_update_invisible_toggle]) + def module_update_non_admin_invisible_any(self): + """Ensure any update module to invisible fails for non-admin.""" + self.test_runner.run_module_update_non_admin_invisible_any() + + +@test(depends_on_groups=[instance_create_group.GROUP, + GROUP_MODULE], + groups=[GROUP, GROUP_INSTANCE_MODULE]) +class ModuleInstanceGroup(TestGroup): + """Test Instance Module functionality.""" + + def __init__(self): + super(ModuleInstanceGroup, self).__init__( + 'module_runners', 'ModuleRunner') + + +@test(depends_on_groups=[GROUP_MODULE], + groups=[GROUP, GROUP_MODULE_DELETE]) +class ModuleDeleteGroup(TestGroup): + """Test Module Delete functionality.""" + + def __init__(self): + super(ModuleDeleteGroup, self).__init__( + 'module_runners', 'ModuleRunner') + + @test(groups=[GROUP, GROUP_MODULE_DELETE]) + def module_delete_non_existent(self): + """Ensure delete non-existent module fails.""" + self.test_runner.run_module_delete_non_existent() + + @test(groups=[GROUP, GROUP_MODULE_DELETE]) + def module_delete_unauth_user(self): + """Ensure delete module by unauth user fails.""" + self.test_runner.run_module_delete_unauth_user() + + @test(groups=[GROUP, GROUP_MODULE_DELETE], + runs_after=[module_delete_unauth_user]) + def module_delete_hidden_by_non_admin(self): + """Ensure delete hidden module by non-admin user fails.""" + self.test_runner.run_module_delete_hidden_by_non_admin() + + @test(groups=[GROUP, GROUP_MODULE_DELETE], + runs_after=[module_delete_hidden_by_non_admin]) + def module_delete_all_tenant_by_non_admin(self): + """Ensure delete all tenant module by non-admin user fails.""" + self.test_runner.run_module_delete_all_tenant_by_non_admin() + + @test(groups=[GROUP, GROUP_MODULE_DELETE], + runs_after=[module_delete_all_tenant_by_non_admin]) + def module_delete_auto_by_non_admin(self): + """Ensure delete auto-apply module by non-admin user fails.""" + self.test_runner.run_module_delete_auto_by_non_admin() + + @test(groups=[GROUP, GROUP_MODULE_DELETE], + runs_after=[module_delete_auto_by_non_admin]) + def module_delete(self): + """Check that delete module works.""" + self.test_runner.run_module_delete_auto_by_non_admin() + + @test(groups=[GROUP, GROUP_MODULE_DELETE], + runs_after=[module_delete]) + def module_delete_all(self): + """Check that delete module works for admin.""" + self.test_runner.run_module_delete() + + @test(groups=[GROUP, GROUP_MODULE_DELETE], + runs_after=[module_delete_all]) + def module_delete_existing(self): + """Delete all remaining test modules.""" + self.test_runner.run_module_delete_existing() diff --git a/trove/tests/scenario/helpers/test_helper.py b/trove/tests/scenario/helpers/test_helper.py index 5d7f54521b..8d39b5e0b3 100644 --- a/trove/tests/scenario/helpers/test_helper.py +++ b/trove/tests/scenario/helpers/test_helper.py @@ -427,3 +427,10 @@ class TestHelper(object): """Return a valid password that can be used by a 'root' user. """ return "RootTestPass" + + ############## + # Module related + ############## + def get_valid_module_type(self): + """Return a valid module type.""" + return "test" diff --git a/trove/tests/scenario/runners/module_runners.py b/trove/tests/scenario/runners/module_runners.py new file mode 100644 index 0000000000..baf9a3604e --- /dev/null +++ b/trove/tests/scenario/runners/module_runners.py @@ -0,0 +1,634 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from proboscis import SkipTest + +from troveclient.compat import exceptions + +from trove.common import utils +from trove.module import models +from trove.tests.scenario.runners.test_runners import TestRunner + + +# Variables here are set up to be used across multiple groups, +# since each group will instantiate a new runner +test_modules = [] +module_count_prior_to_create = 0 +module_admin_count_prior_to_create = 0 +module_other_count_prior_to_create = 0 +module_create_count = 0 +module_admin_create_count = 0 +module_other_create_count = 0 + + +class ModuleRunner(TestRunner): + + def __init__(self): + self.TIMEOUT_MODULE_APPLY = 60 * 10 + + super(ModuleRunner, self).__init__( + sleep_time=10, timeout=self.TIMEOUT_MODULE_APPLY) + + self.MODULE_NAME = 'test_module_1' + self.MODULE_DESC = 'test description' + self.MODULE_CONTENTS = utils.encode_string( + 'mode=echo\nkey=mysecretkey\n') + + self.temp_module = None + self._module_type = None + + @property + def module_type(self): + if not self._module_type: + self._module_type = self.test_helper.get_valid_module_type() + return self._module_type + + @property + def main_test_module(self): + if not test_modules or not test_modules[0]: + SkipTest("No main module created") + return test_modules[0] + + def run_module_delete_existing(self): + modules = self.admin_client.modules.list() + for module in modules: + if module.name.startswith(self.MODULE_NAME): + self.admin_client.modules.delete(module.id) + + def run_module_create_bad_type( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.create, + self.MODULE_NAME, 'invalid-type', self.MODULE_CONTENTS) + + def run_module_create_non_admin_auto( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.create, + self.MODULE_NAME, self.module_type, self.MODULE_CONTENTS, + auto_apply=True) + + def run_module_create_non_admin_all_tenant( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.create, + self.MODULE_NAME, self.module_type, self.MODULE_CONTENTS, + all_tenants=True) + + def run_module_create_non_admin_hidden( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.create, + self.MODULE_NAME, self.module_type, self.MODULE_CONTENTS, + visible=False) + + def run_module_create_bad_datastore( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.create, + self.MODULE_NAME, self.module_type, self.MODULE_CONTENTS, + datastore='bad-datastore') + + def run_module_create_bad_datastore_version( + self, expected_exception=exceptions.BadRequest, + expected_http_code=400): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.create, + self.MODULE_NAME, self.module_type, self.MODULE_CONTENTS, + datastore=self.instance_info.dbaas_datastore, + datastore_version='bad-datastore-version') + + def run_module_create_missing_datastore( + self, expected_exception=exceptions.BadRequest, + expected_http_code=400): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.create, + self.MODULE_NAME, self.module_type, self.MODULE_CONTENTS, + datastore_version=self.instance_info.dbaas_datastore_version) + + def run_module_create(self): + # Necessary to test that the count increases. + global module_count_prior_to_create + global module_admin_count_prior_to_create + global module_other_count_prior_to_create + module_count_prior_to_create = len( + self.auth_client.modules.list()) + module_admin_count_prior_to_create = len( + self.admin_client.modules.list()) + module_other_count_prior_to_create = len( + self.unauth_client.modules.list()) + self.assert_module_create( + self.auth_client, + name=self.MODULE_NAME, + module_type=self.module_type, + contents=self.MODULE_CONTENTS, + description=self.MODULE_DESC) + + def assert_module_create(self, client, name=None, module_type=None, + contents=None, description=None, + all_tenants=False, + datastore=None, datastore_version=None, + auto_apply=False, + live_update=False, visible=True): + result = client.modules.create( + name, module_type, contents, + description=description, + all_tenants=all_tenants, + datastore=datastore, datastore_version=datastore_version, + auto_apply=auto_apply, + live_update=live_update, visible=visible) + global module_create_count + global module_admin_create_count + global module_other_create_count + if (client == self.auth_client or + (client == self.admin_client and visible)): + module_create_count += 1 + elif not visible: + module_admin_create_count += 1 + else: + module_other_create_count += 1 + global test_modules + test_modules.append(result) + + tenant_id = None + tenant = models.Modules.MATCH_ALL_NAME + if not all_tenants: + tenant, tenant_id = self.get_client_tenant(client) + # TODO(peterstac) we don't support tenant name yet ... + tenant = tenant_id + datastore = datastore or models.Modules.MATCH_ALL_NAME + datastore_version = datastore_version or models.Modules.MATCH_ALL_NAME + self.validate_module( + result, validate_all=False, + expected_name=name, + expected_module_type=module_type, + expected_description=description, + expected_tenant=tenant, + expected_tenant_id=tenant_id, + expected_datastore=datastore, + expected_ds_version=datastore_version, + expected_auto_apply=auto_apply) + + def validate_module(self, module, validate_all=False, + expected_name=None, + expected_module_type=None, + expected_description=None, + expected_tenant=None, + expected_tenant_id=None, + expected_datastore=None, + expected_datastore_id=None, + expected_ds_version=None, + expected_ds_version_id=None, + expected_all_tenants=None, + expected_auto_apply=None, + expected_live_update=None, + expected_visible=None, + expected_contents=None): + + if expected_all_tenants: + expected_tenant = expected_tenant or models.Modules.MATCH_ALL_NAME + if expected_name: + self.assert_equal(expected_name, module.name, + 'Unexpected module name') + if expected_module_type: + self.assert_equal(expected_module_type, module.type, + 'Unexpected module type') + if expected_description: + self.assert_equal(expected_description, module.description, + 'Unexpected module description') + if expected_tenant_id: + self.assert_equal(expected_tenant_id, module.tenant_id, + 'Unexpected tenant id') + if expected_tenant: + self.assert_equal(expected_tenant, module.tenant, + 'Unexpected tenant name') + if expected_datastore: + self.assert_equal(expected_datastore, module.datastore, + 'Unexpected datastore') + if expected_ds_version: + self.assert_equal(expected_ds_version, + module.datastore_version, + 'Unexpected datastore version') + if expected_auto_apply is not None: + self.assert_equal(expected_auto_apply, module.auto_apply, + 'Unexpected auto_apply') + if validate_all: + if expected_datastore_id: + self.assert_equal(expected_datastore_id, module.datastore_id, + 'Unexpected datastore id') + if expected_ds_version_id: + self.assert_equal(expected_ds_version_id, + module.datastore_version_id, + 'Unexpected datastore version id') + if expected_live_update is not None: + self.assert_equal(expected_live_update, module.live_update, + 'Unexpected live_update') + if expected_visible is not None: + self.assert_equal(expected_visible, module.visible, + 'Unexpected visible') + + def run_module_create_dupe( + self, expected_exception=exceptions.BadRequest, + expected_http_code=400): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.create, + self.MODULE_NAME, self.module_type, self.MODULE_CONTENTS) + + def run_module_show(self): + test_module = self.main_test_module + result = self.auth_client.modules.get(test_module.id) + self.validate_module( + result, validate_all=True, + expected_name=test_module.name, + expected_module_type=test_module.type, + expected_description=test_module.description, + expected_tenant=test_module.tenant, + expected_datastore=test_module.datastore, + expected_ds_version=test_module.datastore_version, + expected_auto_apply=test_module.auto_apply, + expected_live_update=False, + expected_visible=True) + + def run_module_show_unauth_user( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + self.assert_raises( + expected_exception, None, + self.unauth_client.modules.get, self.main_test_module.id) + # we're using a different client, so we'll check the return code + # on it explicitly, instead of depending on 'assert_raises' + self.assert_client_code(expected_http_code=expected_http_code, + client=self.unauth_client) + + def run_module_list(self): + self.assert_module_list( + self.auth_client, + module_count_prior_to_create + module_create_count) + + def assert_module_list(self, client, expected_count, + skip_validation=False): + module_list = client.modules.list() + self.assert_equal(expected_count, len(module_list), + "Wrong number of modules for list") + if not skip_validation: + for module in module_list: + if module.name != self.MODULE_NAME: + continue + test_module = self.main_test_module + self.validate_module( + module, validate_all=False, + expected_name=test_module.name, + expected_module_type=test_module.type, + expected_description=test_module.description, + expected_tenant=test_module.tenant, + expected_datastore=test_module.datastore, + expected_ds_version=test_module.datastore_version, + expected_auto_apply=test_module.auto_apply) + + def run_module_list_unauth_user(self): + self.assert_module_list(self.unauth_client, 0) + + def run_module_create_admin_all(self): + self.assert_module_create( + self.admin_client, + name=self.MODULE_NAME + '_admin_apply', + module_type=self.module_type, + contents=self.MODULE_CONTENTS, + description=(self.MODULE_DESC + ' admin apply'), + all_tenants=True, + visible=False, + auto_apply=True) + + def run_module_create_admin_hidden(self): + self.assert_module_create( + self.admin_client, + name=self.MODULE_NAME + '_hidden', + module_type=self.module_type, + contents=self.MODULE_CONTENTS, + description=self.MODULE_DESC + ' hidden', + visible=False) + + def run_module_create_admin_auto(self): + self.assert_module_create( + self.admin_client, + name=self.MODULE_NAME + '_auto', + module_type=self.module_type, + contents=self.MODULE_CONTENTS, + description=self.MODULE_DESC + ' hidden', + auto_apply=True) + + def run_module_create_admin_live_update(self): + self.assert_module_create( + self.admin_client, + name=self.MODULE_NAME + '_live', + module_type=self.module_type, + contents=self.MODULE_CONTENTS, + description=(self.MODULE_DESC + ' live update'), + live_update=True) + + def run_module_create_all_tenant(self): + self.assert_module_create( + self.admin_client, + name=self.MODULE_NAME + '_all_tenant', + module_type=self.module_type, + contents=self.MODULE_CONTENTS, + description=self.MODULE_DESC + ' all tenant', + all_tenants=True, + datastore=self.instance_info.dbaas_datastore, + datastore_version=self.instance_info.dbaas_datastore_version) + + def run_module_create_different_tenant(self): + self.assert_module_create( + self.unauth_client, + name=self.MODULE_NAME, + module_type=self.module_type, + contents=self.MODULE_CONTENTS, + description=self.MODULE_DESC) + + def run_module_list_again(self): + self.assert_module_list( + self.auth_client, + # TODO(peterstac) remove the '-1' once the list is fixed to + # include 'all' tenant modules + module_count_prior_to_create + module_create_count - 1, + skip_validation=True) + + def run_module_show_invisible( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + module = self._find_invisible_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.get, module.id) + + def _find_invisible_module(self): + def _match(mod): + return not mod.visible and mod.tenant_id and not mod.auto_apply + return self._find_module(_match, "Could not find invisible module") + + def _find_module(self, match_fn, not_found_message): + module = None + for test_module in test_modules: + if match_fn(test_module): + module = test_module + break + if not module: + self.fail(not_found_message) + return module + + def run_module_list_admin(self): + self.assert_module_list( + self.admin_client, + (module_admin_count_prior_to_create + + module_create_count + + module_admin_create_count + + module_other_create_count), + skip_validation=True) + + def run_module_update(self): + self.assert_module_update( + self.auth_client, + self.main_test_module.id, + description=self.MODULE_DESC + " modified") + + def run_module_update_same_contents(self): + old_md5 = self.main_test_module.md5 + self.assert_module_update( + self.auth_client, + self.main_test_module.id, + contents=self.MODULE_CONTENTS) + self.assert_equal(old_md5, self.main_test_module.md5, + "MD5 changed with same contents") + + def run_module_update_auto_toggle(self): + module = self._find_auto_apply_module() + toggle_off_args = {'auto_apply': False} + toggle_on_args = {'auto_apply': True} + self.assert_module_toggle(module, toggle_off_args, toggle_on_args) + + def assert_module_toggle(self, module, toggle_off_args, toggle_on_args): + # First try to update the module based on the change + # (this should toggle the state and allow non-admin access) + self.assert_module_update( + self.admin_client, module.id, **toggle_off_args) + # Now we can update using the non-admin client + self.assert_module_update( + self.auth_client, module.id, description='Updated by auth') + # Now set it back + self.assert_module_update( + self.admin_client, module.id, description=module.description, + **toggle_on_args) + + def run_module_update_all_tenant_toggle(self): + module = self._find_all_tenant_module() + toggle_off_args = {'all_tenants': False} + toggle_on_args = {'all_tenants': True} + self.assert_module_toggle(module, toggle_off_args, toggle_on_args) + + def run_module_update_invisible_toggle(self): + module = self._find_invisible_module() + toggle_off_args = {'visible': True} + toggle_on_args = {'visible': False} + self.assert_module_toggle(module, toggle_off_args, toggle_on_args) + + def assert_module_update(self, client, module_id, **kwargs): + result = client.modules.update(module_id, **kwargs) + global test_modules + found = False + index = -1 + for test_module in test_modules: + index += 1 + if test_module.id == module_id: + found = True + break + if not found: + self.fail("Could not find updated module in module list") + test_modules[index] = result + + expected_args = {} + for key, value in kwargs.items(): + new_key = 'expected_' + key + expected_args[new_key] = value + self.validate_module(result, **expected_args) + + def run_module_update_unauth( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + self.assert_raises( + expected_exception, expected_http_code, + self.unauth_client.modules.update, + self.main_test_module.id, description='Upd') + + def run_module_update_non_admin_auto( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.update, + self.main_test_module.id, visible=False) + + def run_module_update_non_admin_auto_off( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + module = self._find_auto_apply_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.update, module.id, auto_apply=False) + + def _find_auto_apply_module(self): + def _match(mod): + return mod.auto_apply and mod.tenant_id and mod.visible + return self._find_module(_match, "Could not find auto-apply module") + + def run_module_update_non_admin_auto_any( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + module = self._find_auto_apply_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.update, module.id, description='Upd') + + def run_module_update_non_admin_all_tenant( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.update, + self.main_test_module.id, all_tenants=True) + + def run_module_update_non_admin_all_tenant_off( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + module = self._find_all_tenant_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.update, module.id, all_tenants=False) + + def _find_all_tenant_module(self): + def _match(mod): + return mod.tenant_id is None and mod.visible + return self._find_module(_match, "Could not find all tenant module") + + def run_module_update_non_admin_all_tenant_any( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + module = self._find_all_tenant_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.update, module.id, description='Upd') + + def run_module_update_non_admin_invisible( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.update, + self.main_test_module.id, visible=False) + + def run_module_update_non_admin_invisible_off( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + module = self._find_invisible_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.update, module.id, visible=True) + + def run_module_update_non_admin_invisible_any( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + module = self._find_invisible_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.update, module.id, description='Upd') + + # ModuleDeleteGroup methods + def run_module_delete_non_existent( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.delete, 'bad_id') + + def run_module_delete_unauth_user( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + self.assert_raises( + expected_exception, expected_http_code, + self.unauth_client.modules.delete, self.main_test_module.id) + + def run_module_delete_hidden_by_non_admin( + self, expected_exception=exceptions.NotFound, + expected_http_code=404): + module = self._find_invisible_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.delete, module.id) + + def run_module_delete_all_tenant_by_non_admin( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + module = self._find_all_tenant_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.delete, module.id) + + def run_module_delete_auto_by_non_admin( + self, expected_exception=exceptions.Forbidden, + expected_http_code=403): + module = self._find_auto_apply_module() + self.assert_raises( + expected_exception, expected_http_code, + self.auth_client.modules.delete, module.id) + + def run_module_delete(self): + expected_count = len(self.auth_client.modules.list()) - 1 + test_module = test_modules.pop(0) + self.assert_module_delete(self.auth_client, test_module.id, + expected_count) + + def run_module_delete_admin(self): + start_count = count = len(self.admin_client.modules.list()) + for test_module in test_modules: + count -= 1 + self.report.log("Deleting module '%s' (tenant: %s)" % ( + test_module.name, test_module.tenant_id)) + self.assert_module_delete(self.admin_client, test_module.id, count) + self.assert_not_equal(start_count, count, "Nothing was deleted") + count = len(self.admin_client.modules.list()) + self.assert_equal(module_admin_count_prior_to_create, count, + "Wrong number of admin modules after deleting all") + count = len(self.auth_client.modules.list()) + self.assert_equal(module_count_prior_to_create, count, + "Wrong number of modules after deleting all") + + def assert_module_delete(self, client, module_id, expected_count): + client.modules.delete(module_id) + count = len(client.modules.list()) + self.assert_equal(expected_count, count, + "Wrong number of modules after delete") diff --git a/trove/tests/scenario/runners/test_runners.py b/trove/tests/scenario/runners/test_runners.py index 47601b1b75..f9cb37b8bc 100644 --- a/trove/tests/scenario/runners/test_runners.py +++ b/trove/tests/scenario/runners/test_runners.py @@ -197,6 +197,13 @@ class TestRunner(object): auth_version='2.0', os_options=os_options) + def get_client_tenant(self, client): + tenant_name = client.real_client.client.tenant + service_url = client.real_client.client.service_url + su_parts = service_url.split('/') + tenant_id = su_parts[-1] + return tenant_name, tenant_id + def assert_raises(self, expected_exception, expected_http_code, client_cmd, *cmd_args, **cmd_kwargs): asserts.assert_raises(expected_exception, client_cmd, diff --git a/trove/tests/unittests/common/test_utils.py b/trove/tests/unittests/common/test_utils.py index c7539cf515..b4920f76db 100644 --- a/trove/tests/unittests/common/test_utils.py +++ b/trove/tests/unittests/common/test_utils.py @@ -13,7 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. # + +from Crypto import Random from mock import Mock + from testtools import ExpectedException from trove.common import exception from trove.common import utils @@ -79,3 +82,39 @@ class TestTroveExecuteWithTimeout(trove_testtools.TestCase): def test_pagination_limit(self): self.assertEqual(5, utils.pagination_limit(5, 9)) self.assertEqual(5, utils.pagination_limit(9, 5)) + + def test_encode_decode_string(self): + random_data = bytearray(Random.new().read(12)) + data = ['abc', 'numbers01234', '\x00\xFF\x00\xFF\xFF\x00', random_data] + + for datum in data: + encoded_data = utils.encode_string(datum) + decoded_data = utils.decode_string(encoded_data) + self. assertEqual(datum, decoded_data, + "Encode/decode failed") + + def test_pad_unpad(self): + for size in range(1, 100): + data_str = 'a' * size + padded_str = utils.pad_for_encryption(data_str, utils.IV_BIT_COUNT) + self.assertEqual(0, len(padded_str) % utils.IV_BIT_COUNT, + "Padding not successful") + unpadded_str = utils.unpad_after_decryption(padded_str) + self.assertEqual(data_str, unpadded_str, + "String mangled after pad/unpad") + + def test_encryp_decrypt(self): + key = 'my_secure_key' + for size in range(1, 100): + orig_str = '' + for index in range(1, size): + orig_str += Random.new().read(1) + orig_encoded = utils.encode_string(orig_str) + encrypted = utils.encrypt_string(orig_encoded, key) + encoded = utils.encode_string(encrypted) + decoded = utils.decode_string(encoded) + decrypted = utils.decrypt_string(decoded, key) + final_decoded = utils.decode_string(decrypted) + + self.assertEqual(orig_str, final_decoded, + "String did not match original") diff --git a/trove/tests/unittests/module/__init__.py b/trove/tests/unittests/module/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/trove/tests/unittests/module/test_module_controller.py b/trove/tests/unittests/module/test_module_controller.py new file mode 100644 index 0000000000..08c6d7d76d --- /dev/null +++ b/trove/tests/unittests/module/test_module_controller.py @@ -0,0 +1,80 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import jsonschema +from testtools.matchers import Is, Equals + +from trove.module.service import ModuleController +from trove.tests.unittests import trove_testtools + + +class TestModuleController(trove_testtools.TestCase): + def setUp(self): + super(TestModuleController, self).setUp() + self.controller = ModuleController() + self.module = { + "module": { + "name": 'test_module', + "module_type": 'test', + "contents": 'my_contents\n', + } + } + + def verify_errors(self, errors, msg=None, properties=None, path=None): + msg = msg or [] + properties = properties or [] + self.assertThat(len(errors), Is(len(msg))) + i = 0 + while i < len(msg): + self.assertIn(errors[i].message, msg) + if path: + self.assertThat(path, Equals(properties[i])) + else: + self.assertThat(errors[i].path.pop(), Equals(properties[i])) + i += 1 + + def test_get_schema_create(self): + schema = self.controller.get_schema('create', {'module': {}}) + self.assertIsNotNone(schema) + self.assertTrue('module' in schema['properties']) + + def test_validate_create_complete(self): + body = self.module + schema = self.controller.get_schema('create', body) + validator = jsonschema.Draft4Validator(schema) + self.assertTrue(validator.is_valid(body)) + + def test_validate_create_blankname(self): + body = self.module + body['module']['name'] = " " + schema = self.controller.get_schema('create', body) + validator = jsonschema.Draft4Validator(schema) + self.assertFalse(validator.is_valid(body)) + errors = sorted(validator.iter_errors(body), key=lambda e: e.path) + self.assertThat(len(errors), Is(1)) + self.assertThat(errors[0].message, + Equals("' ' does not match '^.*[0-9a-zA-Z]+.*$'")) + + def test_validate_create_invalid_name(self): + body = self.module + body['module']['name'] = "$#$%^^" + schema = self.controller.get_schema('create', body) + validator = jsonschema.Draft4Validator(schema) + self.assertFalse(validator.is_valid(body)) + errors = sorted(validator.iter_errors(body), key=lambda e: e.path) + self.assertEqual(1, len(errors)) + self.assertIn("'$#$%^^' does not match '^.*[0-9a-zA-Z]+.*$'", + errors[0].message) diff --git a/trove/tests/unittests/module/test_module_models.py b/trove/tests/unittests/module/test_module_models.py new file mode 100644 index 0000000000..21e1033d18 --- /dev/null +++ b/trove/tests/unittests/module/test_module_models.py @@ -0,0 +1,50 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from mock import Mock, patch + +from trove.common import cfg +from trove.module import models +from trove.taskmanager import api as task_api +from trove.tests.unittests import trove_testtools +from trove.tests.unittests.util import util + +CONF = cfg.CONF + + +class CreateModuleTest(trove_testtools.TestCase): + + @patch.object(task_api.API, 'get_client', Mock(return_value=Mock())) + def setUp(self): + util.init_db() + self.context = Mock() + self.name = "name" + self.module_type = 'test' + self.contents = 'my_contents\n' + + super(CreateModuleTest, self).setUp() + + @patch.object(task_api.API, 'get_client', Mock(return_value=Mock())) + def tearDown(self): + super(CreateModuleTest, self).tearDown() + + def test_can_create_module(self): + module = models.Module.create( + self.context, + self.name, self.module_type, self.contents, + 'my desc', 'my_tenant', None, None, False, True, False) + self.assertIsNotNone(module) + module.delete() diff --git a/trove/tests/unittests/module/test_module_views.py b/trove/tests/unittests/module/test_module_views.py new file mode 100644 index 0000000000..ddcb825698 --- /dev/null +++ b/trove/tests/unittests/module/test_module_views.py @@ -0,0 +1,71 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from mock import Mock, patch +from trove.datastore import models +from trove.module.views import DetailedModuleView +from trove.tests.unittests import trove_testtools + + +class ModuleViewsTest(trove_testtools.TestCase): + + def setUp(self): + super(ModuleViewsTest, self).setUp() + + def tearDown(self): + super(ModuleViewsTest, self).tearDown() + + +class DetailedModuleViewTest(trove_testtools.TestCase): + + def setUp(self): + super(DetailedModuleViewTest, self).setUp() + self.module = Mock() + self.module.name = 'test_module' + self.module.type = 'test' + self.module.md5 = 'md5-hash' + self.module.created = 'Yesterday' + self.module.updated = 'Now' + self.module.datastore = 'mysql' + self.module.datastore_version = '5.6' + self.module.auto_apply = False + self.module.tenant_id = 'my_tenant' + + def tearDown(self): + super(DetailedModuleViewTest, self).tearDown() + + def test_data(self): + datastore = Mock() + datastore.name = self.module.datastore + ds_version = Mock() + ds_version.name = self.module.datastore_version + with patch.object(models, 'get_datastore_version', + Mock(return_value=(datastore, ds_version))): + view = DetailedModuleView(self.module) + result = view.data() + self.assertEqual(self.module.name, result['module']['name']) + self.assertEqual(self.module.type, result['module']['type']) + self.assertEqual(self.module.md5, result['module']['md5']) + self.assertEqual(self.module.created, result['module']['created']) + self.assertEqual(self.module.updated, result['module']['updated']) + self.assertEqual(self.module.datastore_version, + result['module']['datastore_version']) + self.assertEqual(self.module.datastore, + result['module']['datastore']) + self.assertEqual(self.module.auto_apply, + result['module']['auto_apply']) + self.assertEqual(self.module.tenant_id, + result['module']['tenant_id'])