Merge "Working on missing postgresql features"

This commit is contained in:
Zuul 2024-12-25 13:52:41 +00:00 committed by Gerrit Code Review
commit d09de01fdf
20 changed files with 707 additions and 755 deletions

1
.gitignore vendored
View File

@ -56,6 +56,7 @@ publish-docs/
*~
.*.swp
.bak
.idea/
# Config sample and policy sample
etc/trove/*.sample

View File

@ -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.

View File

@ -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(

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)