# 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 os 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.i18n import _ from trove.common import instance as rd_instance from trove.common.notification import EndNotification from trove.guestagent import backup from trove.guestagent.common import operating_system from trove.guestagent.datastore import manager from trove.guestagent.datastore.mysql_common import service from trove.guestagent import guest_log from trove.guestagent import volume 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._mysql_app = mysql_app self._mysql_app_status = mysql_app_status self._mysql_admin = mysql_admin self.volume_do_not_start_on_reboot = False @property def mysql_app(self): return self._mysql_app @property def mysql_app_status(self): return self._mysql_app_status @property def mysql_admin(self): return self._mysql_admin @property def status(self): return self.mysql_app_status.get() @property def configuration_manager(self): return self.mysql_app( self.mysql_app_status.get()).configuration_manager @property def datastore_log_defs(self): owner = 'mysql' datastore_dir = self.mysql_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 change_passwords(self, context, users): with EndNotification(context): self.mysql_admin().change_passwords(users) def update_attributes(self, context, username, hostname, user_attrs): with EndNotification(context): self.mysql_admin().update_attributes( username, hostname, user_attrs) def reset_configuration(self, context, configuration): app = self.mysql_app(self.mysql_app_status.get()) app.reset_configuration(configuration) def create_database(self, context, databases): with EndNotification(context): return self.mysql_admin().create_database(databases) def create_user(self, context, users): with EndNotification(context): self.mysql_admin().create_user(users) def delete_database(self, context, database): with EndNotification(context): return self.mysql_admin().delete_database(database) def delete_user(self, context, user): with EndNotification(context): self.mysql_admin().delete_user(user) def get_user(self, context, username, hostname): return self.mysql_admin().get_user(username, hostname) def grant_access(self, context, username, hostname, databases): return self.mysql_admin().grant_access(username, hostname, databases) def revoke_access(self, context, username, hostname, database): return self.mysql_admin().revoke_access(username, hostname, database) def list_access(self, context, username, hostname): return self.mysql_admin().list_access(username, hostname) def list_databases(self, context, limit=None, marker=None, include_marker=False): return self.mysql_admin().list_databases(limit, marker, include_marker) def list_users(self, context, limit=None, marker=None, include_marker=False): return self.mysql_admin().list_users(limit, marker, include_marker) def enable_root(self, context): return self.mysql_admin().enable_root() def enable_root_with_password(self, context, root_password=None): return self.mysql_admin().enable_root(root_password) def is_root_enabled(self, context): return self.mysql_admin().is_root_enabled() def disable_root(self, context): return self.mysql_admin().disable_root() def _perform_restore(self, backup_info, context, restore_location, app): LOG.info(_("Restoring database from backup %s."), backup_info['id']) try: backup.restore(context, backup_info, restore_location) except Exception: LOG.exception(_("Error performing restore from backup %s."), backup_info['id']) app.status.set_status(rd_instance.ServiceStatuses.FAILED) raise LOG.info(_("Restored database successfully.")) def do_prepare(self, context, packages, databases, memory_mb, users, device_path, mount_point, backup_info, config_contents, root_password, overrides, cluster_config, snapshot): """This is called from prepare in the base class.""" app = self.mysql_app(self.mysql_app_status.get()) app.install_if_needed(packages) if device_path: # stop and do not update database app.stop_db( do_not_start_on_reboot=self.volume_do_not_start_on_reboot) device = volume.VolumeDevice(device_path) # unmount if device is already mounted device.unmount_device(device_path) device.format() if os.path.exists(mount_point): # rsync existing data to a "data" sub-directory # on the new volume device.migrate_data(mount_point, target_subdir="data") # mount the volume device.mount(mount_point) operating_system.chown(mount_point, service.MYSQL_OWNER, service.MYSQL_OWNER, recursive=False, as_root=True) LOG.debug("Mounted the volume at %s.", mount_point) # We need to temporarily update the default my.cnf so that # mysql will start after the volume is mounted. Later on it # will be changed based on the config template # (see MySqlApp.secure()) and restart. app.set_data_dir(mount_point + '/data') app.start_mysql() if backup_info: self._perform_restore(backup_info, context, mount_point + "/data", app) app.secure(config_contents) enable_root_on_restore = (backup_info and self.mysql_admin().is_root_enabled()) if enable_root_on_restore: app.secure_root(secure_remote_root=False) self.mysql_app_status.get().report_root(context, 'root') else: app.secure_root(secure_remote_root=True) if snapshot: self.attach_replica(context, snapshot, snapshot['config']) def pre_upgrade(self, context): app = self.mysql_app(self.mysql_app_status.get()) data_dir = app.get_data_dir() mount_point, _data = os.path.split(data_dir) save_dir = "%s/etc_mysql" % mount_point save_etc_dir = "%s/etc" % mount_point home_save = "%s/trove_user" % mount_point app.status.begin_restart() app.stop_db() if operating_system.exists("/etc/my.cnf", as_root=True): operating_system.create_directory(save_etc_dir, as_root=True) operating_system.copy("/etc/my.cnf", save_etc_dir, preserve=True, as_root=True) operating_system.copy("/etc/mysql/.", save_dir, preserve=True, as_root=True) operating_system.copy("%s/." % os.path.expanduser('~'), home_save, preserve=True, as_root=True) self.unmount_volume(context, mount_point=data_dir) return { 'mount_point': mount_point, 'save_dir': save_dir, 'save_etc_dir': save_etc_dir, 'home_save': home_save } def post_upgrade(self, context, upgrade_info): app = self.mysql_app(self.mysql_app_status.get()) app.stop_db() if 'device' in upgrade_info: self.mount_volume(context, mount_point=upgrade_info['mount_point'], device_path=upgrade_info['device'], write_to_fstab=True) if operating_system.exists(upgrade_info['save_etc_dir'], is_directory=True, as_root=True): operating_system.copy("%s/." % upgrade_info['save_etc_dir'], "/etc", preserve=True, as_root=True) operating_system.copy("%s/." % upgrade_info['save_dir'], "/etc/mysql", preserve=True, as_root=True) operating_system.copy("%s/." % upgrade_info['home_save'], os.path.expanduser('~'), preserve=True, as_root=True) self.configuration_manager.refresh_cache() app.start_mysql() def restart(self, context): app = self.mysql_app(self.mysql_app_status.get()) app.restart() def start_db_with_conf_changes(self, context, config_contents): app = self.mysql_app(self.mysql_app_status.get()) app.start_db_with_conf_changes(config_contents) def stop_db(self, context, do_not_start_on_reboot=False): app = self.mysql_app(self.mysql_app_status.get()) app.stop_db(do_not_start_on_reboot=do_not_start_on_reboot) def create_backup(self, context, backup_info): """ Entry point for initiating a backup for this guest agents db instance. The call currently blocks until the backup is complete or errors. If device_path is specified, it will be mounted based to a point specified in configuration. :param backup_info: a dictionary containing the db instance id of the backup task, location, type, and other data. """ with EndNotification(context): backup.backup(context, backup_info) def update_overrides(self, context, overrides, remove=False): app = self.mysql_app(self.mysql_app_status.get()) if remove: app.remove_overrides() app.update_overrides(overrides) def apply_overrides(self, context, overrides): LOG.debug("Applying overrides (%s).", overrides) app = self.mysql_app(self.mysql_app_status.get()) app.apply_overrides(overrides) def backup_required_for_replication(self, context): return self.replication.backup_required_for_replication() def get_replication_snapshot(self, context, snapshot_info, replica_source_config=None): LOG.debug("Getting replication snapshot.") app = self.mysql_app(self.mysql_app_status.get()) self.replication.enable_as_master(app, replica_source_config) snapshot_id, log_position = self.replication.snapshot_for_replication( context, app, None, snapshot_info) volume_stats = self.get_filesystem_stats(context, None) replication_snapshot = { 'dataset': { 'datastore_manager': self.manager, 'dataset_size': volume_stats.get('used', 0.0), 'volume_size': volume_stats.get('total', 0.0), 'snapshot_id': snapshot_id }, 'replication_strategy': self.replication_strategy, 'master': self.replication.get_master_ref(app, snapshot_info), 'log_position': log_position } return replication_snapshot def enable_as_master(self, context, replica_source_config): LOG.debug("Calling enable_as_master.") app = self.mysql_app(self.mysql_app_status.get()) self.replication.enable_as_master(app, replica_source_config) # DEPRECATED: Maintain for API Compatibility def get_txn_count(self, context): LOG.debug("Calling get_txn_count") return self.mysql_app(self.mysql_app_status.get()).get_txn_count() def get_last_txn(self, context): LOG.debug("Calling get_last_txn") return self.mysql_app(self.mysql_app_status.get()).get_last_txn() def get_latest_txn_id(self, context): LOG.debug("Calling get_latest_txn_id.") return self.mysql_app(self.mysql_app_status.get()).get_latest_txn_id() def wait_for_txn(self, context, txn): LOG.debug("Calling wait_for_txn.") self.mysql_app(self.mysql_app_status.get()).wait_for_txn(txn) def detach_replica(self, context, for_failover=False): LOG.debug("Detaching replica.") app = self.mysql_app(self.mysql_app_status.get()) replica_info = self.replication.detach_slave(app, for_failover) return replica_info def get_replica_context(self, context): LOG.debug("Getting replica context.") app = self.mysql_app(self.mysql_app_status.get()) replica_info = self.replication.get_replica_context(app) return replica_info 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): LOG.debug("Attaching replica.") app = self.mysql_app(self.mysql_app_status.get()) try: if 'replication_strategy' in replica_info: self._validate_slave_for_replication(context, replica_info) self.replication.enable_as_slave(app, replica_info, slave_config) except Exception: LOG.exception(_("Error enabling replication.")) app.status.set_status(rd_instance.ServiceStatuses.FAILED) raise def make_read_only(self, context, read_only): LOG.debug("Executing make_read_only(%s)", read_only) app = self.mysql_app(self.mysql_app_status.get()) app.make_read_only(read_only) def cleanup_source_on_replica_detach(self, context, replica_info): LOG.debug("Cleaning up the source on the detach of a replica.") self.replication.cleanup_source_on_replica_detach(self.mysql_admin(), replica_info) def demote_replication_master(self, context): LOG.debug("Demoting replication master.") app = self.mysql_app(self.mysql_app_status.get()) self.replication.demote_master(app)