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:
parent
c05fc3fbe0
commit
f77aab57e1
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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" %
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue