# Copyright 2013 OpenStack Foundation # Copyright 2013 Rackspace Hosting # Copyright 2013 Hewlett-Packard Development Company, L.P. # 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. # import tempfile from oslo_log import log as logging from trove.common import cfg from trove.common import configurations from trove.common import exception from trove.common import utils from trove.common.notification import EndNotification from trove.guestagent import guest_log from trove.guestagent.common import operating_system from trove.guestagent.datastore import manager from trove.guestagent.utils import docker as docker_util from trove.guestagent.utils import mysql as mysql_util from trove.instance import service_status LOG = logging.getLogger(__name__) CONF = cfg.CONF class MySqlManager(manager.Manager): def __init__(self, mysql_app, mysql_app_status, mysql_admin, manager_name='mysql'): super(MySqlManager, self).__init__(manager_name) self.app = mysql_app self.status = mysql_app_status self.adm = mysql_admin self.volume_do_not_start_on_reboot = False @property def configuration_manager(self): return self.app.configuration_manager def get_service_status(self): try: with mysql_util.SqlClient(self.app.get_engine()) as client: cmd = "SELECT 1;" client.execute(cmd) LOG.debug("Database service check: database query is responsive") return service_status.ServiceStatuses.HEALTHY except Exception: return super(MySqlManager, self).get_service_status() def get_start_db_params(self, data_dir): return f'--datadir={data_dir}' def do_prepare(self, context, packages, databases, memory_mb, users, device_path, mount_point, backup_info, config_contents, root_password, overrides, cluster_config, snapshot, ds_version=None): """This is called from prepare in the base class.""" data_dir = mount_point + '/data' self.app.stop_db() operating_system.ensure_directory(data_dir, user=CONF.database_service_uid, group=CONF.database_service_uid, as_root=True) # This makes sure the include dir is created. self.app.set_data_dir(data_dir) # Prepare mysql configuration LOG.info('Preparing database configuration') self.app.configuration_manager.reset_configuration(config_contents) self.app.update_overrides(overrides) # Restore data from backup and reset root password if backup_info: self.perform_restore(context, data_dir, backup_info) self.reset_password_for_restore(ds_version=ds_version, data_dir=data_dir) # Start database service. command = self.get_start_db_params(data_dir) self.app.start_db(ds_version=ds_version, command=command) self.app.secure() enable_remote_root = (backup_info and self.adm.is_root_enabled()) if enable_remote_root: self.status.report_root(context) else: self.app.secure_root() if snapshot: # This instance is a replication slave self.attach_replica(context, snapshot, snapshot['config']) def create_backup(self, context, backup_info): """Create backup for the database. :param context: User context object. :param backup_info: a dictionary containing the db instance id of the backup task, location, type, and other data. """ LOG.info(f"Creating backup {backup_info['id']}") with EndNotification(context): # Set /var/run/mysqld to allow localhost access. volumes_mapping = { '/var/lib/mysql': {'bind': '/var/lib/mysql', 'mode': 'rw'}, "/var/run/mysqld": {"bind": "/var/run/mysqld", "mode": "ro"}, '/tmp': {'bind': '/tmp', 'mode': 'rw'} } self.app.create_backup(context, backup_info, volumes_mapping=volumes_mapping, need_dbuser=True) def get_datastore_log_defs(self): owner = cfg.get_configuration_property('database_service_uid') datastore_dir = self.app.get_data_dir() server_section = configurations.MySQLConfParser.SERVER_CONF_SECTION long_query_time = CONF.get(self.manager).get( 'guest_log_long_query_time') / 1000 general_log_file = self.build_log_file_name( self.GUEST_LOG_DEFS_GENERAL_LABEL, owner, datastore_dir=datastore_dir) error_log_file = self.validate_log_file('/var/log/mysqld.log', owner) slow_query_log_file = self.build_log_file_name( self.GUEST_LOG_DEFS_SLOW_QUERY_LABEL, owner, datastore_dir=datastore_dir) return { self.GUEST_LOG_DEFS_GENERAL_LABEL: { self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.USER, self.GUEST_LOG_USER_LABEL: owner, self.GUEST_LOG_FILE_LABEL: general_log_file, self.GUEST_LOG_SECTION_LABEL: server_section, self.GUEST_LOG_ENABLE_LABEL: { 'general_log': 'on', 'general_log_file': general_log_file, 'log_output': 'file', }, self.GUEST_LOG_DISABLE_LABEL: { 'general_log': 'off', }, }, self.GUEST_LOG_DEFS_SLOW_QUERY_LABEL: { self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.USER, self.GUEST_LOG_USER_LABEL: owner, self.GUEST_LOG_FILE_LABEL: slow_query_log_file, self.GUEST_LOG_SECTION_LABEL: server_section, self.GUEST_LOG_ENABLE_LABEL: { 'slow_query_log': 'on', 'slow_query_log_file': slow_query_log_file, 'long_query_time': long_query_time, }, self.GUEST_LOG_DISABLE_LABEL: { 'slow_query_log': 'off', }, }, self.GUEST_LOG_DEFS_ERROR_LABEL: { self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.SYS, self.GUEST_LOG_USER_LABEL: owner, self.GUEST_LOG_FILE_LABEL: error_log_file, }, } def is_log_enabled(self, logname): if logname == self.GUEST_LOG_DEFS_GENERAL_LABEL: value = self.configuration_manager.get_value( 'general_log', section='mysqld', default='off') LOG.debug(f"The config value of general_log is {value}") return value == 'on' elif logname == self.GUEST_LOG_DEFS_SLOW_QUERY_LABEL: value = self.configuration_manager.get_value( 'slow_query_log', section='mysqld', default='off') LOG.debug(f"The config value of slow_query_log is {value}") return value == 'on' return False def apply_overrides(self, context, overrides): LOG.info("Applying database config.") self.app.apply_overrides(overrides) LOG.info("Finished applying database config.") def reset_password_for_restore(self, ds_version=None, data_dir='/var/lib/mysql/data'): """Reset the root password after restore the db data. We create a temporary database container by running mysqld_safe to reset the root password. """ LOG.info('Starting to reset password for restore') try: root_pass = self.app.get_auth_password(file="root.cnf") except exception.UnprocessableEntity: root_pass = utils.generate_random_password() self.app.save_password('root', root_pass) init_file = tempfile.NamedTemporaryFile(mode='w') operating_system.write_file( init_file.name, f"ALTER USER 'root'@'localhost' IDENTIFIED BY '{root_pass}';" ) err_file = tempfile.NamedTemporaryFile(suffix='.err') # Get the original file owner and group before changing the owner. from pathlib import Path init_file_path = Path(init_file.name) init_file_owner = init_file_path.owner() init_file_group = init_file_path.group() # Allow database service user to access the temporary files. try: for file in [init_file.name, err_file.name]: operating_system.chown(file, CONF.database_service_uid, CONF.database_service_uid, force=True, as_root=True) except Exception as err: LOG.error('Failed to change file owner, error: %s', str(err)) for file in [init_file.name, err_file.name]: LOG.debug('Reverting the %s owner to %s ' 'before close it.', file, init_file_owner) operating_system.chown(file, init_file_owner, init_file_group, force=True, as_root=True) init_file.close() err_file.close() raise err # Allow database service user to access the temporary files. command = ( f'mysqld --init-file={init_file.name} ' f'--log-error={err_file.name} ' f'--datadir={data_dir} ' ) extra_volumes = { init_file.name: {"bind": init_file.name, "mode": "rw"}, err_file.name: {"bind": err_file.name, "mode": "rw"}, } # Start the database container process. try: self.app.start_db(ds_version=ds_version, command=command, extra_volumes=extra_volumes) except Exception as err: LOG.error('Failed to reset password for restore, error: %s', str(err)) raise err # re-raised at the end of the finally clause finally: try: LOG.debug( 'The init container log: %s', docker_util.get_container_logs(self.app.docker_client) ) docker_util.remove_container(self.app.docker_client) except Exception as err: LOG.error('Failed to remove container. error: %s', str(err)) pass for file in [init_file.name, err_file.name]: LOG.debug('Reverting the %s owner to %s ' 'before close it.', file, init_file_owner) operating_system.chown(file, init_file_owner, init_file_group, force=True, as_root=True) init_file.close() err_file.close() LOG.info('Finished to reset password for restore') def _validate_slave_for_replication(self, context, replica_info): if replica_info['replication_strategy'] != self.replication_strategy: raise exception.IncompatibleReplicationStrategy( replica_info.update({ 'guest_strategy': self.replication_strategy })) volume_stats = self.get_filesystem_stats(context, None) if (volume_stats.get('total', 0.0) < replica_info['dataset']['dataset_size']): raise exception.InsufficientSpaceForReplica( replica_info.update({ 'slave_volume_size': volume_stats.get('total', 0.0) })) def attach_replica(self, context, replica_info, slave_config, **kwargs): LOG.info("Attaching replica, replica_info: %s", replica_info) try: if 'replication_strategy' in replica_info: self._validate_slave_for_replication(context, replica_info) self.replication.enable_as_slave(self.app, replica_info, slave_config) except Exception as err: LOG.error("Error enabling replication, error: %s", str(err)) self.status.set_status(service_status.ServiceStatuses.FAILED) raise def make_read_only(self, context, read_only): LOG.info("Executing make_read_only(%s)", read_only) self.app.make_read_only(read_only) def get_latest_txn_id(self, context): LOG.info("Calling get_latest_txn_id.") return self.app.get_latest_txn_id() def get_last_txn(self, context): LOG.info("Calling get_last_txn") return self.app.get_last_txn() def wait_for_txn(self, context, txn): LOG.info("Calling wait_for_txn.") self.app.wait_for_txn(txn) def upgrade(self, context, upgrade_info): """Upgrade the database.""" LOG.info('Starting to upgrade database, upgrade_info: %s', upgrade_info) self.app.upgrade(upgrade_info) def rebuild(self, context, ds_version, config_contents=None, config_overrides=None): """Restore datastore service after instance rebuild.""" LOG.info("Starting to restore database service") self.status.begin_install() mount_point = CONF.get(CONF.datastore_manager).mount_point data_dir = mount_point + '/data' operating_system.ensure_directory(data_dir, user=CONF.database_service_uid, group=CONF.database_service_uid, as_root=True) # This makes sure the include dir is created. self.app.set_data_dir(data_dir) try: # Prepare mysql configuration LOG.debug('Preparing database configuration') self.app.configuration_manager.reset_configuration(config_contents) self.app.update_overrides(config_overrides) # Start database service. command = self.get_start_db_params(data_dir) self.app.start_db(ds_version=ds_version, command=command) except Exception as e: LOG.error(f"Failed to restore database service after rebuild, " f"error: {str(e)}") self.prepare_error = True raise finally: self.status.end_install(error_occurred=self.prepare_error)