def _get_server(self, alt_server=None, raise_error=True):
        """Chooses the server that should be use.

        :param alt_server:  An alternative server instance to use.
        :type alt_server:   Server instance.

        :raise GadgetError: Raises exception if no server has been set to
                            check.

        :return the server that should be use
        :rtype: Server instance
        """
        # Validate alternative server as if given as first option
        server = None
        if alt_server is not None:
            server = alt_server
        # Validate attribute server if set as second option
        if server is None:
            server = self.server
        # If not server to check raise use exception
        if server is None and raise_error:
            msg = "no server has been set to check."
            _LOGGER.error(msg)
            raise GadgetError(msg)

        return server
Esempio n. 2
0
def get_file_differences(file_a, file_b):
    """Compare two files and return their differences.

    Compares the contents of both files and returns an unified diff format
    string with the differences. If there are no differences returns an empty
    string.

    :param file_a: absolute path to one of the files to be compared
    :type file_a: str
    :param file_b: absolute path to one of the files to be compared
    :type file_b: str

    :return: unified diff string with the differences if there are differences
             and if there are no differences return an empty string.
    :rtype: str
    :raises GadgetError: if an error happens while opening the provided files.
    """
    # Check that backup file is equal to original file
    try:
        with open(file_a, 'U') as fa:
            with open(file_b, 'U') as fb:
                diff = list(
                    difflib.unified_diff(fa.readlines(),
                                         fb.readlines(),
                                         fromfile=file_a,
                                         tofile=file_b))
                return "\n".join(diff)

    except (IOError, OSError) as err:
        # wrap error messages in a GadgetError
        raise GadgetError(str(err))
Esempio n. 3
0
def get_abs_path(path_string, relative_to=None):
    """ Get the absolute path for a file
    This function is used to get the absolute path for the argument provided
    via the path_string parameter. If the provided path_string is an
    absolute path, we return it as is, otherwise we will assume it is a
    relative path relative to the relative_to parameter and use that to
    calculate the absolute path.
    :param path_string: Absolute or relative path to which we want to
                        obtain an absolute path.
    :param path_string: string.
    :param relative_to: absolute path to a directory or file which will be used
                        as the path to which the path_string argument is
                        relative.
    :type relative_to: string
    :return: Absolute path for the path specified in path_string.
    :rtype:  string
    :raises GadgetError: if path_string is not an absolute path and the
                 provided relative_dir parameter is not an absolute path.
    """
    if os.path.isabs(os.path.expanduser(path_string)):
        return os.path.expanduser(path_string)
    else:
        if not os.path.isabs(relative_to):
            raise GadgetError(
                "{0} is not a valid absolute path.".format(relative_to))
        else:
            if os.path.isfile(relative_to):
                relative_to = os.path.dirname(relative_to)
            return os.path.normpath(
                os.path.expanduser(os.path.join(relative_to, path_string)))
    def check_variable_values(self, var_values, alt_server=None):
        """Checks the server variable values.

        :param var_values:  dictionary with the variable name as keys and the
                            required value as his value.
        :type var_values:   dict
        :param alt_server:  An alternative server instance to use.
        :type alt_server:   Server instance.

        :return: a dictionary with the results, including the key "pass" with
                 True if all the variables match the required value.
        :rtype:  dict
        """
        # Dictionary should not be used as argument for logger, since it
        # duplicates backslash. Convert to string and convert \\ back to \.
        dic_msg = str(var_values)
        dic_msg = dic_msg.replace("\\\\", "\\")
        _LOGGER.debug('Option checking started: %s', dic_msg)
        # Get the correct server to Validate
        server = self._get_server(alt_server=alt_server)

        # Store Result
        results = {}
        valid = True

        for var_name, value in var_values.items():
            # Value of type Dictionary can hold more complex requirements
            # (namely unwanted values or one of...):
            # key names for comparison:
            #    "NOT IN": The current value must not match any on values
            #    "ONE OF": The current value must match one of
            #    "All OF": The current value must have all the values from the
            #              values list.
            # The value of each key it must be a list of values to use during
            # the comparison.
            if isinstance(value, dict):
                _LOGGER.debug("Checking option: '%s' ", var_name)
                try:
                    cur_val = server.select_variable(var_name)
                    _LOGGER.debug("Option current value: '%s' ", cur_val)
                except GadgetQueryError:
                    cur_val = "<not set>"
                    _LOGGER.debug("Option '%s' does not exists on server %s",
                                  var_name, server)

                if cur_val == "" or cur_val is None:
                    _LOGGER.debug('Option found but with empty value')
                    cur_val = "<no value>"

                res = check_option(var_name, value, cur_val, results)
                valid = res[0] and valid

            else:
                raise GadgetError("The requirements format of option {0} "
                                  "are not valid.".format(var_name))

        _LOGGER.debug('Options check result: %s', valid)
        results["pass"] = valid
        return results
Esempio n. 5
0
    def parse_grant_statement(statement, sql_mode=''):
        """ Returns a namedtuple with the parsed GRANT information.

        :param statement:  Grant string in the sql format returned by the
                           server.
        :type statement:   string
        :param sql_mode:   The sql_mode set on the server.
        :type sql_mode:    string

        :return: named tuple with GRANT information or None.
        :rtype: Tuple or None
        :raise GadgetError: If it is unable to parse grant statement.
        """

        grant_parse_re = re.compile(r"""
            GRANT\s(.+)?\sON\s # grant or list of grants
            (?:(?:PROCEDURE\s)|(?:FUNCTION\s))? # optional for routines only
            (?:(?:(\*|`?[^']+`?)\.(\*|`?[^']+`?)) # object where grant applies
            | ('[^']*'@'[^']*')) # For proxy grants user/host
            \sTO\s([^@]+@[\S]+) # grantee
            (?:\sIDENTIFIED\sBY\sPASSWORD
             (?:(?:\s<secret>)|(?:\s\'[^\']+\')?))? # optional pwd
            (?:\sREQUIRE\sSSL)? # optional SSL
            (\sWITH\sGRANT\sOPTION)? # optional grant option
            $ # End of grant statement
            """, re.VERBOSE)

        grant_tpl_factory = namedtuple("grant_info", "privileges proxy_user "
                                                     "db object user")
        match = re.match(grant_parse_re, statement)

        if match:
            # quote database name and object name with backticks
            if match.group(1).upper() != 'PROXY':
                db_ = match.group(2)
                if not is_quoted_with_backticks(db_, sql_mode) and db_ != '*':
                    db_ = quote_with_backticks(db_, sql_mode)
                obj = match.group(3)
                if not is_quoted_with_backticks(obj, sql_mode) and obj != '*':
                    obj = quote_with_backticks(obj, sql_mode)
            else:  # if it is not a proxy grant
                db_ = obj = None
            grants = grant_tpl_factory(
                # privileges
                set([priv.strip() for priv in match.group(1).split(",")]),
                match.group(4),  # proxied user
                db_,  # database
                obj,  # object
                match.group(5),  # user
            )
            # If user has grant option, add it to the list of privileges
            if match.group(6) is not None:
                grants.privileges.add("GRANT OPTION")
        else:
            raise GadgetError("Unable to parse grant statement "
                              "{0}".format(statement))

        return grants
    def check_server_version(self, ver_values, alt_server=None):
        """Checks server version

        :param ver_values:  string with the required minimum server version in
                            the form "X.Y.Z".
        :type ver_values:   string
        :param alt_server:  An alternative server instance to use.
        :type alt_server:   Server instance.

        :return: a dictionary with the results, including the key "pass" with
                 True if the server is same version or newer otherwise False.
        :rtype:  dict
        """
        _LOGGER.debug('Server version checking: %s', ver_values)
        # Get the correct server to Validate
        server = self._get_server(alt_server=alt_server)

        # Store Result
        results = {}

        # Get the independent values
        try:
            if "." in ver_values:
                ver_vals = ver_values.split(".")
                version = [int(ver_val) for ver_val in ver_vals]
            elif len(ver_values) >= 3:
                version = ver_values[0:3]
            else:
                # The format is not valid.
                raise GadgetError("Unexpected server version format '{0}'"
                                  "".format(ver_values))
        except ValueError:
            msg = ("The given version {0} does not have a valid format 'X.Y.Z'"
                   " or (X, Y, Z)".format(ver_values))
            raise GadgetError(msg)
        except GadgetError:
            raise

        results[SERVER_VERSION] = server.get_version()
        _LOGGER.debug('Server version: %s', results[SERVER_VERSION])

        results["pass"] = results[SERVER_VERSION] >= version
        _LOGGER.debug('Server version check result: %s', results["pass"])
        return results
    def check_user_privileges(self, priv_values, alt_server=None):
        """Verifies the given user's names accounts privileges

        :param priv_values: Dictionary with user name as key and list of
                            privileges as value.
        :type priv_values:  dict
        :param alt_server:  An alternative server instance to use.
        :type alt_server:   Server instance.

        :raise GadgetError: If the user does not have SELECT
                            privilege on mysql.user.

        :return: A dictionary with the results, including the key pass with
                 True if the user has the privileges and False otherwise, and
                 a key with the checked users and the missing privileges as
                 value (or 'NO EXISTS!' if user does not exists).
        :rtype:  dict
        """
        _LOGGER.debug('privileges Checking')
        # Get the correct server to Validate
        server = self._get_server(alt_server=alt_server)

        # Store Result
        results = {}
        # Check required privileges
        for user, privs in priv_values.items():
            _LOGGER.debug('User: %s required privileges: %s', user, privs)
            user_obj = User(server, user)
            try:
                # verify the user exists
                if user_obj.exists():
                    missing_privs = user_obj.check_missing_privileges(privs)
                    _LOGGER.debug('missing privileges: %s', missing_privs)
                    results[user] = missing_privs
                    if missing_privs:
                        results["pass"] = False
                else:
                    # The user's result will be set to ['NO EXISTS!']
                    # as [] and None are evaluated to False
                    results[user] = ['NO EXISTS!']
                    results["pass"] = False

            except GadgetQueryError as err:
                if "SELECT command denied" in err.errmsg:
                    raise GadgetError("User {} does not have SELECT privileges"
                                      " on user table; unable to check "
                                      "privileges with this account."
                                      "".format(server.user))
                else:
                    raise

        if "pass" not in results.keys():
            results["pass"] = True
        _LOGGER.debug('Privileges check result: %s', results)
        return results
Esempio n. 8
0
    def test_gadget_error(self):
        """Test gadget error.
        """
        # Raise GadgetError with default options.
        with self.assertRaises(GadgetError) as cm:
            raise GadgetError("I am a GadgetError")
        self.assertEqual(str(cm.exception), "I am a GadgetError")
        self.assertEqual(cm.exception.errmsg, "I am a GadgetError")
        self.assertEqual(cm.exception.errno, 0)
        self.assertIsNone(cm.exception.cause)

        # Raise GadgetError with specific options.
        with self.assertRaises(GadgetError) as cm:
            raise GadgetError("I am a GadgetError", errno=1234,
                              cause=Exception("Root cause error"))
        self.assertEqual(str(cm.exception), "I am a GadgetError")
        self.assertEqual(cm.exception.errmsg, "I am a GadgetError")
        self.assertEqual(cm.exception.errno, 1234)
        self.assertIsNotNone(cm.exception.cause)
        self.assertEqual(str(cm.exception.cause), "Root cause error")
Esempio n. 9
0
def stop_process_with_pid(pid, force=False):
    """Terminates or kills a process with a given pid.

    This method attempts to stop a process with the provided pid.
    If force parameter is False, this it attempts to gracefully terminate
    the process. Otherwise, if force parameter is True it attempts to
    forcefully terminate the process.

    :param pid: pid of the process we want to terminate/kill.
    :type pid: int
    :param force: If True process is forcefully killed, otherwise it is
                  gracefully terminated.
    :type force: bool
    :raises GadgetError: if unable to kill or terminate the process.
    """
    error_msg = "Unable to {0} process '{1}': '{2}'"
    if force:
        if os.name == "nt":
            # windows doesn't have sigkill, we need to use taskkill
            # to terminate a process.
            kill_proc = run_subprocess("taskkill /PID {0} /F".format(pid),
                                       shell=False,
                                       stderr=subprocess.PIPE,
                                       universal_newlines=True)
            _, err = kill_proc.communicate()
            if kill_proc.returncode:
                raise GadgetError(error_msg.format("kill", pid, str(err)))
        else:  # posix
            try:
                # send SIGKILL
                # pylint: disable=E1101
                os.kill(pid, signal.SIGKILL)
            except OSError as err:
                raise GadgetError(error_msg.format("kill", pid, str(err)))
    else:
        try:
            # send SIGTERM
            os.kill(pid, signal.SIGTERM)
        except OSError as err:
            raise GadgetError(error_msg.format("terminate", pid, str(err)))
Esempio n. 10
0
def check_privileges(server, operation, privileges, description=None):
    """Check required privileges.

    This method check if the used user possess the required privileges to
    execute a statement or operation.
    An exception is thrown if the user doesn't have enough privileges.

    :param server:      Server instance to check.
    :type  server:      Server
    :param operation:   The name of tha task that requires the privileges,
                        used in the error message if an exception is thrown.
    :type  operation:   string
    :param privileges:  List of the required privileges.
    :type  privileges:  List of strings

    :param description:  Description of the operation requiring the User's
                         privileges, if given it will be used in the loggin
                         message.
    :type  description:  string

    :raise GadgetError: if the user lacks one or more required privileges.
    """
    # log message if description was given.
    if description is not None:
        _LOGGER.info("Checking user permission to %s...\n", description)

    # Check privileges
    user_obj = User(server, "{0}@{1}".format(server.user, server.host))

    privileges_needed = user_obj.check_missing_privileges(privileges)

    if len(privileges_needed) > 0:
        raise GadgetError(ERROR_USER_WITHOUT_PRIVILEGES.format(
            user=server.user, host=server.host, port=server.port,
            operation=operation, req_privileges=privileges_needed
        ))
Esempio n. 11
0
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
Esempio n. 12
0
def create_option_file(section_dict, name=None, prefix_dir=None):
    """ Create an option file from a dictionary of dictionaries.

    :param section_dict: dictionary of dictionaries. The keys in the top level
                         dictionary are sections and the values are
                         dictionaries whose keys and values are the key:value
                         pairs of the section.
    :type section_dict:  {section1: {key: val, key2: val},
                          section2: {key: val..}
                          ..}
    :param name: name of the config file. If no name is provided, a randomly
                named option file is created.
    :type name: str or None
    :param prefix_dir: full path to a directory where we want the temporary
                       file to be created. By default it uses the $HOME of the
                       user.
    :type prefix_dir: str
    :return: string with full path to the created config file.
    :rtype: str
    """
    if prefix_dir is None:
        prefix_dir = os.path.expanduser("~")
    else:  # check if prefix points to a valid folder
        # normalize path and expand possible ~
        prefix_dir = os.path.normpath(os.path.expanduser(prefix_dir))
        if not os.path.isdir(prefix_dir):
            raise GadgetError("prefix_dir '{0}' is not a valid folder. Check "
                              "if it exists.".format(prefix_dir))
    _LOGGER.debug("Creating option file under directory %s ...", prefix_dir)
    if name:
        f_path = os.path.join(prefix_dir, name)
        if os.path.exists(f_path):
            raise GadgetError("Unable to create option file '{0}' since a "
                              "file of the same already exists."
                              "".format(f_path))
        try:
            f_handler = os.open(f_path, os.O_CREAT | os.O_WRONLY | os.O_EXCL,
                                0o600)
        except (OSError, IOError) as err:
            raise GadgetError("Unable to create named configuration "
                              "file '{0}': {1}.".format(f_path, str(err)))
    else:
        try:
            # create temporary file under prefix_dir
            f_handler, f_path = tempfile.mkstemp(dir=prefix_dir, suffix=".cnf")
        except (OSError, IOError) as err:
            raise GadgetError("Unable to create randomly named configuration "
                              "file in directory '{0}': {1}."
                              "".format(prefix_dir, str(err)))
    _LOGGER.debug("Config file %s created successfully ", f_path)
    # Create configuration file
    config = configparser.RawConfigParser(allow_no_value=True)

    _LOGGER.debug("Filling config parser object...")
    # Fill it with contents from options
    for section, section_d in section_dict.items():
        config.add_section(section)
        for key, val in section_d.items():
            config.set(section, key, val)
    _LOGGER.debug("Config parser object created.")
    _LOGGER.debug("Writing contents of the configuration file")
    with closing(os.fdopen(f_handler, 'w')) as cnf_file:
        config.write(cnf_file)
    _LOGGER.debug("Config file %s successfully written.", f_path)
    return f_path
Esempio n. 13
0
def resolve_gr_local_address(gr_host, server_host, server_port):
    """Resolves Group Replication local address (host, port).

    If a host is not found on gr_host, the returned host is the one given on
    server_host.
    If a port is not found on gr_host, the returned port is the one given on
    server_port plus 10000, unless the result is higher than 65535, in that
    case a random port number will be generated.

    :param gr_host:     Group replication host address in the format:
                        <host>[:<port>] (i.e., host or host and port separated
                        by ':').
    :type gr_host:      string
    :param server_host: The host where the MySQL server is running.
    :type server_host:  string
    :param server_port: The port that the MySQL server is using.
    :type server_port:  string

    :raise GadgetError:  If could not found a free port.

    :return: A tuple with host and port.
    :rtype:  tuple
    """
    # No info provided, use the server to generate it.
    if gr_host is None or gr_host == "":
        gr_host = server_host
        local_port = str(int(server_port) + 10000)

    # gr_host can have both elements; host and port, but be aware of IPv6
    elif len(gr_host.rsplit(":", 1)) == 2 and gr_host[-1] != "]":
        gr_host, local_port = gr_host.rsplit(":", 1)
        if not gr_host:
            gr_host = server_host
        if not local_port:
            local_port = str(int(server_port) + 10000)
        elif not local_port.isdigit():
            gr_host = "{0}:{1}".format(gr_host, local_port)
            local_port = str(int(server_port) + 10000)

    # Try to get the port only
    elif gr_host.isdigit():
        local_port = gr_host
        gr_host = server_host

    # Generate a local port based on the + 10000 rule.
    else:
        local_port = str(int(server_port) + 10000)

    # in case the gr_host is a IPv6 remove the square brackets '[ ]'
    gr_host = clean_IPv6(gr_host)

    # Generate a random port if out of range.
    if int(local_port) < 0 or int(local_port) > 65535:
        local_port = str(random.randint(10000, 65535))
        # gr_host is host address

    # verify the port is not in use.
    tries = 1
    port_found = False
    while tries < 5 and not port_found:
        tries += 1
        if not is_listening(server_host, int(local_port)):
            port_found = True
        else:
            local_port = str(random.randint(10000, 65535))

    if not port_found:
        raise GadgetError("Unable to find an available port on which the "
                          "member will expose itself to be contacted by the "
                          "other members of the group. Please try again to "
                          "attempt to use another random port or free port "
                          "{0}.".format(str(int(server_port) + 10000)))

    return gr_host, local_port
Esempio n. 14
0
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
Esempio n. 15
0
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
Esempio n. 16
0
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
Esempio n. 17
0
    def check_config_settings(self, var_values, location, alt_server=None):
        """Checks the server variable undesired_values.

        :param var_values:  dictionary with the variable name as keys and the
                            required value as his value.
        :type var_values:   dict
        :param location:    Path to the option file or an option parser.
        :type location:     string or MySQLOptionsParser instance.
        :param alt_server:  An alternative server instance to use.
        :type alt_server:   Server instance.

        :raise GadgetError: If location is not an instance of string or
                            MySQLOptionsParser.

        :return: a dictionary with the results, including the key "pass" with
                 True if all the variables match the required value.
        :rtype:  dict
        """
        # Dictionary should not be used as argument for logger, since it
        # duplicates backslash. Convert to string and convert \\ back to \.
        dic_msg = str(var_values)
        dic_msg = dic_msg.replace("\\\\", "\\")
        _LOGGER.debug('Server config settings checking: %s', dic_msg)
        # Get the correct server to Validate, for check the file the server
        # is optional.
        server = self._get_server(alt_server=alt_server, raise_error=False)

        # Option parser
        if isinstance(location, MySQLOptionsParser):
            opt_parser = location
        elif isinstance(location, str):
            opt_parser = MySQLOptionsParser(location)
        else:
            raise GadgetError(
                "An instance of string or MySQLOptionsParser "
                "was expected not %s", location)

        # Store Result
        results = {}
        # Check is valid only if none option is faulty.
        valid = True
        section = "mysqld"

        if server is not None and opt_parser.has_option(section, "port"):
            port_val = opt_parser.get(section, "port")
            if server.port != int(port_val):
                _LOGGER.warning(
                    "The port number %s in the option file "
                    "differs from server port number"
                    " %s.", port_val, server.port)
        if server is not None and opt_parser.has_option(section, "host"):
            host_val = opt_parser.get(section, "port")
            if server.host != host_val:
                _LOGGER.warning(
                    "The host name %s in the option file "
                    "differs from server host name"
                    " %s.", host_val, server.host)

        missing = {}
        for opt_name, value in var_values.items():
            # Use "_" as standard as is used on the server variables
            opt_name = opt_name.replace("-", "_")

            # Value of type Dictionary can hold more complex requirements
            # (namely unwanted values or one of...):
            # key names for comparison:
            #    "NOT IN": The current value must not match any on values
            #    "ONE OF": The current value must match one of
            #    "All OF": The current value must have all the values from the
            #              values list.
            # The value of each key it must be a list of values to use during
            # the comparison.
            if isinstance(value, dict):
                _LOGGER.debug("Checking option: '%s' ", opt_name)
                if opt_parser.has_option(section, opt_name):
                    # the option is already set:
                    cur_val = opt_parser.get(section, opt_name)
                    _LOGGER.debug("Option current value: '%s' ", cur_val)
                    if cur_val == "" or cur_val is None:
                        _LOGGER.debug('Option found but with empty value')
                        cur_val = "<no value>"
                else:
                    cur_val = "<not set>"
                    _LOGGER.debug("Option does not exists on section: '%s' ",
                                  section)

                res = check_option(opt_name, value, cur_val, results)
                valid = res[0] and valid
                missing = res[1]

            else:
                raise GadgetError("The requirements format of option {0} is "
                                  "not valid.".format(opt_name))

        _LOGGER.debug('Options check result: %s', valid)
        if missing:
            results["missing"] = missing
            results["pass"] = False

        results["pass"] = valid
        return results
Esempio n. 18
0
def check_expected_version(expected_version):
    """ Check the expected version of the used tools.

    Compare the given version with the current version of the modules/tools
    raising an exception if it is not compatible.

    For the expected version to be considered compatible with the current
    version, the major version number must be the same and the minor version
    number of the current version must be greater or equal than the expected
    version. The patch version number is ignored.

    Rationale: The major version number shall always be incremented if an
    incompatible change is made. The minor version number shall be
    incremented if new features are added in a backward-compatible manner,
    meaning that previous features are expected to continue to work (but the
    new added feature is only expected to be available starting from that
    minor version). The patch version is incremented for backward-compatible
    changes (e.g. bug fixes).

    :param expected_version: Excepted version to compare to the current one.
    :type expected_version:  String

    :raises GadgetError: If the specified expected version value/format is
                         invalid.
    :raises GadgetVersionError: If the expected version is not compatible with
                                the current one.
    """
    # Validate expected_version value and format.
    if expected_version:
        version_values = expected_version.split('.')
        if len(version_values) > 3:
            raise GadgetError("Invalid expected version value: '{0}'. Please "
                              "specify at most 3 version number parts "
                              "(format: MAJOR[.MINOR[.PATCH]])."
                              "".format(expected_version))
    else:
        raise GadgetError("Invalid expected version value: '{0}'. Please "
                          "specify a valid version string (format: "
                          "MAJOR[.MINOR[.PATCH]]).".format(expected_version))

    # Get major, minor and patch version numbers.
    version_num = [-1, -1, -1]
    for idx, v_num in enumerate(version_values):
        if idx == 0:
            v_num_type = 'major'
        elif idx == 1:
            v_num_type = 'minor'
        else:
            v_num_type = 'patch'
        try:
            version_num[idx] = int(v_num)
            if version_num[idx] < 0:
                raise GadgetError("Invalid integer for the expected {0} "
                                  "version, it cannot be a negative number: "
                                  "'{1}'.".format(v_num_type, v_num))
        except ValueError:
            raise GadgetError("Invalid integer for the expected {0} version "
                              "number: '{1}'.".format(v_num_type, v_num))

    # Validate the expected version comparing it to the current version.
    if (version_num[0] != VERSION[0]) or (version_num[1] > VERSION[1]):
        # The major expected version number must be the same as the one of the
        # current tool version.
        # The minor expected version number must be lower or equal than the one
        # of the current tool version.
        # The patch expected version number is ignored since it is associated
        # to compatible changes.
        raise GadgetVersionError(
            "The expected version is not compatible with the current version. "
            "Current version '{0}' and expected version: '{1}'."
            "".format('.'.join(map(str, VERSION[0:3])), expected_version))
Esempio n. 19
0
def get_tool_path(basedir,
                  tool,
                  fix_ext=True,
                  required=True,
                  defaults_paths=None,
                  search_path=False,
                  quote=False,
                  check_tool_func=None):
    """Search for a MySQL tool and return the full path

    :param basedir:        The initial basedir (of a MySQL server) to search.
    :type basedir:         string or None
    :param tool:           The name of the tool to find
    :type tool:            string
    :param fix_ext:        If True (default is True), add .exe if running on
                           Windows.
    :type fix_ext:         boolean
    :param required:       If True (default is True) then an error will be
                           raised if the tool is not found.
    :type required:        boolean
    :param defaults_paths: Default list of paths to search for the tool.
                           By default (None) an empty list is assumed, i.e. [].
    :type defaults_paths:  list
    :param search_path:    Indicates if the paths specified by the PATH
                           environment variable will be used to search for the
                           tool. By default the PATH will not be searched,
                           i.e. search_path=False.
    :type search_path:     boolean
    :param quote:          if True then the resulting path is surrounded with
                           the OS quotes.
    :type quote:           boolean
    :param check_tool_func Function to verify the validity of the found tool.
                           This function must take the path of the tool as
                           parameter and return True or False depending if the
                           tool is valid or not (e.g., verify the version).
                           If this check tool function is specified, it will
                           continue searching for the tool in the provided
                           default paths until a valid one is found. By
                           default: None, meaning that it returns the first
                           location found with the tool (without any check).
    :type check_tool_func  function

    :return: the full path to tool or a list of paths where the tool was found
            if 'search_all' is set to True, or None if not found and 'required'
             is set to False.
    :rtype:  string

    :raises GadgetError: if the tool cannot be found and 'required' is set to
                         True.
    """
    if not defaults_paths:
        defaults_paths = []
    search_paths = []
    if quote:
        if os.name == "posix":
            quote_char = "'"
        else:
            quote_char = '"'
    else:
        quote_char = ''
    if basedir:
        # Add specified basedir path to search paths
        _add_basedir(search_paths, basedir)

    # Search in path from the PATH environment variable
    if search_path:
        for path in os.environ['PATH'].split(os.pathsep):
            search_paths.append(path)

    if defaults_paths and len(defaults_paths):
        # Add specified default paths to search paths
        for path in defaults_paths:
            search_paths.append(path)
    else:
        # Add default MySQL paths to search for tool
        if os.name == "nt":
            search_paths.append("C:/Program Files/MySQL/MySQL Server 5.7/bin")
            search_paths.append("C:/Program Files/MySQL/MySQL Server 8.0/bin")
        else:
            search_paths.append("/usr/sbin/")
            search_paths.append("/usr/local/mysql/bin/")
            search_paths.append("/usr/bin/")
            search_paths.append("/usr/local/bin/")
            search_paths.append("/usr/local/sbin/")
            search_paths.append("/opt/local/bin/")
            search_paths.append("/opt/local/sbin/")

    if os.name == "nt" and fix_ext:
        tool = "{0}.exe".format(tool)
    # Search for the tool
    for path in search_paths:
        norm_path = os.path.normpath(path)
        if os.path.isdir(norm_path):
            toolpath = os.path.normpath(os.path.join(norm_path, tool))
            if os.path.isfile(toolpath):
                if not check_tool_func or check_tool_func(toolpath):
                    return r"{0}{1}{0}".format(quote_char, toolpath)
            else:
                if tool == "mysqld.exe":
                    toolpath = os.path.normpath(
                        os.path.join(norm_path, "mysqld-nt.exe"))
                    if os.path.isfile(toolpath):
                        if not check_tool_func or check_tool_func(toolpath):
                            return r"{0}{1}{0}".format(quote_char, toolpath)

    # Tool not found, raise exception or return None.
    if required:
        raise GadgetError("Cannot find location of {0}.".format(tool))

    return None
Esempio n. 20
0
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
Esempio n. 21
0
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
Esempio n. 22
0
def resolve_gr_local_address(gr_host, server_host, server_port):
    """Resolves Group Replication local address (host, port).

    If a host is not found on gr_host, the returned host is the one given on
    server_host.
    If a port is not found on gr_host, the returned port is the one given on
    server_port * 10 + 1, unless the result is higher than 65535, in that case
    a random port number will be generated.

    :param gr_host:     Group replication host address in the format:
                        <host>[:<port>] (i.e., host or host and port separated
                        by ':').
    :type gr_host:      string
    :param server_host: The host where the MySQL server is running.
    :type server_host:  string
    :param server_port: The port that the MySQL server is using.
    :type server_port:  string

    :raise GadgetError:  If the local address port is not valid or not free.

    :return: A tuple with host and port.
    :rtype:  tuple
    """
    is_port_specified = False

    # No info provided, use the server to generate it.
    if gr_host is None or gr_host == "":
        gr_host = server_host
        local_port = str(int(server_port) * 10 + 1)

    # gr_host can have both elements; host and port, but be aware of IPv6
    elif len(gr_host.rsplit(":", 1)) == 2 and gr_host[-1] != "]":
        gr_host, local_port = gr_host.rsplit(":", 1)
        if not gr_host:
            gr_host = server_host
        if not local_port:
            local_port = str(int(server_port) * 10 + 1)
        elif (not local_port.isdigit()
              or int(local_port) <= 0 or int(local_port) > 65535):
            # Raise an error if the specified port part is invalid.
            raise GadgetError(
                _ERROR_INVALID_LOCAL_ADDRESS_PORT.format(local_port))

    # Try to get the port only
    elif gr_host.isdigit():
        local_port = gr_host
        gr_host = server_host
        # Raise an error if the specified port is invalid (out of range).
        if int(local_port) <= 0 or int(local_port) > 65535:
            raise GadgetError(
                _ERROR_INVALID_LOCAL_ADDRESS_PORT.format(local_port))

    # Generate a local port based on the * 10 + 1 rule.
    else:
        local_port = str(int(server_port) * 10 + 1)

    # in case the gr_host is a IPv6 remove the square brackets '[ ]'
    gr_host = clean_IPv6(gr_host)

    # Generate a random port if out of range.
    if int(local_port) <= 0 or int(local_port) > 65535:
        local_port = str(random.randint(10000, 65535))
        # gr_host is host address

    # verify the port is not in use.
    if is_listening(server_host, int(local_port)):
        raise GadgetError(
            _ERROR_LOCAL_ADDRESS_PORT_IN_USE.format(local_port))

    return gr_host, local_port
Esempio n. 23
0
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