def setUp(self):
        """ Setup server connections (for all tests) .
        """
        self.source_cnx = {'conn_info': self.options[SERVER_CNX_OPT][0]}
        self.destination_cnx = {'conn_info': self.options[SERVER_CNX_OPT][1]}
        self.source = server.Server(self.source_cnx)
        self.destination = server.Server(self.destination_cnx)
        self.source.connect()
        self.destination.connect()
        try:
            self.source_gtids_enabled = self.source.supports_gtid()
        except exceptions.GadgetServerError:
            self.source_gtids_enabled = False
        try:
            self.destination_gtids_enabled = self.destination.supports_gtid()
        except exceptions.GadgetServerError:
            self.destination_gtids_enabled = False

        if self.destination_gtids_enabled:
            self.destination_has_no_gtid_executed = not \
                self.destination.get_gtid_executed(skip_gtid_check=False)
        else:
            self.destination_has_no_gtid_executed = False

        self.mysqldump_exec = get_tool_path(None,
                                            "mysqldump",
                                            search_path=True,
                                            required=False)
        self.mysql_exec = get_tool_path(None,
                                        "mysql",
                                        search_path=True,
                                        required=False)

        # Search for mysqldump only on path (ignore MySQL default paths).
        self.mysqldump_exec_in_defaults = get_tool_path(None,
                                                        "mysqldump",
                                                        search_path=False,
                                                        required=False)

        # setup source server with a sample database and users
        data_sql_path = os.path.normpath(
            os.path.join(__file__, "..", "std_data", "basic_data.sql"))
        self.source.read_and_exec_sql(data_sql_path, verbose=True)

        # Create user without super permission on both source and destination
        self.source.exec_query("create user no_super@'%'")
        self.source.exec_query("GRANT ALL on *.* to no_super@'%'")
        self.source.exec_query("REVOKE SUPER on *.* from no_super@'%'")
        self.destination.exec_query("create user no_super@'%'")
        self.destination.exec_query("GRANT ALL on *.* to no_super@'%'")
        self.destination.exec_query("REVOKE SUPER on *.* from no_super@'%'")
        if self.destination_has_no_gtid_executed:
            self.destination.exec_query("RESET MASTER")
Example #2
0
 def test_get_server_version(self):
     """Tests the get_server_version method."""
     # get version from the server
     version_from_server = self.server.get_version()
     # try to find the executable path via basedir
     basedir = self.server.select_variable("basedir", "global")
     try:
         mysqld_path = tools.get_tool_path(basedir, "mysqld", required=True)
     except GadgetError:
         raise unittest.SkipTest("Couldn't find mysqld executable")
     else:
         version_from_binary = server.get_mysqld_version(mysqld_path)
         self.assertEqual(tuple(version_from_server),
                          version_from_binary[0])
         self.assertIn(".".join(str(i) for i in version_from_binary[0]),
                       version_from_binary[1])
 def test_get_server_version(self):
     """Tests the get_server_version method."""
     # get version from the server
     version_from_server = self.server.get_version()
     # try to find the executable path via basedir
     basedir = self.server.select_variable("basedir", "global")
     try:
         mysqld_path = tools.get_tool_path(basedir, "mysqld", required=True)
     except GadgetError:
         raise unittest.SkipTest("Couldn't find mysqld executable")
     else:
         version_from_binary = server.get_mysqld_version(mysqld_path)
         self.assertEqual(tuple(version_from_server),
                          version_from_binary[0])
         self.assertIn(".".join(str(i) for i in version_from_binary[0]),
                       version_from_binary[1])
Example #4
0
    def test_is_executable(self):
        """Test is_executable function"""
        # python is an executable file
        try:
            python_path = tools.get_tool_path(None, "python", required=True,
                                              search_path=True)
        except GadgetError:
            raise unittest.SkipTest("Couldn't find python executable.")
        self.assertTrue(tools.is_executable(python_path))

        # a simple file is not executable, list this test file is not
        # executable
        if os.name == "posix":
            # on windows the test file is deemed as executable
            self.assertFalse(tools.is_executable(__file__))
        # A file that does not exist is also not executable
        self.assertFalse(tools.is_executable(__file__ + "aa"))
Example #5
0
 def test_run_subprocess(self):
     """Test run_subprocess function"""
     # Run a simple echo command
     try:
         echo_path = tools.get_tool_path(None, "echo", required=True,
                                         search_path=True)
     except GadgetError:
         raise unittest.SkipTest("Couldn't find echo executable.")
     p = tools.run_subprocess("{0} output".format(echo_path),
                              stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE,
                              universal_newlines=True)
     out, err = p.communicate()
     ret_code = p.returncode
     # process ran ok
     self.assertEqual(ret_code, 0)
     self.assertEqual(out, "output\n")
     self.assertEqual("", err)
Example #6
0
    def test_get_tool_path(self):
        """ Test get_tool_path function.
        """
        # Get server basedir.
        basedir = self.server.exec_query(
            "SHOW VARIABLES LIKE '%basedir%'")[0][1]

        # Find 'mysqld' using default option.
        res = tools.get_tool_path(basedir, 'mysqld')
        self.assertIn("mysqld", res,
                      "The 'mysqld' file is expected to be found in basedir.")

        # Find 'mysqld' and return result with quotes.
        res = tools.get_tool_path(basedir, 'mysqld', quote=True)
        self.assertIn("mysqld", res,
                      "The 'mysqld' file is expected to be found in basedir.")
        quote_char = "'" if os.name == "posix" else '"'
        self.assertEqual(res[0], quote_char,
                         "Result expected to be quoted with "
                         "({0}).".format(quote_char))

        # Find 'mysqld' using the default path list.
        bindir = os.path.join(basedir, 'bin')
        res = tools.get_tool_path('.', 'mysqld', defaults_paths=[bindir])
        self.assertIn("mysqld", res,
                      "The 'mysqld' file is expected to be found in"
                      "defaults_paths.")

        # Find 'mysqld' using a check tool function.
        res = tools.get_tool_path(basedir, 'mysqld',
                                  check_tool_func=server.is_valid_mysqld)
        self.assertIn("mysqld", res,
                      "The 'mysqld' file is expected to be found in basedir.")

        # Find 'mysqld' using a check tool function and the default path list.
        bindir = os.path.join(basedir, 'bin')
        res = tools.get_tool_path('.', 'mysqld', defaults_paths=[bindir],
                                  check_tool_func=server.is_valid_mysqld)
        self.assertIn("mysqld", res,
                      "The 'mysqld' file is expected to be found in"
                      "defaults_paths.")

        # Find 'mysqld' using the PATH variable.
        path_var = os.environ['PATH']
        os.environ['PATH'] = bindir
        try:
            res = tools.get_tool_path('.', 'mysqld', search_path=True)
        finally:
            # Make sure the PATH variable is restored.
            os.environ['PATH'] = path_var
        self.assertIn("mysqld", res,
                      "The 'mysqld' file is expected to be found in PATH.")

        # Find 'mysqld' using a check tool function and the PATH variable.
        path_var = os.environ['PATH']
        os.environ['PATH'] = bindir
        try:
            res = tools.get_tool_path('.', 'mysqld', search_path=True,
                                      check_tool_func=server.is_valid_mysqld)
        finally:
            # Make sure the PATH variable is restored.
            os.environ['PATH'] = path_var
        self.assertIn("mysqld", res,
                      "The 'mysqld' file is expected to be found in PATH.")

        # Try to find a non existing tool (error raised by default).
        with self.assertRaises(GadgetError) as cm:
            tools.get_tool_path(basedir, 'non_existing_tool')
        self.assertEqual(cm.exception.errno, 1)

        # Cannot find a valid tool, based on check function (raise error)
        # Note: Use a lambda function that always return false.
        with self.assertRaises(GadgetError) as cm:
            tools.get_tool_path(basedir, 'mysqld',
                                check_tool_func=lambda path: False)
        self.assertEqual(cm.exception.errno, 2)

        # Try to find a non existing tool with required set to False.
        res = tools.get_tool_path(basedir, 'non_existing_tool', required=False)
        self.assertIsNone(res, "None expected when trying to find"
                               "'non_existing_tool'")
    def clone_stream(connection_dict):
        """Clone the contents of source server into destination using a stream.

        :param connection_dict: dictionary of dictionaries of connection
                                information: mysql users and host users. It can
                                have the following keys: MYSQL_SOURCE,
                                MYSQL_DEST, HOST_SOURCE and HOST_DEST.
                                Each of these keys has as a value a dict with
                                the following keys: user, hostname, port,
                                passwd and their respective values.
        :type connection_dict: dict
        """
        # Check tool requirements
        mysqldump_exe = get_tool_path(None,
                                      "mysqldump",
                                      search_path=True,
                                      required=False)
        if not mysqldump_exe:
            raise exceptions.GadgetError(
                "Could not find mysqldump executable. Make sure it is on "
                "{0}.".format(PATH_ENV_VAR))

        mysqlc_exe = get_tool_path(None,
                                   "mysql",
                                   search_path=True,
                                   required=False)
        if not mysqlc_exe:
            raise exceptions.GadgetError(
                "Could not find mysql client executable. Make sure it is on "
                "{0}.".format(PATH_ENV_VAR))

        # Creating Server instances for source and destination servers
        source_dict = connection_dict[MYSQL_SOURCE]
        destination_dict = connection_dict[MYSQL_DEST]
        try:
            source_server = Server({'conn_info': source_dict})
        except exceptions.GadgetError as err:
            _LOGGER.error(
                "Unable to create a Server instance for source server."
                "Source dict was: %s", source_dict)
            raise err
        try:
            destination_server = Server({'conn_info': destination_dict})
        except exceptions.GadgetError as err:
            _LOGGER.error(
                "Unable to create a Server instance for destination "
                "server. Destination dict was: %s", destination_dict)
            raise err

        # Connect to source and destination servers
        try:
            source_server.connect()
        except exceptions.GadgetServerError as err:
            raise exceptions.GadgetError(
                "Unable to connect to source server: {0}".format(str(err)))
        try:
            destination_server.connect()
        except exceptions.GadgetServerError as err:
            raise exceptions.GadgetError("Unable to connect to destination "
                                         "server: {0}.".format(str(err)))

        # Create config_file for mysqldump
        dump_config_file = Server.to_config_file(source_server, "mysqldump")

        # Create config_file for mysql client
        client_config_file = Server.to_config_file(destination_server,
                                                   "client")

        # Create command list to create the backup
        backup_cmd = shlex.split(
            _MYSQLDUMP_STREAM_BACKUP_CMD.format(mysqldump_exec=mysqldump_exe,
                                                config_file=dump_config_file,
                                                quote=QUOTE_CHAR))

        # Create command list to restore the backup
        restore_cmd = shlex.split(
            _MYSQLDUMP_STREAM_RESTORE_CMD.format(
                mysqlc_exec=mysqlc_exe,
                config_file=client_config_file,
                quote=QUOTE_CHAR))

        # enable global read_lock
        _LOGGER.debug("Locking global read lock on source server to prevent "
                      "modifications during clone.")
        source_server.toggle_global_read_lock(True)
        _LOGGER.debug("Source server locked (read-only=ON)")

        try:
            _LOGGER.debug(
                "Dumping contents of source server using command: "
                "%s", " ".join(backup_cmd))

            dump_process = subprocess.Popen(backup_cmd,
                                            stdout=subprocess.PIPE,
                                            stderr=subprocess.PIPE,
                                            universal_newlines=True)
            _LOGGER.debug(
                "Restoring contents to destination server using "
                "command: %s", " ".join(restore_cmd))
            restore_process = subprocess.Popen(restore_cmd,
                                               stdin=dump_process.stdout,
                                               stdout=subprocess.PIPE,
                                               stderr=subprocess.PIPE,
                                               universal_newlines=True)
            # We call dump_process.stdout.close before
            # restore_process.communicate so that if restore_process dies
            # prematurely the SIGPIPE signal can be processed by dump_process
            # allowing it to exit.
            dump_process.stdout.close()
            # Wait for restore process to end and get the output.
            _, err = restore_process.communicate()
            dump_process.wait()
            error_msg = ""
            if dump_process.returncode:
                error_msg = (
                    "mysqldump exited with error code '{0}' and message: "
                    "'{1}'. ".format(dump_process.returncode,
                                     dump_process.stderr.read().strip()))
            else:
                _LOGGER.info("Dump process successfully completed.")
            if restore_process.returncode:
                error_msg += (
                    "MySQL client exited with error code '{0}' and message: "
                    "'{1}'".format(restore_process.returncode, err.strip()))
            else:
                _LOGGER.info("Restore process successfully completed.")

            # If there were errors, raise an exception to warn the user.
            if error_msg:
                raise exceptions.GadgetError(error_msg)
        finally:
            # disable global read_lock
            _LOGGER.debug("Unlocking global read lock on source server.")
            source_server.toggle_global_read_lock(False)
            _LOGGER.debug("Source server unlocked. (read-only=OFF)")
            # delete created configuration files
            try:
                _LOGGER.debug("Removing configuration file '%s'",
                              dump_config_file)
                os.remove(dump_config_file)
            except OSError:
                _LOGGER.warning("Unable to remove configuration file '%s'",
                                dump_config_file)
            else:
                _LOGGER.debug("Configuration file '%s' successfully removed",
                              dump_config_file)
            try:
                _LOGGER.debug("Removing configuration file '%s'",
                              client_config_file)
                os.remove(client_config_file)
            except OSError:
                _LOGGER.warning("Unable to remove configuration file '%s'",
                                client_config_file)
            else:
                _LOGGER.debug("Configuration file '%s' successfully removed",
                              client_config_file)

        _LOGGER.info("Contents loaded successfully into destination " "server")
    def restore_from_image(connection_dict, image_path):
        """"Restore an image file into the destination server.

        :param connection_dict: dictionary of dictionaries of connection
                                information: mysql users and host users. It can
                                have the following keys: MYSQL_SOURCE,
                                MYSQL_DEST, HOST_SOURCE and HOST_DEST.
                                Each of these keys has as a value a dict with
                                the following keys: user, hostname, port,
                                passwd and their respective values.
        :type connection_dict: dict
        :param image_path: name/path of the image that we will be read to do
                           the restore operation.
        :type image_path:  string
        """
        # Creating Server for destination server
        destination_dict = connection_dict[MYSQL_DEST]
        try:
            destination_server = Server({'conn_info': destination_dict})
        except exceptions.GadgetError as err:
            _LOGGER.error(
                "Unable to create a Server instance for destination "
                "server. Destination dict was: %s", destination_dict)
            raise err

        # Connect to destination server
        try:
            destination_server.connect()
        except exceptions.GadgetServerError as err:
            raise exceptions.GadgetError("Unable to connect to destination "
                                         "server: {0}.".format(str(err)))

        mysqlc_exe = get_tool_path(None,
                                   "mysql",
                                   search_path=True,
                                   required=False)
        if not mysqlc_exe:
            raise exceptions.GadgetError(
                "Could not find MySQL client executable. Make sure it is on "
                "{0}.".format(PATH_ENV_VAR))

        # Create config_file for mysql client
        client_config_file = Server.to_config_file(destination_server,
                                                   "client")
        # Replace image_name backslashes with forward slashes to pass it to the
        # mysql source command
        if os.name == 'nt':
            image_path = '/'.join(image_path.split(os.sep))
        # Create command list to restore the backup
        restore_cmd = shlex.split(
            _MYSQLDUMP_IMAGE_RESTORE_CMD.format(mysqlc_exec=mysqlc_exe,
                                                config_file=client_config_file,
                                                image_file=image_path,
                                                quote=QUOTE_CHAR))
        try:
            _LOGGER.debug(
                "Restoring contents of destination server from "
                "image file %s using command: %s", image_path,
                " ".join(restore_cmd))
            restore_process = subprocess.Popen(restore_cmd,
                                               stderr=subprocess.PIPE,
                                               universal_newlines=True)

            _, err = restore_process.communicate()
            if restore_process.returncode:
                raise exceptions.GadgetError(
                    "MySQL client exited with error code '{0}' and message: "
                    "'{1}'. ".format(restore_process.returncode, err.strip()))
            else:
                _LOGGER.info("Restoring from file was successful.")
        finally:
            # delete created configuration file
            try:
                _LOGGER.debug("Removing configuration file '%s'",
                              client_config_file)
                os.remove(client_config_file)
            except OSError:
                _LOGGER.warning("Unable to remove configuration file '%s'",
                                client_config_file)
            else:
                _LOGGER.debug("Configuration file '%s' successfully removed",
                              client_config_file)

        _LOGGER.info(
            "Destination server contents successfully loaded from "
            "file '%s'.", image_path)
    def backup_to_image(connection_dict, image_path):
        """"Backup the contents of source server into an image file.

        :param connection_dict: dictionary of dictionaries of connection
                                information: mysql users and host users. It can
                                have the following keys: MYSQL_SOURCE,
                                MYSQL_DEST, HOST_SOURCE and HOST_DEST.
                                Each of these keys has as a value a dict with
                                the following keys: user, hostname, port,
                                passwd and their respective values.
        :type connection_dict: dict
        :param image_path: name/path of the image that we will create with the
                           backup.
        :type image_path:  string
        """
        # Check tool requirements
        mysqldump_exe = get_tool_path(None,
                                      "mysqldump",
                                      search_path=True,
                                      required=False)
        if not mysqldump_exe:
            raise exceptions.GadgetError(
                "Could not find mysqldump executable. Make sure it is on "
                "{0}.".format(PATH_ENV_VAR))

        # Creating Server instance for source server
        source_dict = connection_dict[MYSQL_SOURCE]
        try:
            source_server = Server({'conn_info': source_dict})
        except exceptions.GadgetError as err:
            _LOGGER.error(
                "Unable to create a Server instance for source "
                "server. Source dict was: %s", source_dict)
            raise err

        # Connect to source server
        try:
            source_server.connect()
        except exceptions.GadgetServerError as err:
            raise exceptions.GadgetError(
                "Unable to connect to source server: {0}".format(str(err)))

        # Create config_file for mysqldump
        dump_config_file = Server.to_config_file(source_server, "mysqldump")

        # Create command list for backup
        backup_cmd = shlex.split(
            _MYSQLDUMP_IMAGE_BACKUP_CMD.format(mysqldump_exec=mysqldump_exe,
                                               config_file=dump_config_file,
                                               image_file=image_path,
                                               quote=QUOTE_CHAR))

        # enable global read_lock
        _LOGGER.debug("Locking global read lock on source server to prevent "
                      "modifications during clone.")
        source_server.toggle_global_read_lock(True)
        _LOGGER.debug("Source server locked (read-only=ON)")

        # Do the backup
        try:
            _LOGGER.debug(
                "Dumping contents of source server to image file %s "
                "using command: %s", image_path, " ".join(backup_cmd))
            dump_process = subprocess.Popen(backup_cmd,
                                            stderr=subprocess.PIPE,
                                            universal_newlines=True)
            _, err = dump_process.communicate()
            if dump_process.returncode:
                raise exceptions.GadgetError(
                    "mysqldump exited with error code '{0}' and message: "
                    "'{1}'. ".format(dump_process.returncode, err.strip()))
            else:
                _LOGGER.info("Dumping to file was successful.")

        finally:
            # disable global read_lock
            _LOGGER.debug("Unlocking global read lock on source server.")
            source_server.toggle_global_read_lock(False)
            _LOGGER.debug("Source server unlocked. (read-only=OFF)")
            # delete created configuration file
            try:
                _LOGGER.debug("Removing configuration file '%s'",
                              dump_config_file)
                os.remove(dump_config_file)
            except OSError:
                _LOGGER.warning("Unable to remove configuration file '%s'",
                                dump_config_file)
            else:
                _LOGGER.debug("Configuration file '%s' successfully removed",
                              dump_config_file)

        _LOGGER.info(
            "Source server contents successfully cloned to file "
            "'%s'.", image_path)