From 89274df2aff829cf73920e030679c6fb0bd461e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=C5=9Fit=20Demir?= Date: Fri, 10 Mar 2023 06:47:14 +0000 Subject: [PATCH] Working on missing postgresql features * Db list operation added for postgresql * User list operation added for postgresql * User create operation added for postgresql * User delete operation added for postgresql * Db create operation added for postgresql * Db delete operation added for postgresql * User grant access operation added for postgresql * User revoke access and update attributes operations added for postgresql * Common/db/models collate and character_set setter functions are updated. Depends-On: https://review.opendev.org/c/openstack/trove-tempest-plugin/+/930276 Change-Id: I4c000363dd046fc82f099de0a30c16b1f01d6d2c --- .gitignore | 1 + trove/common/db/models.py | 22 ++ trove/conductor/manager.py | 4 +- trove/db/sqlalchemy/session.py | 4 +- trove/extensions/{mysql => common}/common.py | 8 +- trove/extensions/common/models.py | 263 +++++++++++- trove/extensions/common/service.py | 344 ++++++++++++++++ trove/extensions/common/views.py | 46 +++ trove/extensions/mgmt/instances/models.py | 6 +- trove/extensions/mgmt/instances/service.py | 8 +- trove/extensions/mysql/__init__.py | 0 trove/extensions/mysql/models.py | 273 ------------- trove/extensions/mysql/service.py | 373 ------------------ trove/extensions/mysql/views.py | 74 ---- trove/extensions/routes/mysql.py | 7 +- trove/instance/service.py | 4 +- trove/taskmanager/models.py | 4 +- trove/tests/unittests/mysql/test_common.py | 4 +- .../unittests/mysql/test_user_controller.py | 6 +- .../unittests/taskmanager/test_models.py | 11 +- 20 files changed, 707 insertions(+), 755 deletions(-) rename trove/extensions/{mysql => common}/common.py (91%) delete mode 100644 trove/extensions/mysql/__init__.py delete mode 100644 trove/extensions/mysql/models.py delete mode 100644 trove/extensions/mysql/service.py delete mode 100644 trove/extensions/mysql/views.py diff --git a/.gitignore b/.gitignore index 0a9dbf570d..d5dc2e66b3 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ publish-docs/ *~ .*.swp .bak +.idea/ # Config sample and policy sample etc/trove/*.sample diff --git a/trove/common/db/models.py b/trove/common/db/models.py index 1b5a1f1f7f..fb07a4b92e 100644 --- a/trove/common/db/models.py +++ b/trove/common/db/models.py @@ -114,6 +114,28 @@ class DatastoreSchema(DatastoreModelsBase): self._validate_schema_name(value) self._name = value + @property + def collate(self): + return self._collate + + @collate.setter + def collate(self, value): + if not value: + pass + else: + self._collate = value + + @property + def character_set(self): + return self._character_set + + @character_set.setter + def character_set(self, value): + if not value: + pass + else: + self._character_set = value + def _validate_schema_name(self, value): """Perform checks on a given schema name. :param value: Validated schema name. diff --git a/trove/conductor/manager.py b/trove/conductor/manager.py index d5738e198c..d2be8f29d2 100644 --- a/trove/conductor/manager.py +++ b/trove/conductor/manager.py @@ -22,7 +22,7 @@ from trove.common import exception as trove_exception from trove.common.rpc import version as rpc_version from trove.common.serializable_notification import SerializableNotification from trove.conductor.models import LastSeen -from trove.extensions.mysql import models as mysql_models +from trove.extensions.common import models as common_models from trove.instance import models as inst_models from trove.instance import service_status as svc_status @@ -150,7 +150,7 @@ class Manager(periodic_task.PeriodicTasks): if user is not None: LOG.debug("calling report_root with a username: %s, " "is deprecated now!" % user) - mysql_models.RootHistory.create(context, instance_id) + common_models.RootHistory.create(context, instance_id) def notify_end(self, context, serialized_notification, notification_args): notification = SerializableNotification.deserialize( diff --git a/trove/db/sqlalchemy/session.py b/trove/db/sqlalchemy/session.py index a7d01a11a5..93b764ec7c 100644 --- a/trove/db/sqlalchemy/session.py +++ b/trove/db/sqlalchemy/session.py @@ -41,7 +41,7 @@ def configure_db(models_mapper=None): from trove.configuration import models as configurations_models from trove.datastore import models as datastores_models from trove.dns import models as dns_models - from trove.extensions.mysql import models as mysql_models + from trove.extensions.common import models as common_models 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 @@ -52,7 +52,7 @@ def configure_db(models_mapper=None): base_models, datastores_models, dns_models, - mysql_models, + common_models, agent_models, quota_models, backup_models, diff --git a/trove/extensions/mysql/common.py b/trove/extensions/common/common.py similarity index 91% rename from trove/extensions/mysql/common.py rename to trove/extensions/common/common.py index 3ab4b068cf..4244f1b969 100644 --- a/trove/extensions/mysql/common.py +++ b/trove/extensions/common/common.py @@ -14,7 +14,7 @@ from urllib.parse import unquote -from trove.common.db.mysql import models as guest_models +from trove.common.db import models as guest_models from trove.common import exception @@ -27,7 +27,7 @@ def populate_validated_databases(dbs): databases = [] unique_identities = set() for database in dbs: - mydb = guest_models.MySQLSchema(name=database.get('name', '')) + mydb = guest_models.DatastoreSchema(name=database.get('name', '')) mydb.check_reserved() if mydb.name in unique_identities: raise exception.DatabaseInitialDatabaseDuplicateError() @@ -49,8 +49,8 @@ def populate_users(users, initial_databases=None): users_data = [] unique_identities = set() for user in users: - u = guest_models.MySQLUser(name=user.get('name', ''), - host=user.get('host', '%')) + u = guest_models.DatastoreUser(name=user.get('name', ''), + host=user.get('host', '%')) u.check_reserved() user_identity = (u.name, u.host) if user_identity in unique_identities: diff --git a/trove/extensions/common/models.py b/trove/extensions/common/models.py index 0ea791d066..f619c89fe7 100644 --- a/trove/extensions/common/models.py +++ b/trove/extensions/common/models.py @@ -22,12 +22,17 @@ from trove.common import timeutils from trove.db import get_db_api from trove.instance import models as base_models +from trove.common import cfg +from trove.common.notification import StartNotification +from trove.common import utils LOG = logging.getLogger(__name__) +CONF = cfg.CONF + def load_and_verify(context, instance_id, - enabled_datastore=['mysql', 'mariadb']): + enabled_datastore=['mysql', 'mariadb', 'postgresql']): """Check instance datastore. Some API operations are only supported for some specific datastores. @@ -141,3 +146,259 @@ class RootHistory(object): return history history = RootHistory(instance_id, context.user_id) return history.save() + + +def load_via_context(cls, context, instance_id): + """Creates guest and fetches pagination arguments from the context.""" + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + limit = utils.pagination_limit(context.limit, cls.DEFAULT_LIMIT) + client = create_guest_client(context, instance_id) + # The REST API standard dictates that we *NEVER* include the marker. + return cls.load_with_client(client=client, limit=limit, + marker=context.marker, include_marker=False) + + +def persisted_models(): + return {'root_enabled_history': RootHistory} + + +class User(object): + + _data_fields = ['name', 'host', 'password', 'databases'] + + def __init__(self, name, host, password, databases): + self.name = name + self.host = host + self.password = password + self.databases = databases + + @classmethod + def load(cls, context, instance_id, username, hostname, root_user=False): + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + validate = guest_models.DatastoreUser(name=username, host=hostname) + if root_user: + validate.make_root() + validate.check_reserved() + client = create_guest_client(context, instance_id) + found_user = client.get_user(username=username, hostname=hostname) + if not found_user: + return None + database_names = [{'name': db['_name']} + for db in found_user['_databases']] + return cls(found_user['_name'], + found_user['_host'], + found_user['_password'], + database_names) + + @classmethod + def create(cls, context, instance_id, users): + # Load InstanceServiceStatus to verify if it's running + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + client = create_guest_client(context, instance_id) + for user in users: + user_name = user['_name'] + host_name = user['_host'] + userhost = "%s@%s" % (user_name, host_name) + existing_users, _nadda = Users.load_with_client( + client, + limit=1, + marker=userhost, + include_marker=True) + if (len(existing_users) > 0 and + str(existing_users[0].name) == str(user_name) and + str(existing_users[0].host) == str(host_name)): + raise exception.UserAlreadyExists(name=user_name, + host=host_name) + return client.create_user(users) + + @classmethod + def delete(cls, context, instance_id, user): + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + + with StartNotification(context, instance_id=instance_id, + username=user): + create_guest_client(context, instance_id).delete_user(user) + + @classmethod + def access(cls, context, instance_id, username, hostname): + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + client = create_guest_client(context, instance_id) + databases = client.list_access(username, hostname) + dbs = [] + for db in databases: + dbs.append(Schema(name=db['_name'], + collate=db['_collate'], + character_set=db['_character_set'])) + return UserAccess(dbs) + + @classmethod + def grant(cls, context, instance_id, username, hostname, databases): + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + client = create_guest_client(context, instance_id) + client.grant_access(username, hostname, databases) + + @classmethod + def revoke(cls, context, instance_id, username, hostname, database): + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + client = create_guest_client(context, instance_id) + client.revoke_access(username, hostname, database) + + @classmethod + def change_password(cls, context, instance_id, users): + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + client = create_guest_client(context, instance_id) + change_users = [] + for user in users: + change_user = {'name': user.name, + 'host': user.host, + 'password': user.password, + } + change_users.append(change_user) + client.change_passwords(change_users) + + @classmethod + def update_attributes(cls, context, instance_id, username, hostname, + user_attrs): + load_and_verify(context, instance_id) + client = create_guest_client(context, instance_id) + + user_changed = user_attrs.get('name') + host_changed = user_attrs.get('host') + + user = user_changed or username + host = host_changed or hostname + + validate = guest_models.DatastoreUser(name=user, host=host) + validate.check_reserved() + + userhost = "%s@%s" % (user, host) + if user_changed or host_changed: + existing_users, _nadda = Users.load_with_client( + client, + limit=1, + marker=userhost, + include_marker=True) + if (len(existing_users) > 0 and + existing_users[0].name == user and + existing_users[0].host == host): + raise exception.UserAlreadyExists(name=user, + host=host) + client.update_attributes(username, hostname, user_attrs) + + +class Users(object): + + DEFAULT_LIMIT = CONF.users_page_size + + @classmethod + def load(cls, context, instance_id): + return load_via_context(cls, context, instance_id) + + @classmethod + def load_with_client(cls, client, limit, marker, include_marker): + user_list, next_marker = client.list_users( + limit=limit, + marker=marker, + include_marker=include_marker) + model_users = [] + for user in user_list: + guest_user = guest_models.DatastoreUser.deserialize(user, + verify=False) + if guest_user.name in cfg.get_ignored_users(): + continue + # TODO(hub-cap): databases are not being returned in the + # reference agent + dbs = [] + for db in guest_user.databases: + dbs.append({'name': db['_name']}) + model_users.append(User(guest_user.name, + guest_user.host, + guest_user.password, + dbs)) + return model_users, next_marker + + +class UserAccess(object): + _data_fields = ['databases'] + + def __init__(self, databases): + self.databases = databases + + +class Schema(object): + + _data_fields = ['name', 'collate', 'character_set'] + + def __init__(self, name, collate, character_set): + self.name = name + self.collate = collate + self.character_set = character_set + + @classmethod + def create(cls, context, instance_id, schemas): + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + client = create_guest_client(context, instance_id) + for schema in schemas: + schema_name = schema['_name'] + existing_schema, _nadda = Schemas.load_with_client( + client, + limit=1, + marker=schema_name, + include_marker=True) + if (len(existing_schema) > 0 and + str(existing_schema[0].name) == str(schema_name)): + raise exception.DatabaseAlreadyExists(name=schema_name) + return client.create_database(schemas) + + @classmethod + def delete(cls, context, instance_id, schema): + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + create_guest_client(context, instance_id).delete_database(schema) + + +class Schemas(object): + + DEFAULT_LIMIT = CONF.databases_page_size + + @classmethod + def load(cls, context, instance_id): + return load_via_context(cls, context, instance_id) + + @classmethod + def load_with_client(cls, client, limit, marker, include_marker): + schemas, next_marker = client.list_databases( + limit=limit, + marker=marker, + include_marker=include_marker) + model_schemas = [] + for schema in schemas: + guest_schema = guest_models.DatastoreSchema.deserialize( + schema, verify=False) + if guest_schema.name in cfg.get_ignored_dbs(): + continue + + model_schemas.append(Schema(guest_schema.name, + guest_schema.collate, + guest_schema.character_set)) + return model_schemas, next_marker + + @classmethod + def find(cls, context, instance_id, schema_id): + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) + client = create_guest_client(context, instance_id) + model_schemas, _ = cls.load_with_client(client, 1, schema_id, True) + if model_schemas and model_schemas[0].name == schema_id: + return model_schemas[0] + + return None diff --git a/trove/extensions/common/service.py b/trove/extensions/common/service.py index 42465ad5b2..3cecc11392 100644 --- a/trove/extensions/common/service.py +++ b/trove/extensions/common/service.py @@ -33,6 +33,19 @@ from trove.extensions.common import views from trove.instance import models as instance_models from trove.instance.models import DBInstance +from oslo_utils import strutils +import webob.exc + +import trove.common.apischema as apischema +from trove.common.db import models as guest_models +from trove.common import notification +from trove.common.notification import StartNotification +from trove.common import pagination +from trove.common.utils import correct_id_with_req +from trove.extensions.common.common import populate_users +from trove.extensions.common.common import populate_validated_databases +from trove.extensions.common.common import unquote_user_host + LOG = logging.getLogger(__name__) import_class = importutils.import_class @@ -270,3 +283,334 @@ class RootController(ExtensionController): f"root_controller not configured for datastore {manager}") raise exception.DatastoreOperationNotSupported( datastore=manager, operation='root') + + +class UserController(ExtensionController): + """Controller for instance functionality.""" + schemas = apischema.user + + @classmethod + def get_schema(cls, action, body): + action_schema = super(UserController, cls).get_schema(action, body) + if 'update_all' == action: + update_type = list(body.keys())[0] + action_schema = action_schema.get(update_type, {}) + return action_schema + + def index(self, req, tenant_id, instance_id): + """Return all users.""" + LOG.info("Listing users for instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": req}) + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action(context, 'user:index', instance_id) + users, next_marker = models.Users.load(context, instance_id) + view = views.UsersView(users) + paged = pagination.SimplePaginatedDataView(req.url, 'users', view, + next_marker) + return wsgi.Result(paged.data(), 200) + + def create(self, req, body, tenant_id, instance_id): + """Creates a set of users.""" + LOG.info("Creating users for instance '%(id)s'\n" + "req : '%(req)s'\n\n" + "body: '%(body)s'\n'n", + {"id": instance_id, + "req": strutils.mask_password(req), + "body": strutils.mask_password(body)}) + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action(context, 'user:create', instance_id) + context.notification = notification.DBaaSUserCreate(context, + request=req) + users = body['users'] + with StartNotification(context, instance_id=instance_id, + username=",".join([user['name'] + for user in users])): + try: + model_users = populate_users(users) + models.User.create(context, instance_id, model_users) + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("User create error: %(e)s") + % {'e': e}) + return wsgi.Result(None, 202) + + def delete(self, req, tenant_id, instance_id, id): + LOG.info("Delete instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": req}) + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action(context, 'user:delete', instance_id) + id = correct_id_with_req(id, req) + username, host = unquote_user_host(id) + user = None + context.notification = notification.DBaaSUserDelete(context, + request=req) + with StartNotification(context, instance_id=instance_id, + username=username): + try: + user = guest_models.DatastoreUser(name=username, + host=host) + found_user = models.User.load(context, instance_id, username, + host) + if not found_user: + user = None + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("User delete error: %(e)s") + % {'e': e}) + if not user: + raise exception.UserNotFound(uuid=id) + models.User.delete(context, instance_id, user.serialize()) + return wsgi.Result(None, 202) + + def show(self, req, tenant_id, instance_id, id): + """Return a single user.""" + LOG.info("Showing a user for instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": req}) + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action(context, 'user:show', instance_id) + id = correct_id_with_req(id, req) + username, host = unquote_user_host(id) + user = None + try: + user = models.User.load(context, instance_id, username, host) + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("User show error: %(e)s") + % {'e': e}) + if not user: + raise exception.UserNotFound(uuid=id) + view = views.UserView(user) + return wsgi.Result(view.data(), 200) + + def update(self, req, body, tenant_id, instance_id, id): + """Change attributes for one user.""" + LOG.info("Updating user attributes for instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": strutils.mask_password(req)}) + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action(context, 'user:update', instance_id) + id = correct_id_with_req(id, req) + username, hostname = unquote_user_host(id) + user = None + user_attrs = body['user'] + context.notification = notification.DBaaSUserUpdateAttributes( + context, request=req) + with StartNotification(context, instance_id=instance_id, + username=username): + try: + user = models.User.load(context, instance_id, username, + hostname) + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("Error loading user: %(e)s") + % {'e': e}) + if not user: + raise exception.UserNotFound(uuid=id) + try: + models.User.update_attributes(context, instance_id, username, + hostname, user_attrs) + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("User update error: %(e)s") + % {'e': e}) + return wsgi.Result(None, 202) + + def update_all(self, req, body, tenant_id, instance_id): + """Change the password of one or more users.""" + LOG.info("Updating user password for instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": strutils.mask_password(req)}) + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action(context, 'user:update_all', instance_id) + context.notification = notification.DBaaSUserChangePassword( + context, request=req) + users = body['users'] + model_users = [] + with StartNotification(context, instance_id=instance_id, + username=",".join([user['name'] + for user in users])): + for user in users: + try: + mu = guest_models.DatastoreUser(name=user['name'], + host=user.get('host'), + password=user['password']) + found_user = models.User.load(context, instance_id, + mu.name, mu.host) + if not found_user: + user_and_host = mu.name + if mu.host: + user_and_host += '@' + mu.host + raise exception.UserNotFound(uuid=user_and_host) + model_users.append(mu) + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("Error loading user: %(e)s") + % {'e': e}) + try: + models.User.change_password(context, instance_id, model_users) + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("User password update error: " + "%(e)s") + % {'e': e}) + return wsgi.Result(None, 202) + + +class UserAccessController(ExtensionController): + """Controller for adding and removing database access for a user.""" + schemas = apischema.user + + @classmethod + def get_schema(cls, action, body): + schema = {} + if 'update_all' == action: + schema = cls.schemas.get(action).get('databases') + return schema + + def _get_user(self, context, instance_id, user_id): + username, hostname = unquote_user_host(user_id) + try: + user = models.User.load(context, instance_id, username, hostname) + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("Error loading user: %(e)s") + % {'e': e}) + if not user: + raise exception.UserNotFound(uuid=user_id) + return user + + def index(self, req, tenant_id, instance_id, user_id): + """Show permissions for the given user.""" + LOG.info("Showing user access for instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": req}) + + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action( + context, 'user_access:index', instance_id) + # Make sure this user exists. + user_id = correct_id_with_req(user_id, req) + user = self._get_user(context, instance_id, user_id) + if not user: + LOG.error("No such user: %(user)s ", {'user': user}) + raise exception.UserNotFound(uuid=user) + username, hostname = unquote_user_host(user_id) + access = models.User.access(context, instance_id, username, hostname) + view = views.UserAccessView(access.databases) + return wsgi.Result(view.data(), 200) + + def update(self, req, body, tenant_id, instance_id, user_id): + """Grant access for a user to one or more databases.""" + LOG.info("Granting user access for instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": req}) + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action( + context, 'user_access:update', instance_id) + context.notification = notification.DBaaSUserGrant( + context, request=req) + user_id = correct_id_with_req(user_id, req) + user = self._get_user(context, instance_id, user_id) + if not user: + LOG.error("No such user: %(user)s ", {'user': user}) + raise exception.UserNotFound(uuid=user) + username, hostname = unquote_user_host(user_id) + databases = [db['name'] for db in body['databases']] + with StartNotification(context, instance_id=instance_id, + username=username, database=databases): + models.User.grant(context, instance_id, username, hostname, + databases) + return wsgi.Result(None, 202) + + def delete(self, req, tenant_id, instance_id, user_id, id): + """Revoke access for a user.""" + LOG.info("Revoking user access for instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": req}) + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action( + context, 'user_access:delete', instance_id) + context.notification = notification.DBaaSUserRevoke( + context, request=req) + user_id = correct_id_with_req(user_id, req) + user = self._get_user(context, instance_id, user_id) + if not user: + LOG.error("No such user: %(user)s ", {'user': user}) + raise exception.UserNotFound(uuid=user) + username, hostname = unquote_user_host(user_id) + access = models.User.access(context, instance_id, username, hostname) + databases = [db.name for db in access.databases] + with StartNotification(context, instance_id=instance_id, + username=username, database=databases): + if id not in databases: + raise exception.DatabaseNotFound(uuid=id) + models.User.revoke(context, instance_id, username, hostname, id) + return wsgi.Result(None, 202) + + +class SchemaController(ExtensionController): + """Controller for instance functionality.""" + schemas = apischema.dbschema + + def index(self, req, tenant_id, instance_id): + """Return all schemas.""" + LOG.info("Listing schemas for instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": req}) + + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action( + context, 'database:index', instance_id) + schemas, next_marker = models.Schemas.load(context, instance_id) + view = views.SchemasView(schemas) + paged = pagination.SimplePaginatedDataView(req.url, 'databases', view, + next_marker) + return wsgi.Result(paged.data(), 200) + + def create(self, req, body, tenant_id, instance_id): + """Creates a set of schemas.""" + LOG.info("Creating schema for instance '%(id)s'\n" + "req : '%(req)s'\n\n" + "body: '%(body)s'\n'n", + {"id": instance_id, + "req": req, + "body": body}) + + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action( + context, 'database:create', instance_id) + schemas = body['databases'] + context.notification = notification.DBaaSDatabaseCreate(context, + request=req) + with StartNotification(context, instance_id=instance_id, + dbname=".".join([db['name'] + for db in schemas])): + try: + model_schemas = populate_validated_databases(schemas) + models.Schema.create(context, instance_id, model_schemas) + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("Database create error: %(e)s") + % {'e': e}) + return wsgi.Result(None, 202) + + def delete(self, req, tenant_id, instance_id, id): + LOG.info("Deleting schema for instance '%(id)s'\n" + "req : '%(req)s'\n\n", + {"id": instance_id, "req": req}) + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action( + context, 'database:delete', instance_id) + context.notification = notification.DBaaSDatabaseDelete( + context, request=req) + with StartNotification(context, instance_id=instance_id, dbname=id): + try: + schema = guest_models.DatastoreSchema(name=id) + schema.check_delete() + if not models.Schemas.find(context, instance_id, id): + raise exception.DatabaseNotFound(uuid=id) + models.Schema.delete(context, instance_id, schema.serialize()) + except (ValueError, AttributeError) as e: + raise exception.BadRequest(_("Database delete error: %(e)s") + % {'e': e}) + return wsgi.Result(None, 202) + + def show(self, req, tenant_id, instance_id, id): + context = req.environ[wsgi.CONTEXT_KEY] + self.authorize_target_action( + context, 'database:show', instance_id) + raise webob.exc.HTTPNotImplemented() diff --git a/trove/extensions/common/views.py b/trove/extensions/common/views.py index 098028d5bb..bbbdc3a7d4 100644 --- a/trove/extensions/common/views.py +++ b/trove/extensions/common/views.py @@ -45,3 +45,49 @@ class RootEnabledView(object): def data(self): return {'rootEnabled': self.is_root_enabled} + + +class UsersView(object): + + def __init__(self, users): + self.users = users + + def data(self): + userlist = [{"name": user.name, + "host": user.host, + "databases": user.databases} + for user in self.users] + + return {"users": userlist} + + +class UserAccessView(object): + def __init__(self, databases): + self.databases = databases + + def data(self): + dbs = [{"name": db.name} for db in self.databases] + return {"databases": dbs} + + +class SchemaView(object): + + def __init__(self, schema): + self.schema = schema + + def data(self): + return {"name": self.schema.name} + + +class SchemasView(object): + + def __init__(self, schemas): + self.schemas = schemas + + def data(self): + data = [] + # These are model instances + for schema in self.schemas: + data.append(SchemaView(schema).data()) + + return {"databases": data} diff --git a/trove/extensions/mgmt/instances/models.py b/trove/extensions/mgmt/instances/models.py index c70a550e5d..a6726d7d61 100644 --- a/trove/extensions/mgmt/instances/models.py +++ b/trove/extensions/mgmt/instances/models.py @@ -19,7 +19,7 @@ from trove.common import cfg from trove.common import clients from trove.common import exception from trove.common import timeutils -from trove.extensions.mysql import models as mysql_models +from trove.extensions.common import models as common_models from trove.instance import models as instance_models from trove import rpc @@ -131,8 +131,8 @@ class DetailedMgmtInstance(SimpleMgmtInstance): instance.volume = None # Populate the volume_used attribute from the guest agent. instance_models.load_guest_info(instance, context, id) - instance.root_history = mysql_models.RootHistory.load(context=context, - instance_id=id) + instance.root_history = common_models.RootHistory.load(context=context, + instance_id=id) return instance diff --git a/trove/extensions/mgmt/instances/service.py b/trove/extensions/mgmt/instances/service.py index b05b94af42..d8b6e32451 100644 --- a/trove/extensions/mgmt/instances/service.py +++ b/trove/extensions/mgmt/instances/service.py @@ -25,11 +25,11 @@ from trove.common.i18n import _ from trove.common import notification from trove.common.notification import StartNotification from trove.common import wsgi +from trove.extensions.common import models as common_models from trove.extensions.mgmt.instances import models from trove.extensions.mgmt.instances import views from trove.extensions.mgmt.instances.views import DiagnosticsView from trove.extensions.mgmt.instances.views import HwInfoView -from trove.extensions.mysql import models as mysql_models from trove.instance import models as instance_models from trove.instance.service import InstanceController @@ -85,8 +85,8 @@ class MgmtInstanceController(InstanceController): include_deleted = deleted_q == 'true' server = models.DetailedMgmtInstance.load(context, id, include_deleted) - root_history = mysql_models.RootHistory.load(context=context, - instance_id=id) + root_history = common_models.RootHistory.load(context=context, + instance_id=id) return wsgi.Result( views.MgmtInstanceDetailView( server, @@ -189,7 +189,7 @@ class MgmtInstanceController(InstanceController): LOG.exception(e) return wsgi.Result(str(e), 404) rhv = views.RootHistoryView(id) - reh = mysql_models.RootHistory.load(context=context, instance_id=id) + reh = common_models.RootHistory.load(context=context, instance_id=id) if reh: rhv = views.RootHistoryView(reh.id, enabled=reh.created, user_id=reh.user) diff --git a/trove/extensions/mysql/__init__.py b/trove/extensions/mysql/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/trove/extensions/mysql/models.py b/trove/extensions/mysql/models.py deleted file mode 100644 index 5b17320539..0000000000 --- a/trove/extensions/mysql/models.py +++ /dev/null @@ -1,273 +0,0 @@ -# Copyright 2010-2011 OpenStack Foundation -# 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 extend the instances functionality for MySQL instances. -""" - -from trove.common import cfg -from trove.common.clients import create_guest_client -from trove.common.db.mysql import models as guest_models -from trove.common import exception -from trove.common.notification import StartNotification -from trove.common import utils -from trove.extensions.common.models import load_and_verify -from trove.extensions.common.models import RootHistory - -CONF = cfg.CONF - - -def persisted_models(): - return {'root_enabled_history': RootHistory} - - -class User(object): - - _data_fields = ['name', 'host', 'password', 'databases'] - - def __init__(self, name, host, password, databases): - self.name = name - self.host = host - self.password = password - self.databases = databases - - @classmethod - def load(cls, context, instance_id, username, hostname, root_user=False): - load_and_verify(context, instance_id) - validate = guest_models.MySQLUser(name=username, host=hostname) - if root_user: - validate.make_root() - validate.check_reserved() - client = create_guest_client(context, instance_id) - found_user = client.get_user(username=username, hostname=hostname) - if not found_user: - return None - database_names = [{'name': db['_name']} - for db in found_user['_databases']] - return cls(found_user['_name'], - found_user['_host'], - found_user['_password'], - database_names) - - @classmethod - def create(cls, context, instance_id, users): - # Load InstanceServiceStatus to verify if it's running - load_and_verify(context, instance_id) - client = create_guest_client(context, instance_id) - for user in users: - user_name = user['_name'] - host_name = user['_host'] - userhost = "%s@%s" % (user_name, host_name) - existing_users, _nadda = Users.load_with_client( - client, - limit=1, - marker=userhost, - include_marker=True) - if (len(existing_users) > 0 and - str(existing_users[0].name) == str(user_name) and - str(existing_users[0].host) == str(host_name)): - raise exception.UserAlreadyExists(name=user_name, - host=host_name) - return client.create_user(users) - - @classmethod - def delete(cls, context, instance_id, user): - load_and_verify(context, instance_id) - - with StartNotification(context, instance_id=instance_id, - username=user): - create_guest_client(context, instance_id).delete_user(user) - - @classmethod - def access(cls, context, instance_id, username, hostname): - load_and_verify(context, instance_id) - client = create_guest_client(context, instance_id) - databases = client.list_access(username, hostname) - dbs = [] - for db in databases: - dbs.append(Schema(name=db['_name'], - collate=db['_collate'], - character_set=db['_character_set'])) - return UserAccess(dbs) - - @classmethod - def grant(cls, context, instance_id, username, hostname, databases): - load_and_verify(context, instance_id) - client = create_guest_client(context, instance_id) - client.grant_access(username, hostname, databases) - - @classmethod - def revoke(cls, context, instance_id, username, hostname, database): - load_and_verify(context, instance_id) - client = create_guest_client(context, instance_id) - client.revoke_access(username, hostname, database) - - @classmethod - def change_password(cls, context, instance_id, users): - load_and_verify(context, instance_id) - client = create_guest_client(context, instance_id) - change_users = [] - for user in users: - change_user = {'name': user.name, - 'host': user.host, - 'password': user.password, - } - change_users.append(change_user) - client.change_passwords(change_users) - - @classmethod - def update_attributes(cls, context, instance_id, username, hostname, - user_attrs): - load_and_verify(context, instance_id) - client = create_guest_client(context, instance_id) - - user_changed = user_attrs.get('name') - host_changed = user_attrs.get('host') - - user = user_changed or username - host = host_changed or hostname - - validate = guest_models.MySQLUser(name=user, host=host) - validate.check_reserved() - - userhost = "%s@%s" % (user, host) - if user_changed or host_changed: - existing_users, _nadda = Users.load_with_client( - client, - limit=1, - marker=userhost, - include_marker=True) - if (len(existing_users) > 0 and - existing_users[0].name == user and - existing_users[0].host == host): - raise exception.UserAlreadyExists(name=user, - host=host) - client.update_attributes(username, hostname, user_attrs) - - -class UserAccess(object): - _data_fields = ['databases'] - - def __init__(self, databases): - self.databases = databases - - -def load_via_context(cls, context, instance_id): - """Creates guest and fetches pagination arguments from the context.""" - load_and_verify(context, instance_id) - limit = utils.pagination_limit(context.limit, cls.DEFAULT_LIMIT) - client = create_guest_client(context, instance_id) - # The REST API standard dictates that we *NEVER* include the marker. - return cls.load_with_client(client=client, limit=limit, - marker=context.marker, include_marker=False) - - -class Users(object): - - DEFAULT_LIMIT = CONF.users_page_size - - @classmethod - def load(cls, context, instance_id): - return load_via_context(cls, context, instance_id) - - @classmethod - def load_with_client(cls, client, limit, marker, include_marker): - user_list, next_marker = client.list_users( - limit=limit, - marker=marker, - include_marker=include_marker) - model_users = [] - for user in user_list: - mysql_user = guest_models.MySQLUser.deserialize(user, - verify=False) - if mysql_user.name in cfg.get_ignored_users(): - continue - # TODO(hub-cap): databases are not being returned in the - # reference agent - dbs = [] - for db in mysql_user.databases: - dbs.append({'name': db['_name']}) - model_users.append(User(mysql_user.name, - mysql_user.host, - mysql_user.password, - dbs)) - return model_users, next_marker - - -class Schema(object): - - _data_fields = ['name', 'collate', 'character_set'] - - def __init__(self, name, collate, character_set): - self.name = name - self.collate = collate - self.character_set = character_set - - @classmethod - def create(cls, context, instance_id, schemas): - load_and_verify(context, instance_id) - client = create_guest_client(context, instance_id) - for schema in schemas: - schema_name = schema['_name'] - existing_schema, _nadda = Schemas.load_with_client( - client, - limit=1, - marker=schema_name, - include_marker=True) - if (len(existing_schema) > 0 and - str(existing_schema[0].name) == str(schema_name)): - raise exception.DatabaseAlreadyExists(name=schema_name) - return client.create_database(schemas) - - @classmethod - def delete(cls, context, instance_id, schema): - load_and_verify(context, instance_id) - create_guest_client(context, instance_id).delete_database(schema) - - -class Schemas(object): - - DEFAULT_LIMIT = CONF.databases_page_size - - @classmethod - def load(cls, context, instance_id): - return load_via_context(cls, context, instance_id) - - @classmethod - def load_with_client(cls, client, limit, marker, include_marker): - schemas, next_marker = client.list_databases( - limit=limit, - marker=marker, - include_marker=include_marker) - model_schemas = [] - for schema in schemas: - mysql_schema = guest_models.MySQLSchema.deserialize(schema, - verify=False) - if mysql_schema.name in cfg.get_ignored_dbs(): - continue - model_schemas.append(Schema(mysql_schema.name, - mysql_schema.collate, - mysql_schema.character_set)) - return model_schemas, next_marker - - @classmethod - def find(cls, context, instance_id, schema_id): - load_and_verify(context, instance_id) - client = create_guest_client(context, instance_id) - model_schemas, _ = cls.load_with_client(client, 1, schema_id, True) - if model_schemas and model_schemas[0].name == schema_id: - return model_schemas[0] - - return None diff --git a/trove/extensions/mysql/service.py b/trove/extensions/mysql/service.py deleted file mode 100644 index f68e1c7f88..0000000000 --- a/trove/extensions/mysql/service.py +++ /dev/null @@ -1,373 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# 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 oslo_utils import importutils -from oslo_utils import strutils -import webob.exc - -import trove.common.apischema as apischema -from trove.common import cfg -from trove.common.db.mysql import models as guest_models -from trove.common import exception -from trove.common.i18n import _ -from trove.common import notification -from trove.common.notification import StartNotification -from trove.common import pagination -from trove.common.utils import correct_id_with_req -from trove.common import wsgi -from trove.extensions.common.service import ExtensionController -from trove.extensions.mysql.common import populate_users -from trove.extensions.mysql.common import populate_validated_databases -from trove.extensions.mysql.common import unquote_user_host -from trove.extensions.mysql import models -from trove.extensions.mysql import views - - -LOG = logging.getLogger(__name__) -import_class = importutils.import_class -CONF = cfg.CONF - - -class UserController(ExtensionController): - """Controller for instance functionality.""" - schemas = apischema.user - - @classmethod - def get_schema(cls, action, body): - action_schema = super(UserController, cls).get_schema(action, body) - if 'update_all' == action: - update_type = list(body.keys())[0] - action_schema = action_schema.get(update_type, {}) - return action_schema - - def index(self, req, tenant_id, instance_id): - """Return all users.""" - LOG.info("Listing users for instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": req}) - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action(context, 'user:index', instance_id) - users, next_marker = models.Users.load(context, instance_id) - view = views.UsersView(users) - paged = pagination.SimplePaginatedDataView(req.url, 'users', view, - next_marker) - return wsgi.Result(paged.data(), 200) - - def create(self, req, body, tenant_id, instance_id): - """Creates a set of users.""" - LOG.info("Creating users for instance '%(id)s'\n" - "req : '%(req)s'\n\n" - "body: '%(body)s'\n'n", - {"id": instance_id, - "req": strutils.mask_password(req), - "body": strutils.mask_password(body)}) - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action(context, 'user:create', instance_id) - context.notification = notification.DBaaSUserCreate(context, - request=req) - users = body['users'] - with StartNotification(context, instance_id=instance_id, - username=",".join([user['name'] - for user in users])): - try: - model_users = populate_users(users) - models.User.create(context, instance_id, model_users) - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("User create error: %(e)s") - % {'e': e}) - return wsgi.Result(None, 202) - - def delete(self, req, tenant_id, instance_id, id): - LOG.info("Delete instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": req}) - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action(context, 'user:delete', instance_id) - id = correct_id_with_req(id, req) - username, host = unquote_user_host(id) - user = None - context.notification = notification.DBaaSUserDelete(context, - request=req) - with StartNotification(context, instance_id=instance_id, - username=username): - try: - user = guest_models.MySQLUser(name=username, - host=host) - found_user = models.User.load(context, instance_id, username, - host) - if not found_user: - user = None - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("User delete error: %(e)s") - % {'e': e}) - if not user: - raise exception.UserNotFound(uuid=id) - models.User.delete(context, instance_id, user.serialize()) - return wsgi.Result(None, 202) - - def show(self, req, tenant_id, instance_id, id): - """Return a single user.""" - LOG.info("Showing a user for instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": req}) - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action(context, 'user:show', instance_id) - id = correct_id_with_req(id, req) - username, host = unquote_user_host(id) - user = None - try: - user = models.User.load(context, instance_id, username, host) - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("User show error: %(e)s") - % {'e': e}) - if not user: - raise exception.UserNotFound(uuid=id) - view = views.UserView(user) - return wsgi.Result(view.data(), 200) - - def update(self, req, body, tenant_id, instance_id, id): - """Change attributes for one user.""" - LOG.info("Updating user attributes for instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": strutils.mask_password(req)}) - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action(context, 'user:update', instance_id) - id = correct_id_with_req(id, req) - username, hostname = unquote_user_host(id) - user = None - user_attrs = body['user'] - context.notification = notification.DBaaSUserUpdateAttributes( - context, request=req) - with StartNotification(context, instance_id=instance_id, - username=username): - try: - user = models.User.load(context, instance_id, username, - hostname) - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("Error loading user: %(e)s") - % {'e': e}) - if not user: - raise exception.UserNotFound(uuid=id) - try: - models.User.update_attributes(context, instance_id, username, - hostname, user_attrs) - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("User update error: %(e)s") - % {'e': e}) - return wsgi.Result(None, 202) - - def update_all(self, req, body, tenant_id, instance_id): - """Change the password of one or more users.""" - LOG.info("Updating user password for instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": strutils.mask_password(req)}) - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action(context, 'user:update_all', instance_id) - context.notification = notification.DBaaSUserChangePassword( - context, request=req) - users = body['users'] - model_users = [] - with StartNotification(context, instance_id=instance_id, - username=",".join([user['name'] - for user in users])): - for user in users: - try: - mu = guest_models.MySQLUser(name=user['name'], - host=user.get('host'), - password=user['password']) - found_user = models.User.load(context, instance_id, - mu.name, mu.host) - if not found_user: - user_and_host = mu.name - if mu.host: - user_and_host += '@' + mu.host - raise exception.UserNotFound(uuid=user_and_host) - model_users.append(mu) - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("Error loading user: %(e)s") - % {'e': e}) - try: - models.User.change_password(context, instance_id, model_users) - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("User password update error: " - "%(e)s") - % {'e': e}) - return wsgi.Result(None, 202) - - -class UserAccessController(ExtensionController): - """Controller for adding and removing database access for a user.""" - schemas = apischema.user - - @classmethod - def get_schema(cls, action, body): - schema = {} - if 'update_all' == action: - schema = cls.schemas.get(action).get('databases') - return schema - - def _get_user(self, context, instance_id, user_id): - username, hostname = unquote_user_host(user_id) - try: - user = models.User.load(context, instance_id, username, hostname) - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("Error loading user: %(e)s") - % {'e': e}) - if not user: - raise exception.UserNotFound(uuid=user_id) - return user - - def index(self, req, tenant_id, instance_id, user_id): - """Show permissions for the given user.""" - LOG.info("Showing user access for instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": req}) - - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action( - context, 'user_access:index', instance_id) - # Make sure this user exists. - user_id = correct_id_with_req(user_id, req) - user = self._get_user(context, instance_id, user_id) - if not user: - LOG.error("No such user: %(user)s ", {'user': user}) - raise exception.UserNotFound(uuid=user) - username, hostname = unquote_user_host(user_id) - access = models.User.access(context, instance_id, username, hostname) - view = views.UserAccessView(access.databases) - return wsgi.Result(view.data(), 200) - - def update(self, req, body, tenant_id, instance_id, user_id): - """Grant access for a user to one or more databases.""" - LOG.info("Granting user access for instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": req}) - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action( - context, 'user_access:update', instance_id) - context.notification = notification.DBaaSUserGrant( - context, request=req) - user_id = correct_id_with_req(user_id, req) - user = self._get_user(context, instance_id, user_id) - if not user: - LOG.error("No such user: %(user)s ", {'user': user}) - raise exception.UserNotFound(uuid=user) - username, hostname = unquote_user_host(user_id) - databases = [db['name'] for db in body['databases']] - with StartNotification(context, instance_id=instance_id, - username=username, database=databases): - models.User.grant(context, instance_id, username, hostname, - databases) - return wsgi.Result(None, 202) - - def delete(self, req, tenant_id, instance_id, user_id, id): - """Revoke access for a user.""" - LOG.info("Revoking user access for instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": req}) - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action( - context, 'user_access:delete', instance_id) - context.notification = notification.DBaaSUserRevoke( - context, request=req) - user_id = correct_id_with_req(user_id, req) - user = self._get_user(context, instance_id, user_id) - if not user: - LOG.error("No such user: %(user)s ", {'user': user}) - raise exception.UserNotFound(uuid=user) - username, hostname = unquote_user_host(user_id) - access = models.User.access(context, instance_id, username, hostname) - databases = [db.name for db in access.databases] - with StartNotification(context, instance_id=instance_id, - username=username, database=databases): - if id not in databases: - raise exception.DatabaseNotFound(uuid=id) - models.User.revoke(context, instance_id, username, hostname, id) - return wsgi.Result(None, 202) - - -class SchemaController(ExtensionController): - """Controller for instance functionality.""" - schemas = apischema.dbschema - - def index(self, req, tenant_id, instance_id): - """Return all schemas.""" - LOG.info("Listing schemas for instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": req}) - - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action( - context, 'database:index', instance_id) - schemas, next_marker = models.Schemas.load(context, instance_id) - view = views.SchemasView(schemas) - paged = pagination.SimplePaginatedDataView(req.url, 'databases', view, - next_marker) - return wsgi.Result(paged.data(), 200) - - def create(self, req, body, tenant_id, instance_id): - """Creates a set of schemas.""" - LOG.info("Creating schema for instance '%(id)s'\n" - "req : '%(req)s'\n\n" - "body: '%(body)s'\n'n", - {"id": instance_id, - "req": req, - "body": body}) - - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action( - context, 'database:create', instance_id) - schemas = body['databases'] - context.notification = notification.DBaaSDatabaseCreate(context, - request=req) - with StartNotification(context, instance_id=instance_id, - dbname=".".join([db['name'] - for db in schemas])): - try: - model_schemas = populate_validated_databases(schemas) - models.Schema.create(context, instance_id, model_schemas) - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("Database create error: %(e)s") - % {'e': e}) - return wsgi.Result(None, 202) - - def delete(self, req, tenant_id, instance_id, id): - LOG.info("Deleting schema for instance '%(id)s'\n" - "req : '%(req)s'\n\n", - {"id": instance_id, "req": req}) - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action( - context, 'database:delete', instance_id) - context.notification = notification.DBaaSDatabaseDelete( - context, request=req) - with StartNotification(context, instance_id=instance_id, dbname=id): - try: - schema = guest_models.MySQLSchema(name=id) - schema.check_delete() - if not models.Schemas.find(context, instance_id, id): - raise exception.DatabaseNotFound(uuid=id) - models.Schema.delete(context, instance_id, schema.serialize()) - except (ValueError, AttributeError) as e: - raise exception.BadRequest(_("Database delete error: %(e)s") - % {'e': e}) - return wsgi.Result(None, 202) - - def show(self, req, tenant_id, instance_id, id): - context = req.environ[wsgi.CONTEXT_KEY] - self.authorize_target_action( - context, 'database:show', instance_id) - raise webob.exc.HTTPNotImplemented() diff --git a/trove/extensions/mysql/views.py b/trove/extensions/mysql/views.py deleted file mode 100644 index 8e92bdde47..0000000000 --- a/trove/extensions/mysql/views.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# 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. - - -class UserView(object): - - def __init__(self, user): - self.user = user - - def data(self): - user_dict = { - "name": self.user.name, - "host": self.user.host, - "databases": self.user.databases - } - return {"user": user_dict} - - -class UsersView(object): - - def __init__(self, users): - self.users = users - - def data(self): - userlist = [{"name": user.name, - "host": user.host, - "databases": user.databases} - for user in self.users] - - return {"users": userlist} - - -class UserAccessView(object): - def __init__(self, databases): - self.databases = databases - - def data(self): - dbs = [{"name": db.name} for db in self.databases] - return {"databases": dbs} - - -class SchemaView(object): - - def __init__(self, schema): - self.schema = schema - - def data(self): - return {"name": self.schema.name} - - -class SchemasView(object): - - def __init__(self, schemas): - self.schemas = schemas - - def data(self): - data = [] - # These are model instances - for schema in self.schemas: - data.append(SchemaView(schema).data()) - - return {"databases": data} diff --git a/trove/extensions/routes/mysql.py b/trove/extensions/routes/mysql.py index a422681735..c91b17e36c 100644 --- a/trove/extensions/routes/mysql.py +++ b/trove/extensions/routes/mysql.py @@ -15,7 +15,6 @@ from trove.common import extensions from trove.extensions.common import service as common_service -from trove.extensions.mysql import service as mysql_service class Mysql(extensions.ExtensionDescriptor): @@ -40,14 +39,14 @@ class Mysql(extensions.ExtensionDescriptor): resource = extensions.ResourceExtension( 'databases', - mysql_service.SchemaController(), + common_service.SchemaController(), parent={'member_name': 'instance', 'collection_name': '{tenant_id}/instances'}) resources.append(resource) resource = extensions.ResourceExtension( 'users', - mysql_service.UserController(), + common_service.UserController(), parent={'member_name': 'instance', 'collection_name': '{tenant_id}/instances'}, member_actions={'update': 'PUT'}, @@ -57,7 +56,7 @@ class Mysql(extensions.ExtensionDescriptor): collection_url = '{tenant_id}/instances/:instance_id/users' resource = extensions.ResourceExtension( 'databases', - mysql_service.UserAccessController(), + common_service.UserAccessController(), parent={'member_name': 'user', 'collection_name': collection_url}, collection_actions={'update': 'PUT'}) diff --git a/trove/instance/service.py b/trove/instance/service.py index 1dd30b3289..93036737e1 100644 --- a/trove/instance/service.py +++ b/trove/instance/service.py @@ -34,8 +34,8 @@ from trove.common import policy from trove.common import utils from trove.common import wsgi from trove.datastore import models as ds_models -from trove.extensions.mysql.common import populate_users -from trove.extensions.mysql.common import populate_validated_databases +from trove.extensions.common.common import populate_users +from trove.extensions.common.common import populate_validated_databases from trove.instance import models, views from trove.module import models as module_models from trove.module import views as module_views diff --git a/trove/taskmanager/models.py b/trove/taskmanager/models.py index 5b91797867..80954bc4e9 100755 --- a/trove/taskmanager/models.py +++ b/trove/taskmanager/models.py @@ -57,7 +57,7 @@ from trove.common import timeutils from trove.common import utils from trove.common.utils import try_recover from trove.configuration import models as config_models -from trove.extensions.mysql import models as mysql_models +from trove.extensions.common import models as common_models from trove.instance import models as inst_models from trove.instance.models import DBInstance from trove.instance.models import FreshInstance @@ -789,7 +789,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): create_fmt_content, err) def report_root_enabled(self): - mysql_models.RootHistory.create(self.context, self.id) + common_models.RootHistory.create(self.context, self.id) def update_statuses_on_time_out(self): if CONF.update_status_on_fail: diff --git a/trove/tests/unittests/mysql/test_common.py b/trove/tests/unittests/mysql/test_common.py index 001bd8df46..7053e7077e 100644 --- a/trove/tests/unittests/mysql/test_common.py +++ b/trove/tests/unittests/mysql/test_common.py @@ -17,8 +17,8 @@ from testtools.matchers import Is from trove.common.exception import DatabaseForUserNotInDatabaseListError from trove.common.exception import DatabaseInitialDatabaseDuplicateError from trove.common.exception import DatabaseInitialUserDuplicateError -from trove.extensions.mysql.common import populate_users -from trove.extensions.mysql.common import populate_validated_databases +from trove.extensions.common.common import populate_users +from trove.extensions.common.common import populate_validated_databases from trove.tests.unittests import trove_testtools diff --git a/trove/tests/unittests/mysql/test_user_controller.py b/trove/tests/unittests/mysql/test_user_controller.py index 7350455d96..ccc7baf792 100644 --- a/trove/tests/unittests/mysql/test_user_controller.py +++ b/trove/tests/unittests/mysql/test_user_controller.py @@ -16,9 +16,9 @@ import jsonschema from testtools.matchers import Is -from trove.extensions.mysql.service import SchemaController -from trove.extensions.mysql.service import UserAccessController -from trove.extensions.mysql.service import UserController +from trove.extensions.common.service import SchemaController +from trove.extensions.common.service import UserAccessController +from trove.extensions.common.service import UserController from trove.tests.unittests import trove_testtools diff --git a/trove/tests/unittests/taskmanager/test_models.py b/trove/tests/unittests/taskmanager/test_models.py index 674389c400..5f29dbd3de 100644 --- a/trove/tests/unittests/taskmanager/test_models.py +++ b/trove/tests/unittests/taskmanager/test_models.py @@ -51,7 +51,6 @@ from trove.common import timeutils from trove.common import utils from trove.datastore import models as datastore_models from trove.extensions.common import models as common_models -from trove.extensions.mysql import models as mysql_models from trove.instance.models import BaseInstance from trove.instance.models import DBInstance from trove.instance.models import InstanceServiceStatus @@ -1211,7 +1210,7 @@ class RootReportTest(trove_testtools.TestCase): def test_report_root_first_time(self): context = Mock() context.user_id = utils.generate_uuid() - report = mysql_models.RootHistory.create( + report = common_models.RootHistory.create( context, utils.generate_uuid()) self.assertIsNotNone(report) @@ -1219,11 +1218,11 @@ class RootReportTest(trove_testtools.TestCase): context = Mock() context.user_id = utils.generate_uuid() id = utils.generate_uuid() - history = mysql_models.RootHistory(id, context.user_id).save() - with patch.object(mysql_models.RootHistory, 'load', + history = common_models.RootHistory(id, context.user_id).save() + with patch.object(common_models.RootHistory, 'load', Mock(return_value=history)): - report = mysql_models.RootHistory.create(context, id) - self.assertTrue(mysql_models.RootHistory.load.called) + report = common_models.RootHistory.create(context, id) + self.assertTrue(common_models.RootHistory.load.called) self.assertEqual(history.user, report.user) self.assertEqual(history.id, report.id)