Support Optional Super User in Instance Create

The creation of initial users and/or databases is supported in an
instance-create request, but the creation of a super/root user is
not. Granted, it's possible to create or reset the root user, but it
must be done as a separate request, after the instance has completely
initialized.

Changeset: If CONF.root_on_create == True, a UUID is generated for
the root user's password and is returned in the instance-create
response as the "password" property/field.

Change-Id: I300d3f019766445b90411d5ab8651b1aa34a77e9
Implements: blueprint instance-initial-super-user
Closes-Bug: #1219624
This commit is contained in:
amcrn 2013-09-09 19:42:46 -07:00
parent c05fc3fbe0
commit f77aab57e1
16 changed files with 153 additions and 27 deletions

View File

@ -75,6 +75,8 @@ taskmanager_queue = taskmanager
# Auth
admin_roles = admin
root_on_create = False
# Users to ignore for user create/list/delete operations
ignore_users = os_admin, root
ignore_dbs = lost+found, mysql, information_schema

View File

@ -136,6 +136,7 @@ if __name__ == "__main__":
from trove.tests.api import instances_resize
from trove.tests.api import databases
from trove.tests.api import root
from trove.tests.api import root_on_create
from trove.tests.api import users
from trove.tests.api import user_access
from trove.tests.api.mgmt import accounts

View File

@ -118,6 +118,11 @@ common_opts = [
cfg.IntOpt('dns_time_out', default=60 * 2),
cfg.IntOpt('resize_time_out', default=60 * 10),
cfg.IntOpt('revert_time_out', default=60 * 10),
cfg.BoolOpt('root_on_create', default=False,
help='Enable the automatic creation of the root user for the '
' service during instance-create. The generated password for '
' the root user is immediately returned in the response of '
" instance-create as the 'password' field."),
cfg.ListOpt('root_grant', default=['ALL']),
cfg.BoolOpt('root_grant_option', default=True),
cfg.IntOpt('http_get_rate', default=200),

View File

@ -214,7 +214,7 @@ class API(proxy.RpcProxy):
def prepare(self, memory_mb, databases, users,
device_path='/dev/vdb', mount_point='/mnt/volume',
backup_id=None, config_contents=None):
backup_id=None, config_contents=None, root_password=None):
"""Make an asynchronous call to prepare the guest
as a database container optionally includes a backup id for restores
"""
@ -222,7 +222,8 @@ class API(proxy.RpcProxy):
self._cast_with_consumer(
"prepare", databases=databases, memory_mb=memory_mb,
users=users, device_path=device_path, mount_point=mount_point,
backup_id=backup_id, config_contents=config_contents)
backup_id=backup_id, config_contents=config_contents,
root_password=root_password)
def restart(self):
"""Restart the MySQL server."""

View File

@ -85,7 +85,8 @@ class Manager(periodic_task.PeriodicTasks):
LOG.info(_("Restored database successfully"))
def prepare(self, context, databases, memory_mb, users, device_path=None,
mount_point=None, backup_id=None, config_contents=None):
mount_point=None, backup_id=None, config_contents=None,
root_password=None):
"""Makes ready DBAAS on a Guest container."""
MySqlAppStatus.get().begin_mysql_install()
# status end_mysql_install set with secure()
@ -114,9 +115,16 @@ class Manager(periodic_task.PeriodicTasks):
LOG.info(_("Securing mysql now."))
app.secure(config_contents)
enable_root_on_restore = (backup_id and MySqlAdmin().is_root_enabled())
if enable_root_on_restore:
if root_password and not backup_id:
app.secure_root(secure_remote_root=True)
MySqlAdmin().enable_root(root_password)
MySqlAdmin().report_root_enabled(context)
app.secure_root(secure_remote_root=not enable_root_on_restore)
elif enable_root_on_restore:
app.secure_root(secure_remote_root=False)
MySqlAdmin().report_root_enabled(context)
else:
app.secure_root(secure_remote_root=True)
app.complete_install_or_restart()
if databases:

View File

@ -453,9 +453,9 @@ class MySqlAdmin(object):
"""Return True if root access is enabled; False otherwise."""
return MySqlRootAccess.is_root_enabled()
def enable_root(self):
def enable_root(self, root_password=None):
"""Enable the root user global access and/or reset the root password"""
return MySqlRootAccess.enable_root()
return MySqlRootAccess.enable_root(root_password)
def report_root_enabled(self, context=None):
"""Records in the Root History that the root is enabled"""
@ -889,12 +889,12 @@ class MySqlRootAccess(object):
return result.rowcount != 0
@classmethod
def enable_root(cls):
def enable_root(cls, root_password=None):
"""Enable the root user global access and/or reset the root password"""
user = models.RootUser()
user.name = "root"
user.host = "%"
user.password = generate_random_password()
user.password = root_password or generate_random_password()
with LocalSqlClient(get_engine()) as client:
print(client)
try:

View File

@ -36,6 +36,7 @@ from trove.instance.tasks import InstanceTask
from trove.instance.tasks import InstanceTasks
from trove.taskmanager import api as task_api
from trove.openstack.common import log as logging
from trove.openstack.common import uuidutils
from trove.openstack.common.gettextutils import _
@ -115,10 +116,11 @@ class SimpleInstance(object):
"""
def __init__(self, context, db_info, service_status):
def __init__(self, context, db_info, service_status, root_password=None):
self.context = context
self.db_info = db_info
self.service_status = service_status
self.root_pass = root_password
@property
def addresses(self):
@ -228,6 +230,10 @@ class SimpleInstance(object):
def service_type(self):
return self.db_info.service_type
@property
def root_password(self):
return self.root_pass
class DetailInstance(SimpleInstance):
"""A detailed view of an Instnace.
@ -488,13 +494,19 @@ class Instance(BuiltInstance):
)
security_groups = [security_group["name"]]
root_password = None
if CONF.root_on_create and not backup_id:
root_password = uuidutils.generate_uuid()
task_api.API(context).create_instance(db_info.id, name, flavor,
image_id, databases, users,
service_type, volume_size,
security_groups, backup_id,
availability_zone)
availability_zone,
root_password)
return SimpleInstance(context, db_info, service_status)
return SimpleInstance(context, db_info, service_status,
root_password)
return run_with_quotas(context.tenant,
deltas,

View File

@ -93,8 +93,8 @@ class InstanceDetailView(InstanceView):
if ip is not None and len(ip) > 0:
result['instance']['ip'] = ip
if isinstance(self.instance, models.DetailInstance) and \
self.instance.volume_used:
if (isinstance(self.instance, models.DetailInstance) and
self.instance.volume_used):
used = self.instance.volume_used
if CONF.trove_volume_support:
result['instance']['volume']['used'] = used
@ -102,6 +102,9 @@ class InstanceDetailView(InstanceView):
# either ephemeral or root partition
result['instance']['local_storage'] = {'used': used}
if self.instance.root_password:
result['instance']['password'] = self.instance.root_password
return result

View File

@ -108,11 +108,12 @@ class API(ManagerAPI):
def create_instance(self, instance_id, name, flavor,
image_id, databases, users, service_type,
volume_size, security_groups, backup_id=None,
availability_zone=None):
availability_zone=None, root_password=None):
LOG.debug("Making async call to create instance %s " % instance_id)
self._cast("create_instance", instance_id=instance_id, name=name,
flavor=self._transform_obj(flavor), image_id=image_id,
databases=databases, users=users,
service_type=service_type, volume_size=volume_size,
security_groups=security_groups, backup_id=backup_id,
availability_zone=availability_zone)
availability_zone=availability_zone,
root_password=root_password)

View File

@ -83,12 +83,13 @@ class Manager(periodic_task.PeriodicTasks):
def create_instance(self, context, instance_id, name, flavor,
image_id, databases, users, service_type,
volume_size, security_groups, backup_id,
availability_zone):
availability_zone, root_password):
instance_tasks = FreshInstanceTasks.load(context, instance_id)
instance_tasks.create_instance(flavor, image_id,
databases, users, service_type,
volume_size, security_groups,
backup_id, availability_zone)
backup_id, availability_zone,
root_password)
if CONF.exists_notification_transformer:
@periodic_task.periodic_task(

View File

@ -135,7 +135,7 @@ class ConfigurationMixin(object):
class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
def create_instance(self, flavor, image_id, databases, users,
service_type, volume_size, security_groups,
backup_id, availability_zone):
backup_id, availability_zone, root_password):
if use_heat:
server, volume_info = self._create_server_volume_heat(
flavor,
@ -173,7 +173,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
if server:
self._guest_prepare(server, flavor['ram'], volume_info,
databases, users, backup_id,
config.config_contents)
config.config_contents, root_password)
if not self.db_info.task_status.is_error:
self.update_db(task_status=inst_models.InstanceTasks.NONE)
@ -416,14 +416,15 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
def _guest_prepare(self, server, flavor_ram, volume_info,
databases, users, backup_id=None,
config_contents=None):
config_contents=None, root_password=None):
LOG.info("Entering guest_prepare.")
# Now wait for the response from the create to do additional work
self.guest.prepare(flavor_ram, databases, users,
device_path=volume_info['device_path'],
mount_point=volume_info['mount_point'],
backup_id=backup_id,
config_contents=config_contents)
config_contents=config_contents,
root_password=root_password)
def _create_dns_entry(self):
LOG.debug("%s: Creating dns entry for instance: %s" %

View File

@ -113,6 +113,7 @@ class InstanceTestInfo(object):
instance_info = InstanceTestInfo()
dbaas = None # Rich client used throughout this test.
dbaas_admin = None # Same as above, with admin privs.
ROOT_ON_CREATE = CONFIG.get('root_on_create', False)
VOLUME_SUPPORT = CONFIG.get('trove_volume_support', False)
EPHEMERAL_SUPPORT = not VOLUME_SUPPORT and CONFIG.get('device_path',
'/dev/vdb') is not None
@ -390,6 +391,8 @@ class CreateInstance(object):
# Check these attrs only are returned in create response
expected_attrs = ['created', 'flavor', 'addresses', 'id', 'links',
'name', 'status', 'updated']
if ROOT_ON_CREATE:
expected_attrs.append('password')
if VOLUME_SUPPORT:
expected_attrs.append('volume')
if CONFIG.trove_dns_support:

View File

@ -0,0 +1,86 @@
# Copyright 2013 OpenStack Foundation
#
# 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 trove.common import cfg
from proboscis import before_class
from proboscis import after_class
from proboscis import test
from proboscis.asserts import assert_equal
from proboscis.asserts import assert_not_equal
from proboscis.asserts import assert_true
from trove import tests
from trove.tests.api.users import TestUsers
from trove.tests.api.instances import instance_info
from trove.tests import util
from trove.tests.api.databases import TestMysqlAccess
CONF = cfg.CONF
GROUP = "dbaas.api.root.oncreate"
@test(depends_on_classes=[TestMysqlAccess],
runs_after=[TestUsers],
groups=[tests.DBAAS_API, GROUP, tests.INSTANCES])
class TestRootOnCreate(object):
"""
Test 'CONF.root_on_create', which if True, creates the root user upon
database instance initialization.
"""
root_enabled_timestamp = 'Never'
@before_class
def setUp(self):
self.orig_conf_value = CONF.root_on_create
CONF.root_on_create = True
self.dbaas = util.create_dbaas_client(instance_info.user)
self.dbaas_admin = util.create_dbaas_client(instance_info.admin_user)
self.history = self.dbaas_admin.management.root_enabled_history
self.enabled = self.dbaas.root.is_root_enabled
@after_class
def tearDown(self):
CONF.root_on_create = self.orig_conf_value
@test
def test_root_on_create(self):
"""Test that root is enabled after instance creation"""
enabled = self.enabled(instance_info.id).rootEnabled
assert_equal(200, self.dbaas.last_http_code)
assert_true(enabled)
@test(depends_on=[test_root_on_create])
def test_history_after_root_on_create(self):
"""Test that the timestamp in the root enabled history is set"""
self.root_enabled_timestamp = self.history(instance_info.id).enabled
assert_equal(200, self.dbaas.last_http_code)
assert_not_equal(self.root_enabled_timestamp, 'Never')
@test(depends_on=[test_history_after_root_on_create])
def test_reset_root(self):
"""Test that root reset does not alter the timestamp"""
orig_timestamp = self.root_enabled_timestamp
self.dbaas.root.create(instance_info.id)
assert_equal(200, self.dbaas.last_http_code)
self.root_enabled_timestamp = self.history(instance_info.id).enabled
assert_equal(200, self.dbaas.last_http_code)
assert_equal(orig_timestamp, self.root_enabled_timestamp)
@test(depends_on=[test_reset_root])
def test_root_still_enabled(self):
"""Test that after root was reset, it's still enabled."""
enabled = self.enabled(instance_info.id).rootEnabled
assert_equal(200, self.dbaas.last_http_code)
assert_true(enabled)

View File

@ -86,6 +86,7 @@ class TestConfig(object):
"trove_volume_support": True,
"trove_max_volumes_per_user": 100,
"usage_endpoint": USAGE_ENDPOINT,
"root_on_create": False
}
self._frozen_values = FrozenDict(self._values)
self._users = None

View File

@ -208,7 +208,8 @@ class FakeGuest(object):
return self.users.get((username, hostname), None)
def prepare(self, memory_mb, databases, users, device_path=None,
mount_point=None, backup_id=None, config_contents=None):
mount_point=None, backup_id=None, config_contents=None,
root_password=None):
from trove.instance.models import DBInstance
from trove.instance.models import InstanceServiceStatus
from trove.instance.models import ServiceStatuses

View File

@ -254,12 +254,12 @@ class ApiTest(testtools.TestCase):
when(mock_conn).create_consumer(any(), any(), any()).thenReturn(None)
exp_msg = RpcMsgMatcher('prepare', 'memory_mb', 'databases', 'users',
'device_path', 'mount_point', 'backup_id',
'config_contents')
'config_contents', 'root_password')
when(rpc).cast(any(), any(), exp_msg).thenReturn(None)
self.api.prepare('2048', 'db1', 'user1', '/dev/vdt', '/mnt/opt',
'bkup-1232', 'cont')
'bkup-1232', 'cont', '1-2-3-4')
self._verify_rpc_connection_and_cast(rpc, mock_conn, exp_msg)
@ -269,11 +269,11 @@ class ApiTest(testtools.TestCase):
when(mock_conn).create_consumer(any(), any(), any()).thenReturn(None)
exp_msg = RpcMsgMatcher('prepare', 'memory_mb', 'databases', 'users',
'device_path', 'mount_point', 'backup_id',
'config_contents')
'config_contents', 'root_password')
when(rpc).cast(any(), any(), exp_msg).thenReturn(None)
self.api.prepare('2048', 'db1', 'user1', '/dev/vdt', '/mnt/opt',
'backup_id_123', 'cont')
'backup_id_123', 'cont', '1-2-3-4')
self._verify_rpc_connection_and_cast(rpc, mock_conn, exp_msg)