def test_get_server(self): """Tests the get_server method. """ ssl_dict = {} connect = False # get_server from connection information given as dictionary new_server = server.get_server(self.server_cnx["conn_info"], ssl_dict, connect) self.assertIsInstance(new_server, server.Server) # get_server from a Server instance new_server = server.get_server(self.server, ssl_dict, connect=True) self.assertIsInstance(new_server, server.Server) self.assertIsNone(server.get_server(None, ssl_dict, connect=False)) with self.assertRaises(GadgetCnxFormatError): server.get_server("localhost:3396", ssl_dict, connect=False) with self.assertRaises(GadgetCnxInfoError): server.get_server(3396, ssl_dict, connect=False)
def test_get_server(self): """Tests the get_server method. """ ssl_dict = {} connect = False # get_server from connection information given as dictionary new_server = server.get_server(self.server_cnx["conn_info"], ssl_dict, connect) self.assertIsInstance(new_server, server.Server) # get_server from a Server instance new_server = server.get_server(self.server, ssl_dict, connect=True) self.assertIsInstance(new_server, server.Server) self.assertIsNone(server.get_server(None, ssl_dict, connect=False)) with self.assertRaises(GadgetCnxFormatError): server.get_server("localhost:3396", ssl_dict, connect=False) with self.assertRaises(GadgetCnxInfoError): server.get_server(3396, ssl_dict, connect=False)
def leave(server_info, **kwargs): """Removes a server from a Group Replication group. :param server_info: Connection information :type server_info: dict | Server | str :param kwargs: Keyword arguments: dry_run: Do not make changes to the server option_file: The path to a options file to check configuration. skip_backup: if True, skip the creation of a backup file when modifying the options file. :type kwargs: dict :raise GadgetError: If the given server is not a member of a Group Replication :return: True if the server stop_gr_plugin command was executed otherwise False. :rtype: boolean """ # get the server instance server = get_server(server_info=server_info) dry_run = kwargs.get("dry_run", False) option_file = kwargs.get("option_file", None) skip_backup = kwargs.get("skip_backup", False) if server is None: raise GadgetError(_ERROR_NO_SERVER) msg = _RUNNING_COMMAND.format(LEAVE, server) _LOGGER.info("") _LOGGER.log(STEP_LOG_LEVEL_VALUE, msg) try: # verify server status (is in a group?) if is_member_of_group(server) and is_active_member(server): _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Attempting to leave from the " "Group Replication group...") if not dry_run: stop_gr_plugin(server) _LOGGER.info("Server state: %s", get_member_state(server)) _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Server %s has left the group.", server) # Update the Group Replication options on defaults file. # Note: Set group_replication_start_on_boot=ON if option_file is not None and option_file != "": persist_gr_config(option_file, None, set_on=False, dry_run=dry_run, skip_backup=skip_backup) return True # server is not active elif is_member_of_group(server): _LOGGER.warning("The server %s is not actively replicating.", server) _LOGGER.info("Server state: %s", get_member_state(server)) _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Server %s is " "not active in the group.", server) # Update the group_replication_start_on_boot option on defaults # file. # Note: Set group_replication_start_on_boot=OFF if option_file is not None and option_file != "": persist_gr_config(option_file, None, set_on=False, dry_run=dry_run, skip_backup=skip_backup) # the server has not been configured for GR else: raise GadgetError(_ERROR_NOT_A_MEMBER.format(server)) if dry_run: _LOGGER.warning(_WARN_DRY_RUN_USED) finally: # disconnect servers. if server is not None: server.disconnect() return False
def join(server_info, peer_server_info, **kwargs): """Add a server to an existing Group Replication group. The contact point to add the new server to the group replication is the specified peer server, which must be already a member of the group. :param server_info: Connection information :type server_info: dict | Server | str :param peer_server_info: Connection information of a server member of a Group Replication group. :type peer_server_info: dict | Server | str :param kwargs: Keyword arguments: gr_address: The host:port that the gcs plugin uses to other servers communicate with this server. dry_run: Do not make changes to the server verbose: If the command should show more verbose information. option_file: The path to a options file to check configuration. skip_backup: if True, skip the creation of a backup file when modifying the options file. ssl_mode: SSL mode to be used with group replication. (Note server GR SSL modes need to be consistent with the SSL GR modes on the peer-server otherwise an error will be thrown). skip_rpl_user: If True, skip the creation of the replication user. :type kwargs: dict :raise GadgetError: If server_info or peer_server_info is None. If the given peer_server_info is from a server that is not a member of Group Replication. :raise GadgetCnxInfoError: If the connection information on server_info or peer_server_info could not be parsed. :raise GadgetServerError: If a connection fails. :return: True if the start_gr_plugin method was executed otherwise False. :rtype: boolean """ verbose = kwargs.get("verbose", False) dry_run = kwargs.get("dry_run", False) gr_host = kwargs.get("gr_host", None) option_file = kwargs.get("option_file", None) skip_backup = kwargs.get("skip_backup", False) # Default is value for ssl_mode is REQUIRED ssl_mode = kwargs.get("ssl_mode", GR_SSL_REQUIRED) skip_rpl_user = kwargs.get("skip_rpl_user", False) # Connect to the server server = get_server(server_info=server_info) if server is None: raise GadgetError(_ERROR_NO_SERVER) msg = _RUNNING_COMMAND.format(JOIN, server) _LOGGER.info("") _LOGGER.log(STEP_LOG_LEVEL_VALUE, msg) _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Checking Group Replication " "prerequisites.") peer_server = get_server(server_info=peer_server_info) # Connect to the peer server if peer_server is None: if server is not None: server.disconnect() raise GadgetError("No peer server provided. It is required to get " "information from the group.") try: # verify the peer server belong to a GR group. if not is_member_of_group(peer_server): raise GadgetError("Peer server '{0}' is not a member of a GR " "group.".format(peer_server)) # verify the server status is ONLINE peer_server_state = get_member_state(peer_server) if peer_server_state != 'ONLINE': raise GadgetError("Cannot join instance {0}. Peer instance {1} " "state is currently '{2}', but is expected to " "be 'ONLINE'.".format(server, peer_server, peer_server_state)) # Throw an error in case server doesn't support SSL and the ssl_mode # option was provided a value other than DISABLED if server.select_variable(HAVE_SSL) != 'YES' and \ ssl_mode != GR_SSL_DISABLED: raise GadgetError(_ERROR_NO_HAVE_SSL.format(server)) # Throw an error in case there is any SSL incompatibilities are found # on the peer server or if the peer-server is incompatible with the # value of the ssl_mode option. check_peer_ssl_compatibility(peer_server, ssl_mode) if not skip_rpl_user: rpl_user_dict = get_rpl_usr(kwargs) req_dict = get_req_dict(server, rpl_user_dict["replication_user"], peer_server, option_file=option_file) else: # if replication user is to be skipped, no need to add the # replication user to the requirements list req_dict = get_req_dict(server, None, peer_server, option_file=option_file) rpl_user_dict = None check_server_requirements(server, req_dict, rpl_user_dict, verbose, dry_run, skip_backup=skip_backup) # verify the group replication is installed and not disabled. check_gr_plugin_is_installed(server, option_file, dry_run) # Initialize log error access and get current position in it error_log_size = None if server.is_alias("127.0.0.1"): error_log = LocalErrorLog(server) try: error_log_size = error_log.get_size() except Exception as err: # pylint: disable=W0703 _LOGGER.warning( "Unable to access the server error log: %s", str(err)) else: _LOGGER.warning("Not running locally on the server and can not " "access its error log.") # verify the server does not belong already to a GR group. if is_active_member(server): health(server, **kwargs) raise GadgetError(_ERROR_ALREADY_A_MEMBER.format(server, JOIN)) gr_host, local_port = resolve_gr_local_address(gr_host, server.host, server.port) local_address = "{0}:{1}".format(gr_host, local_port) _LOGGER.debug("local_address to use: %s", local_address) # Get local_address from the peer server to add to the list of # group_seeds. peer_local_address = get_gr_local_address_from(peer_server) option_parser = req_dict.get(OPTION_PARSER, None) gr_config_vars = get_gr_config_vars(local_address, kwargs, option_parser, peer_local_address) # Do several replication user related tasks if the # skip-replication-user option was not provided if not skip_rpl_user: # The replication user for be check/create on the peer server. # NOTE: rpl_user_dict["host"] has the FQDN resolved from the host # provided by the user replication_user = "******".format( rpl_user_dict["recovery_user"], rpl_user_dict["host"]) rpl_user_dict["replication_user"] = replication_user # Check the given replication user exists on peer req_dict_user = get_req_dict_user_check(peer_server, replication_user) # Check and create the given replication user on peer server. # NOTE: No other checks will be performed, only the replication # user. check_server_requirements(peer_server, req_dict_user, rpl_user_dict, verbose, dry_run, skip_schema_checks=True) # IF the group name is not set, try to acquire it from a peer server. if gr_config_vars[GR_GROUP_NAME] is None: _LOGGER.debug("Trying to retrieve group replication name from " "peer server.") group_name = get_gr_name_from_peer(peer_server) _LOGGER.debug("Retrieved group replication name from peer" " server: %s.", group_name) gr_config_vars[GR_GROUP_NAME] = group_name # Set the single_primary mode according to the value set on peer # server if gr_config_vars[GR_SINGLE_PRIMARY_MODE] is None: gr_config_vars[GR_SINGLE_PRIMARY_MODE] = \ get_gr_variable_from_peer(peer_server, GR_SINGLE_PRIMARY_MODE) if gr_config_vars[GR_GROUP_NAME] is None: raise GadgetError( _ERROR_UNABLE_TO_GET.format("Group Replication group name", peer_server)) _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Joining Group Replication group: %s", gr_config_vars[GR_GROUP_NAME]) if gr_config_vars[GR_GROUP_SEEDS] is None: raise GadgetError( _ERROR_UNABLE_TO_GET.format("peer addresses", peer_server)) # Remove IP whitelist variable if not set (by user or from the option # file) to use the default server value and not set it with None. if gr_config_vars[GR_IP_WHITELIST] is None: gr_config_vars.pop(GR_IP_WHITELIST) setup_gr_config(server, gr_config_vars, dry_run=dry_run) if not skip_rpl_user: # if the skip replication user option was not specified, # run the change master to store MySQL replication user name or # password information in the master info repository do_change_master(server, rpl_user_dict, dry_run=dry_run) _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Attempting to join to Group Replication group...") try: start_gr_plugin(server, dry_run) _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Server %s joined " "Group Replication group %s.", server, gr_config_vars[GR_GROUP_NAME]) except: _LOGGER.error("\nGroup Replication join failed.") if error_log_size is not None: log_data = error_log.read(error_log_size) if log_data: _LOGGER.error("Group Replication plugin failed to start. " "Server error log contains the following " "errors: \n %s", log_data) raise # Update the Group Replication options on defaults file. # Note: Set group_replication_start_on_boot=ON if OPTION_PARSER in req_dict.keys(): persist_gr_config(req_dict[OPTION_PARSER], gr_config_vars, dry_run=dry_run, skip_backup=skip_backup) if dry_run: _LOGGER.warning(_WARN_DRY_RUN_USED) return False finally: # disconnect servers. if server is not None: server.disconnect() if peer_server is not None: peer_server.disconnect() return True
def health(server_info, **kwargs): """Display Group Replication health/status information of a server. :param server_info: Connection information :type server_info: dict | Server | str :param kwargs: Keyword arguments: verbose: If the command should show more verbose information. detailed: Show extra health info for the server. :type kwargs: dict :raise GadgetError: If server_info is None or belongs to a server that is not a member of Group Replication. :raise GadgetCnxInfoError: If the connection information on server_info could not be parsed. :raise GadgetServerError: If a connection fails. """ verbose = kwargs.get("verbose", False) detailed = kwargs.get("detailed", False) query_options = { "columns": True, } server = get_server(server_info=server_info) if server is None: raise GadgetError(_ERROR_NO_SERVER) _LOGGER.info("") msg = _RUNNING_COMMAND.format((STATUS if detailed else HEALTH), server) _LOGGER.log(STEP_LOG_LEVEL_VALUE, msg) try: # SELECT privilege is required for check for membership if not is_member_of_group(server): raise GadgetError("The server '{0}' is not a member of a GR " "group.".format(server)) # Retrieve members information columns = [MEMBER_ID, MEMBER_HOST, MEMBER_PORT, MEMBER_STATE] res = server.exec_query("SELECT {0} FROM {1}" "".format(", ".join(columns), REP_GROUP_MEMBERS_TABLE), query_options) # If no info could be retrieve raise and error if len(res[1]) == 0: _LOGGER.debug("Cannot retrieve any value from table %s", REP_GROUP_MEMBERS_TABLE) if server is not None: server.disconnect() raise GadgetError(_ERROR_NOT_A_MEMBER.format(server)) total_members = 0 online_members = 0 # Log members information _LOGGER.info("Group Replication members: ") for results_set in res[1]: member_dict = OrderedDict(zip(res[0], results_set)) if detailed: member_dict["ID"] = member_dict.pop(MEMBER_ID) else: member_dict.pop(MEMBER_ID) member_dict["HOST"] = member_dict.pop(MEMBER_HOST) member_dict["PORT"] = member_dict.pop(MEMBER_PORT) member_dict["STATE"] = member_dict.pop(MEMBER_STATE) total_members += 1 if member_dict["STATE"] == "ONLINE": online_members += 1 _report_dict(member_dict, "{0:<4}{1}: {2}", "ID" if detailed else "HOST", "{0:<2}- {1}: {2}", show_empty_values=verbose) _LOGGER.info("") # The detailed information is logged as the full HEALTH command. if not verbose and not detailed: return # Retrieve the (replication channels) applier and retriever threads # status columns = ["m.{0}".format(MEMBER_ID), MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, VIEW_ID, COUNT_TRANSACTIONS_IN_QUEUE, COUNT_TRANSACTIONS_CHECKED, COUNT_CONFLICTS_DETECTED, COUNT_TRANSACTIONS_ROWS_VALIDATING, TRANSACTIONS_COMMITTED_ALL_MEMBERS, LAST_CONFLICT_FREE_TRANSACTION] res = server.exec_query("SELECT {0} FROM {1} as m JOIN {2} as s " "on m.MEMBER_ID = s.MEMBER_ID" "".format(", ".join(columns), REP_GROUP_MEMBERS_TABLE, REP_MEMBER_STATS_TABLE), query_options) # Log the member information of the server that is owner of the # following stats, current status of applier and retriever threads output = "Server stats: " _LOGGER.info(output) res_pairs = OrderedDict(zip(res[0], res[1][0])) # Log member information output = "{0:<2}{1}".format("", "Member") _LOGGER.info(output) member_dict = OrderedDict() member_dict["HOST"] = res_pairs.pop(MEMBER_HOST) member_dict["PORT"] = res_pairs.pop(MEMBER_PORT) member_dict["STATE"] = res_pairs.pop(MEMBER_STATE) member_dict[VIEW_ID] = res_pairs.pop(VIEW_ID) member_dict["ID"] = res_pairs.pop(MEMBER_ID) _report_dict(member_dict, "{0:<4}{1}: {2}", show_empty_values=verbose) # Log Transactions stats output = "{0:<2}{1}".format("", "Transactions") _LOGGER.info(output) trans_dict = OrderedDict() trans_dict["IN_QUEUE"] = res_pairs.pop(COUNT_TRANSACTIONS_IN_QUEUE) trans_dict["CHECKED"] = res_pairs.pop(COUNT_TRANSACTIONS_CHECKED) trans_dict["CONFLICTS_DETECTED"] = \ res_pairs.pop(COUNT_CONFLICTS_DETECTED) trans_dict["VALIDATING"] = res_pairs.pop( COUNT_TRANSACTIONS_ROWS_VALIDATING) trans_dict["LAST_CONFLICT_FREE"] = res_pairs.pop( LAST_CONFLICT_FREE_TRANSACTION) trans_dict["COMMITTED_ALL_MEMBERS"] = res_pairs.pop( TRANSACTIONS_COMMITTED_ALL_MEMBERS) _report_dict(trans_dict, "{0:<4}{1}: {2}", show_empty_values=verbose) _report_dict(res_pairs, "{0:<2}{1}: {2}", show_empty_values=verbose) res = server.exec_query( "SELECT * FROM {0} as c " "JOIN {1} as a ON c.channel_name = a.channel_name " "ORDER BY c.channel_name, a.worker_id" "".format(REP_CONN_STATUS_TABLE, REP_APPLIER_STATUS_BY_WORKER), query_options) # Log connection stats of the channels output = "{0:<2}{1}".format("", "Connection status") _LOGGER.info(output) for results_set in res[1]: res_pairs = OrderedDict(zip(res[0], results_set)) errors_dict = OrderedDict() errors_dict["NUMBER"] = res_pairs.pop("LAST_ERROR_NUMBER") errors_dict["MESSAGE"] = res_pairs.pop("LAST_ERROR_MESSAGE") errors_dict["TIMESTAMP"] = res_pairs.pop("LAST_ERROR_TIMESTAMP") _report_dict(res_pairs, "{0:<4}{1}: {2}", "CHANNEL_NAME", "{0:<2}- {1}: {2}", show_empty_values=verbose) if "0000-00-00" not in errors_dict["TIMESTAMP"] or verbose: output = "{0:<4}{1}".format("", "Last error") _LOGGER.info(output) _report_dict(errors_dict, "{0:<6}{1}: {2}", show_empty_values=verbose) else: output = "{0:<4}{1}".format("", "Last error: None") _LOGGER.info(output) _LOGGER.info("") if online_members < 3: plural = "s" if online_members > 1 else "" output = ("The group is currently not HA because it has {0} " "ONLINE member{1}.".format(online_members, plural)) _LOGGER.warning(output) _LOGGER.info("") finally: # Disconnect the server prior to end method invocation. if server is not None: server.disconnect() return
def start(server_info, **kwargs): """Start a group replication group with the given server information. :param server_info: Connection information :type server_info: dict | Server | str :param kwargs: Keyword arguments: gr_address: The host:port that the gcs plugin uses to other servers communicate with this server. dry_run: Do not make changes to the server verbose: If the command should show more verbose information. option_file: The path to a options file to check configuration. skip_backup: if True, skip the creation of a backup file when modifying the options file. skip_schema_checks: Skip schema validation. ssl_mode: SSL mode to be used with group replication. :type kwargs: dict :raise GadgetError: If server_info is None. :raise GadgetCnxInfoError: If the connection information on server_info could not be parsed. :raise GadgetServerError: If a connection fails. :return: True if the start_gr_plugin method was executed otherwise False. :rtype: boolean """ server = get_server(server_info=server_info) if server is None: raise GadgetError(_ERROR_NO_SERVER) msg = _RUNNING_COMMAND.format(START, server) _LOGGER.info("") _LOGGER.log(STEP_LOG_LEVEL_VALUE, msg) verbose = kwargs.get("verbose", False) dry_run = kwargs.get("dry_run", False) gr_host = kwargs.get("gr_address", None) skip_schema_checks = kwargs.get("skip_schema_checks", []) option_file = kwargs.get("option_file", None) skip_backup = kwargs.get("skip_backup", False) _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Checking Group Replication " "prerequisites.") try: # Throw an error in case server doesn't support SSL and the ssl_mode # option was provided a value other than DISABLED if server.select_variable(HAVE_SSL) != 'YES' and \ kwargs["ssl_mode"] != GR_SSL_DISABLED: raise GadgetError(_ERROR_NO_HAVE_SSL.format(server)) rpl_user_dict = get_rpl_usr(kwargs) req_dict = get_req_dict(server, rpl_user_dict["replication_user"], option_file=option_file) check_server_requirements(server, req_dict, rpl_user_dict, verbose, dry_run, skip_schema_checks, skip_backup=skip_backup) gr_host, local_port = resolve_gr_local_address(gr_host, server.host, server.port) # verify the group replication is not Disabled in the server. check_gr_plugin_is_installed(server, option_file, dry_run) # verify the server does not belong already to a GR group. if is_active_member(server): health(server, **kwargs) raise GadgetError(_ERROR_ALREADY_A_MEMBER.format(server, START)) local_address = "{0}:{1}".format(gr_host, local_port) option_parser = req_dict.get(OPTION_PARSER, None) gr_config_vars = get_gr_config_vars(local_address, kwargs, option_parser) if gr_config_vars[GR_GROUP_SEEDS] is None: gr_config_vars.pop(GR_GROUP_SEEDS) # Remove IP whitelist variable if not set (by user or from the option # file) to use the default server value and not set it with None. if gr_config_vars[GR_IP_WHITELIST] is None: gr_config_vars.pop(GR_IP_WHITELIST) if gr_config_vars[GR_GROUP_NAME] is None: new_uuid = get_group_uuid_name(server) _LOGGER.debug("A new UUID has been generated for the group " "replication name %s", new_uuid) gr_config_vars[GR_GROUP_NAME] = new_uuid # Set the single_primary mode, if no value given then set ON as # default. if gr_config_vars[GR_SINGLE_PRIMARY_MODE] is None: gr_config_vars[GR_SINGLE_PRIMARY_MODE] = '"ON"' _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Group Replication group name: %s", gr_config_vars[GR_GROUP_NAME]) setup_gr_config(server, gr_config_vars, dry_run=dry_run) # Run the change master to store MySQL replication user name or # password information in the master info repository do_change_master(server, rpl_user_dict, dry_run=dry_run) set_bootstrap(server, dry_run) _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Attempting to start the Group Replication group...") # Attempt to start the Group Replication plugin start_gr_plugin(server, dry_run) # Wait for the super_read_only to be unset. super_read_only = server.select_variable("super_read_only", 'global') _LOGGER.debug("super_read_only: %s", super_read_only) waiting_time = 0 informed = False while int(super_read_only) and waiting_time < TIME_OUT: time.sleep(WAIT_SECONDS) waiting_time += WAIT_SECONDS _LOGGER.debug("have been waiting %s seconds", waiting_time) # inform what are we waiting for if waiting_time >= 10 and not informed: _LOGGER.info("Waiting for super_read_only to be unset.") informed = True super_read_only = server.select_variable("super_read_only", 'global') _LOGGER.debug("super_read_only: %s", super_read_only) if int(super_read_only): raise GadgetError("Timeout waiting for super_read_only to be " "unset after call to start Group Replication " "plugin.") _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Group Replication started for group: %s.", gr_config_vars[GR_GROUP_NAME]) unset_bootstrap(server, dry_run) # Update the Group Replication options on defaults file. # Note: Set group_replication_start_on_boot=ON if OPTION_PARSER in req_dict.keys(): persist_gr_config(req_dict[OPTION_PARSER], gr_config_vars, dry_run=dry_run, skip_backup=skip_backup) if dry_run: _LOGGER.warning(_WARN_DRY_RUN_USED) return False finally: # Disconnect the server prior to end method invocation. if server is not None: server.disconnect() return True
def check(**kwargs): """Check and update instance configurations relative to Group Replication. :param kwargs: Keyword arguments: update: Make changes to the options file. verbose: If the command should show more verbose information. option_file: The path to a options file to check configuration. skip_backup: if True, skip the creation of a backup file when modifying the options file. server: Connection information (dict | Server | str) :type kwargs: dict :raise GadgetError: If the file cannot be updated. :return: True if the options file was modified. :rtype: boolean """ _LOGGER.info("") verbose = kwargs.get("verbose", False) update = kwargs.get("update", False) option_file = kwargs.get("option_file", None) skip_backup = kwargs.get("skip_backup", False) server_info = kwargs.get("server", None) # Get the server instance server = get_server(server_info=server_info) # This method requires at least one of the server or the option file. if (option_file is None or option_file == "") and server is None: raise GadgetError("Either the server or the defaults file was not " "given.") if option_file: msg = "Running {0} command for file '{1}'.".format(CHECK, option_file) else: msg = "Running {0} command.".format(CHECK) _LOGGER.log(STEP_LOG_LEVEL_VALUE, msg) try: # if server already belongs to a group and the update option # was provided, dump its GR configurations to the option file if is_member_of_group(server) and is_active_member(server): gr_configs = get_gr_configs_from_instance(server) if update: # the variable comes from the server, no need to test for # loose prefix variant. if not gr_configs.get("group_replication_group_seeds", None): _LOGGER.warning( "The 'group_replication_group_seeds' is not defined " "on %s. This option is mandatory to allow the server " "to automatically rejoin the cluster after reboot. " "Please manually update its value on option file " "'%s'.", str(server), option_file) _LOGGER.info("Updating option file '%s' with Group " "Replication settings from " "%s", option_file, str(server)) persist_gr_config(option_file, gr_configs) result = True else: result = False # If the server doesn't belong to any group, check if it meets GR # requirements, printing to stdout what needs to be changed and # update the configuration file if provided else: # The dictionary with the requirements to be verified. if server is not None: req_dict = get_req_dict(server, None, peer_server=None, option_file=option_file) skip_schema_checks = False else: req_dict = get_req_dict_for_opt_file(option_file) skip_schema_checks = True _LOGGER.log(STEP_LOG_LEVEL_VALUE, "Checking Group Replication " "prerequisites.") # set dry_run to avoid changes on server as replication user # creation. result = check_server_requirements( server, req_dict, None, verbose=verbose, dry_run=True, skip_schema_checks=skip_schema_checks, update=update, skip_backup=skip_backup) # verify the group replication is installed and not disabled. if server is not None: check_gr_plugin_is_installed(server, option_file, dry_run=(not update)) finally: # Disconnect the server prior to end method invocation. if server is not None: server.disconnect() return result
def join(server_info, peer_server_info, **kwargs): """Add a server to an existing Group Replication group. The contact point to add the new server to the group replication is the specified peer server, which must be already a member of the group. :param server_info: Connection information :type server_info: dict | Server | str :param peer_server_info: Connection information of a server member of a Group Replication group. :type peer_server_info: dict | Server | str :param kwargs: Keyword arguments: gr_address: The host:port that the gcs plugin uses to other servers communicate with this server. dry_run: Do not make changes to the server verbose: If the command should show more verbose information. option_file: The path to a options file to check configuration. skip_backup: if True, skip the creation of a backup file when modifying the options file. ssl_mode: SSL mode to be used with group replication. (Note server GR SSL modes need to be consistent with the SSL GR modes on the peer-server otherwise an error will be thrown). failover_consistency: Group Replication failover Consistency, must be a string containing either "BEFORE_ON_PRIMARY_FAILOVER", "EVENTUAL", "0" or "1". expel_timeout: Group Replication member expel Timeout. Must must be an integer value containing the time in seconds to wait before ghe killer node expels members suspected of having failed from the group. skip_rpl_user: If True, skip the creation of the replication user. target_is_local: Target is running in the same host :type kwargs: dict :raise GadgetError: If server_info or peer_server_info is None. If the given peer_server_info is from a server that is not a member of Group Replication. :raise GadgetCnxInfoError: If the connection information on server_info or peer_server_info could not be parsed. :raise GadgetServerError: If a connection fails. :return: True if the start_gr_plugin method was executed otherwise False. :rtype: boolean """ verbose = kwargs.get("verbose", False) dry_run = kwargs.get("dry_run", False) gr_host = kwargs.get("gr_address", None) option_file = kwargs.get("option_file", None) skip_backup = kwargs.get("skip_backup", False) # Default is value for ssl_mode is REQUIRED ssl_mode = kwargs.get("ssl_mode", GR_SSL_REQUIRED) skip_rpl_user = kwargs.get("skip_rpl_user", False) target_is_local = kwargs.get("target_is_local", False) exit_state_action = kwargs.get("exit_state_action", None) member_weight = kwargs.get("member_weight", None) failover_consistency = kwargs.get("failover_consistency", None) expel_timeout = kwargs.get("expel_timeout", None) # Connect to the server server = get_server(server_info=server_info) if server is None: raise GadgetError(_ERROR_NO_SERVER) msg = _RUNNING_COMMAND.format(JOIN, server) _LOGGER.info("") _LOGGER.step(msg) _LOGGER.step("Checking Group Replication " "prerequisites.") peer_server = get_server(server_info=peer_server_info) # Connect to the peer server if peer_server is None: if server is not None: server.disconnect() raise GadgetError("No peer server provided. It is required to get " "information from the group.") try: # verify the peer server belong to a GR group. if not is_member_of_group(peer_server): raise GadgetError("Peer server '{0}' is not a member of a GR " "group.".format(peer_server)) # verify the server status is ONLINE peer_server_state = get_member_state(peer_server) if peer_server_state != 'ONLINE': raise GadgetError("Cannot join instance {0}. Peer instance {1} " "state is currently '{2}', but is expected to " "be 'ONLINE'.".format(server, peer_server, peer_server_state)) # Throw an error in case server doesn't support SSL and the ssl_mode # option was provided a value other than DISABLED if server.select_variable(HAVE_SSL) != 'YES' and \ ssl_mode != GR_SSL_DISABLED: raise GadgetError(_ERROR_NO_HAVE_SSL.format(server)) # Throw an error in case there is any SSL incompatibilities are found # on the peer server or if the peer-server is incompatible with the # value of the ssl_mode option. check_peer_ssl_compatibility(peer_server, ssl_mode) if not skip_rpl_user: rpl_user_dict = get_rpl_usr(kwargs) else: rpl_user_dict = None # Do not check/create the replication user in the instance to add, # in order to avoid errors if it is in read-only-mode. # (GR automatically enables super-read-only when stopping the # plugin, starting with version 8.0.2) req_dict = get_req_dict(server, None, peer_server, option_file=option_file) check_server_requirements(server, req_dict, rpl_user_dict, verbose, dry_run, skip_backup=skip_backup, var_change_warning=True) # verify the group replication is installed and not disabled. check_gr_plugin_is_installed(server, option_file, dry_run) # attempt to set the group_replication_exit_state_action in order to # let GR do the value validation and catch any error right away if exit_state_action is not None: validate_exit_state_action(server, exit_state_action, dry_run) # attempt to set the group_replication_member_weight in order to # let GR do the value validation and catch any error right away if member_weight is not None: validate_member_weight(server, member_weight, dry_run) # attempt to set the group_replication_consistency in order to # let GR do the value validation and catch any error right away if failover_consistency is not None: validate_failover_consistency(server, failover_consistency, dry_run) # attempt to set the group_replication_member_expel_timeout in order to # let GR do the value validation and catch any error right away if expel_timeout is not None: validate_expel_timeout(server, expel_timeout, dry_run) # Initialize log error access and get current position in it error_log_size = None # is_alias(127.0.0.1) != is_alias(gethostname()), but they should # match. also is_alias() can't be made to work nicely with recording if target_is_local: # server.is_alias("127.0.0.1"): try: error_log = LocalErrorLog(server) error_log_size = error_log.get_size() except Exception as err: # pylint: disable=W0703 _LOGGER.warning( "Unable to access the server error log: %s", str(err)) else: _LOGGER.warning("Not running locally on the server and can not " "access its error log.") # verify the server does not belong already to a GR group. if is_active_member(server): health(server, **kwargs) raise GadgetError(_ERROR_ALREADY_A_MEMBER.format(server, JOIN)) gr_host, local_port = resolve_gr_local_address(gr_host, server.host, server.port) local_address = "{0}:{1}".format(gr_host, local_port) _LOGGER.debug("local_address to use: %s", local_address) # Get local_address from the peer server to add to the list of # group_seeds. peer_local_address = get_gr_local_address_from(peer_server) if peer_server.select_variable( "group_replication_single_primary_mode") in ('1', 'ON'): kwargs["single_primary"] = "ON" else: kwargs["single_primary"] = "OFF" option_parser = req_dict.get(OPTION_PARSER, None) gr_config_vars = get_gr_config_vars(local_address, kwargs, option_parser, peer_local_address, server_id=server.select_variable("server_id")) # The following code has been commented because the logic to create # the replication-user has been moved to the Shell c++ code. # The code wasn't removed to serve as knowledge base for the MP # refactoring to C++ # Do several replication user related tasks if the # skip-replication-user option was not provided #if not skip_rpl_user: # The replication user for be check/create on the peer server. # NOTE: rpl_user_dict["host"] has the FQDN resolved from the host # provided by the user # replication_user = "******".format( # rpl_user_dict["recovery_user"], rpl_user_dict["host"]) # rpl_user_dict["replication_user"] = replication_user # Check the given replication user exists on peer # req_dict_user = get_req_dict_user_check(peer_server, # replication_user) # Check and create the given replication user on peer server. # NOTE: No other checks will be performed, only the replication # user. # check_server_requirements(peer_server, req_dict_user, # rpl_user_dict, verbose, dry_run, # skip_schema_checks=True) # IF the group name is not set, try to acquire it from a peer server. if gr_config_vars[GR_GROUP_NAME] is None: _LOGGER.debug("Trying to retrieve group replication name from " "peer server.") group_name = get_gr_name_from_peer(peer_server) _LOGGER.debug("Retrieved group replication name from peer" " server: %s.", group_name) gr_config_vars[GR_GROUP_NAME] = group_name # Set the single_primary mode according to the value set on peer # server if gr_config_vars[GR_SINGLE_PRIMARY_MODE] is None: gr_config_vars[GR_SINGLE_PRIMARY_MODE] = \ get_gr_variable_from_peer(peer_server, GR_SINGLE_PRIMARY_MODE) if gr_config_vars[GR_GROUP_NAME] is None: raise GadgetError( _ERROR_UNABLE_TO_GET.format("Group Replication group name", peer_server)) _LOGGER.step( "Joining Group Replication group: %s", gr_config_vars[GR_GROUP_NAME]) if gr_config_vars[GR_GROUP_SEEDS] is None: raise GadgetError( _ERROR_UNABLE_TO_GET.format("peer addresses", peer_server)) # Remove IP whitelist variable if not set (by user or from the option # file) to use the default server value and not set it with None. if gr_config_vars[GR_IP_WHITELIST] is None: gr_config_vars.pop(GR_IP_WHITELIST) if gr_config_vars[GR_EXIT_STATE_ACTION] is None: gr_config_vars.pop(GR_EXIT_STATE_ACTION) if gr_config_vars[GR_MEMBER_WEIGHT] is None: gr_config_vars.pop(GR_MEMBER_WEIGHT) if gr_config_vars[GR_FAILOVER_CONSISTENCY] is None: gr_config_vars.pop(GR_FAILOVER_CONSISTENCY) if gr_config_vars[GR_EXPEL_TIMEOUT] is None: gr_config_vars.pop(GR_EXPEL_TIMEOUT) gr_config_vars[GR_START_ON_BOOT] = "ON" setup_gr_config(server, gr_config_vars, dry_run=dry_run) if not skip_rpl_user: # if the skip replication user option was not specified, # run the change master to store MySQL replication user name or # password information in the master info repository do_change_master(server, rpl_user_dict, dry_run=dry_run) _LOGGER.step( "Attempting to join to Group Replication group...") try: start_gr_plugin(server, dry_run) _LOGGER.step("Server %s joined " "Group Replication group %s.", server, gr_config_vars[GR_GROUP_NAME]) except: _LOGGER.error("\nGroup Replication join failed.") if error_log_size is not None: log_data = error_log.read(error_log_size) if log_data: _LOGGER.error("Group Replication plugin failed to start. " "Server error log contains the following " "errors: \n %s", log_data) raise # Update the Group Replication options on defaults file. # Note: Set group_replication_start_on_boot=ON if OPTION_PARSER in req_dict.keys(): persist_gr_config(req_dict[OPTION_PARSER], gr_config_vars, dry_run=dry_run, skip_backup=skip_backup) if dry_run: _LOGGER.warning(_WARN_DRY_RUN_USED) return False finally: # disconnect servers. if server is not None: server.disconnect() if peer_server is not None: peer_server.disconnect() return True
def start(server_info, **kwargs): """Start a group replication group with the given server information. :param server_info: Connection information :type server_info: dict | Server | str :param kwargs: Keyword arguments: gr_address: The host:port that the gcs plugin uses to other servers communicate with this server. dry_run: Do not make changes to the server verbose: If the command should show more verbose information. option_file: The path to a options file to check configuration. skip_backup: if True, skip the creation of a backup file when modifying the options file. skip_schema_checks: Skip schema validation. ssl_mode: SSL mode to be used with group replication. exit_state_action: Group Replication Exit State Action, must be a string containing either "ABORT_SERVER", "READ_ONLY", "0" or "1". The string is case-insensitive. member_weight: Group Replication Member Weight, must be an integer value, with a percentage weight for automatic primary election on failover. failover_consistency: Group Replication failover Consistency, must be a string containing either "BEFORE_ON_PRIMARY_FAILOVER", "EVENTUAL", "0" or "1". The string is case-insensitive. expel_timeout: Group Replication member expel Timeout. Must must be an integer value containing the time in seconds to wait before ghe killer node expels members suspected of having failed from the group. :type kwargs: dict :raise GadgetError: If server_info is None. :raise GadgetCnxInfoError: If the connection information on server_info could not be parsed. :raise GadgetServerError: If a connection fails. :return: True if the start_gr_plugin method was executed otherwise False. :rtype: boolean """ server = get_server(server_info=server_info) if server is None: raise GadgetError(_ERROR_NO_SERVER) msg = _RUNNING_COMMAND.format(START, server) _LOGGER.info("") _LOGGER.step(msg) verbose = kwargs.get("verbose", False) dry_run = kwargs.get("dry_run", False) gr_host = kwargs.get("gr_address", None) skip_schema_checks = kwargs.get("skip_schema_checks", []) option_file = kwargs.get("option_file", None) skip_backup = kwargs.get("skip_backup", False) skip_rpl_user = kwargs.get("skip_rpl_user", False) exit_state_action = kwargs.get("exit_state_action", None) member_weight = kwargs.get("member_weight", None) failover_consistency = kwargs.get("failover_consistency", None) expel_timeout = kwargs.get("expel_timeout", None) _LOGGER.step("Checking Group Replication prerequisites.") try: # Throw an error in case server doesn't support SSL and the ssl_mode # option was provided a value other than DISABLED if server.select_variable(HAVE_SSL) != 'YES' and \ kwargs["ssl_mode"] != GR_SSL_DISABLED: raise GadgetError(_ERROR_NO_HAVE_SSL.format(server)) # Skip replication user checks if requested. rpl_user_dict = None if not skip_rpl_user: rpl_user_dict = get_rpl_usr(kwargs) # Do not check the replication user, already handled by the AdminAPI. req_dict = get_req_dict(server, None, option_file=option_file) check_server_requirements(server, req_dict, rpl_user_dict, verbose, dry_run, skip_schema_checks, skip_backup=skip_backup, var_change_warning=True) gr_host, local_port = resolve_gr_local_address(gr_host, server.host, server.port) # verify the group replication is not Disabled in the server. check_gr_plugin_is_installed(server, option_file, dry_run) # attempt to set the group_replication_exit_state_action in order to # let GR do the value validation and catch any error right away if exit_state_action is not None: validate_exit_state_action(server, exit_state_action, dry_run) # attempt to set the group_replication_member_weight in order to # let GR do the value validation and catch any error right away if member_weight is not None: validate_member_weight(server, member_weight, dry_run) # attempt to set the group_replication_consistency in order to # let GR do the value validation and catch any error right away if failover_consistency is not None: validate_failover_consistency(server, failover_consistency, dry_run) # attempt to set the group_replication_member_expel_timeout in order to # let GR do the value validation and catch any error right away if expel_timeout is not None: validate_expel_timeout(server, expel_timeout, dry_run) # verify the server does not belong already to a GR group. if is_active_member(server): health(server, **kwargs) raise GadgetError(_ERROR_ALREADY_A_MEMBER.format(server, START)) local_address = "{0}:{1}".format(gr_host, local_port) option_parser = req_dict.get(OPTION_PARSER, None) gr_config_vars = get_gr_config_vars(local_address, kwargs, option_parser, server_id=server.select_variable("server_id")) if gr_config_vars[GR_GROUP_SEEDS] is None: gr_config_vars.pop(GR_GROUP_SEEDS) # Remove IP whitelist variable if not set (by user or from the option # file) to use the default server value and not set it with None. if gr_config_vars[GR_IP_WHITELIST] is None: gr_config_vars.pop(GR_IP_WHITELIST) if gr_config_vars[GR_EXIT_STATE_ACTION] is None: gr_config_vars.pop(GR_EXIT_STATE_ACTION) if gr_config_vars[GR_MEMBER_WEIGHT] is None: gr_config_vars.pop(GR_MEMBER_WEIGHT) if gr_config_vars[GR_FAILOVER_CONSISTENCY] is None: gr_config_vars.pop(GR_FAILOVER_CONSISTENCY) if gr_config_vars[GR_EXPEL_TIMEOUT] is None: gr_config_vars.pop(GR_EXPEL_TIMEOUT) if gr_config_vars[GR_GROUP_NAME] is None: new_uuid = get_group_uuid_name(server) _LOGGER.debug("A new UUID has been generated for the group " "replication name %s", new_uuid) gr_config_vars[GR_GROUP_NAME] = new_uuid # Set the single_primary mode, if no value given then set ON as # default. if gr_config_vars[GR_SINGLE_PRIMARY_MODE] is None: gr_config_vars[GR_SINGLE_PRIMARY_MODE] = '"ON"' _LOGGER.step("Group Replication group name: %s", gr_config_vars[GR_GROUP_NAME]) gr_config_vars[GR_START_ON_BOOT] = "ON" setup_gr_config(server, gr_config_vars, dry_run=dry_run) # Run the change master to store MySQL replication user name or # password information in the master info repository, but only if # replication user is NOT skipped. if not skip_rpl_user: do_change_master(server, rpl_user_dict, dry_run=dry_run) set_bootstrap(server, dry_run) _LOGGER.step("Attempting to start the Group Replication group...") # Attempt to start the Group Replication plugin start_gr_plugin(server, dry_run) # Wait for the super_read_only to be unset. super_read_only = server.select_variable("super_read_only", 'global') _LOGGER.debug("super_read_only: %s", super_read_only) waiting_time = 0 informed = False while int(super_read_only) and waiting_time < TIME_OUT: time.sleep(WAIT_SECONDS) waiting_time += WAIT_SECONDS _LOGGER.debug("have been waiting %s seconds", waiting_time) # inform what are we waiting for if waiting_time >= 10 and not informed: _LOGGER.info("Waiting for super_read_only to be unset.") informed = True super_read_only = server.select_variable("super_read_only", 'global') _LOGGER.debug("super_read_only: %s", super_read_only) if int(super_read_only): raise GadgetError("Timeout waiting for super_read_only to be " "unset after call to start Group Replication " "plugin.") _LOGGER.step( "Group Replication started for group: %s.", gr_config_vars[GR_GROUP_NAME]) unset_bootstrap(server, dry_run) # Update the Group Replication options on defaults file. # Note: Set group_replication_start_on_boot=ON if OPTION_PARSER in req_dict.keys(): persist_gr_config(req_dict[OPTION_PARSER], gr_config_vars, dry_run=dry_run, skip_backup=skip_backup) if dry_run: _LOGGER.warning(_WARN_DRY_RUN_USED) return False finally: # Disconnect the server prior to end method invocation. if server is not None: server.disconnect() return True