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
This commit is contained in:
parent
0138ce30e2
commit
89274df2af
1
.gitignore
vendored
1
.gitignore
vendored
@ -56,6 +56,7 @@ publish-docs/
|
||||
*~
|
||||
.*.swp
|
||||
.bak
|
||||
.idea/
|
||||
|
||||
# Config sample and policy sample
|
||||
etc/trove/*.sample
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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:
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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()
|
@ -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}
|
@ -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'})
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user