|
|
@@ -54,9 +54,14 @@ LOG_MYSQL_QUERIES = True
|
|
|
|
|
|
MAX_ALLOWED_PACKET = 1073741824
|
|
|
|
|
|
+CHARSET_TO_ENCODING = {
|
|
|
+ 'utf8': 'utf-8',
|
|
|
+ 'utf8mb4': 'utf-8',
|
|
|
+ 'latin1': 'latin'
|
|
|
+}
|
|
|
|
|
|
-# Utilities
|
|
|
|
|
|
+# Utilities
|
|
|
def load_settings():
|
|
|
""" Load the settings from the 'settings.yml' file
|
|
|
If there is no such file, the base settings.yml file is created
|
|
|
@@ -217,6 +222,13 @@ class MySqlServer:
|
|
|
row = cursor.fetchone()
|
|
|
return row is not None
|
|
|
|
|
|
+ def get_db_charset(self, dbname):
|
|
|
+ """ return the charset (encoding) of the mysql database """
|
|
|
+ cursor = self.exec_query(f"""SELECT default_character_set_name
|
|
|
+ FROM information_schema.SCHEMATA S
|
|
|
+ WHERE schema_name = '{dbname}';""")
|
|
|
+ return cursor.fetchone()[0]
|
|
|
+
|
|
|
def list_tables(self, dbname=""):
|
|
|
""" Return a list of tables (but not views!)
|
|
|
for either the currently selected database,
|
|
|
@@ -236,7 +248,7 @@ class MySqlServer:
|
|
|
def get_view_definition(self, view_name, set_definer=""):
|
|
|
""" Return the SQL create statement for the view
|
|
|
If 'set_definer' is not empty, the username in the 'SET DEFINER' part
|
|
|
- of the create statement is replace by the one given
|
|
|
+ of the create statement is replaced by the one given
|
|
|
"""
|
|
|
cursor = self.exec_query(f"show create view {view_name}")
|
|
|
definition = cursor.fetchone()[1]
|
|
|
@@ -248,6 +260,13 @@ class MySqlServer:
|
|
|
return definition
|
|
|
|
|
|
|
|
|
+class MysqlUser:
|
|
|
+ def __init__(self, username, pwd, host='localhost'):
|
|
|
+ self.username = username
|
|
|
+ self.pwd = pwd
|
|
|
+ self.host = host
|
|
|
+
|
|
|
+
|
|
|
# Operation status
|
|
|
UNKNOWN = 0
|
|
|
SUCCESS = 1
|
|
|
@@ -263,12 +282,14 @@ class CloningOperation:
|
|
|
""" A database cloning operation between two Mysql servers
|
|
|
"""
|
|
|
|
|
|
- def __init__(self, name, dbname, from_server, to_server, is_default=True, ignore_tables=None, structure_only=None,
|
|
|
+ def __init__(self, name, dbname, from_server, to_server, grant=None,
|
|
|
+ is_default=True, ignore_tables=None, structure_only=None,
|
|
|
filter_tables=None, ignore_views=None, compress=True):
|
|
|
self.name = name
|
|
|
self.dbname = dbname
|
|
|
self.from_server = from_server
|
|
|
self.to_server = to_server
|
|
|
+ self.grant = grant if grant is not None else []
|
|
|
|
|
|
self.is_default = is_default
|
|
|
self.compress = compress
|
|
|
@@ -298,6 +319,7 @@ class CloningOperation:
|
|
|
"--skip-add-drop-table",
|
|
|
"--skip-add-locks",
|
|
|
"--skip-comments",
|
|
|
+ "--column-statistics=0"
|
|
|
]
|
|
|
|
|
|
if self.compress:
|
|
|
@@ -344,7 +366,22 @@ class CloningOperation:
|
|
|
return cmd
|
|
|
|
|
|
@staticmethod
|
|
|
- def _run_piped_processes(dump_cmd, restore_cmd, tbl_count):
|
|
|
+ def _clean_sql(bin_cmd, encoding):
|
|
|
+ """ clean some old sql declaration from mysql 5 in order to preserve
|
|
|
+ a compatibility between servers"""
|
|
|
+ cmd = bin_cmd.decode('latin')
|
|
|
+
|
|
|
+ # To ensure compatibility between mysql5 and 8+
|
|
|
+ cmd = re.sub(",?NO_AUTO_CREATE_USER", "", cmd)
|
|
|
+
|
|
|
+ return cmd.encode('latin')
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _run_piped_processes(
|
|
|
+ dump_cmd,
|
|
|
+ restore_cmd,
|
|
|
+ tbl_count,
|
|
|
+ encoding):
|
|
|
""" Run the dump and the restore commands by piping them
|
|
|
The output of the mysqldump process is piped into the input of the mysql one
|
|
|
"""
|
|
|
@@ -358,7 +395,9 @@ class CloningOperation:
|
|
|
with Popen(restore_cmd, stdin=PIPE, stdout=mysql_handler, stderr=mysql_handler) as mysql:
|
|
|
# noinspection PyTypeChecker
|
|
|
with Popen(dump_cmd, stdout=PIPE, stderr=mysqldump_handler) as mysqldump:
|
|
|
- mysql.stdin.write(mysqldump.stdout.read())
|
|
|
+ cmd = mysqldump.stdout.read()
|
|
|
+ cmd = CloningOperation._clean_sql(cmd, encoding)
|
|
|
+ mysql.stdin.write(cmd)
|
|
|
|
|
|
if mysqldump.returncode:
|
|
|
raise RuntimeError('mysqldump returned a non zero code')
|
|
|
@@ -390,6 +429,18 @@ class CloningOperation:
|
|
|
self.to_server.connect()
|
|
|
logger.debug('Connected to %s', self.to_server)
|
|
|
|
|
|
+ # Create admin users if not exist
|
|
|
+ for user in self.grant:
|
|
|
+ exists = self.to_server.exec_query(
|
|
|
+ f"SELECT count(*) FROM mysql.user WHERE User = '{user.username}' and Host='{user.host}';"
|
|
|
+ ).fetchone()[0] > 0
|
|
|
+
|
|
|
+ if not exists:
|
|
|
+ logger.info(f'Create user %s@%s on %s', user.username, user.host, self.to_server)
|
|
|
+ self.to_server.exec_query(
|
|
|
+ f"CREATE USER '{user.username}'@'{user.host}' IDENTIFIED BY '{user.pwd}';")
|
|
|
+
|
|
|
+ # List tables
|
|
|
tables = {}
|
|
|
for tname in self.from_server.list_tables():
|
|
|
if any(rx.match(tname) for rx in self.ignore_tables):
|
|
|
@@ -422,15 +473,39 @@ class CloningOperation:
|
|
|
self.to_server.exec_query(f"CREATE SCHEMA `{self.dbname}`;")
|
|
|
self.to_server.set_active_db(self.dbname)
|
|
|
|
|
|
+ # Following is to avoid conflict between mysql 5 and mysql 8+
|
|
|
+ # (@see https://stackoverflow.com/questions/50336378/variable-sql-mode-cant-be-set-to-the-value-of-no-auto-create-user)
|
|
|
+ self.to_server.exec_query(f"SET GLOBAL log_bin_trust_function_creators = 1;")
|
|
|
+
|
|
|
+ # Grant admin users if any
|
|
|
+ for user in self.grant:
|
|
|
+ self.to_server.exec_query(
|
|
|
+ f"GRANT ALL ON {self.dbname}.* TO '{user.username}'@'{user.host}';"
|
|
|
+ )
|
|
|
+
|
|
|
+ # charsets
|
|
|
+ charset = self.from_server.get_db_charset(self.dbname)
|
|
|
+ encoding = CHARSET_TO_ENCODING[charset]
|
|
|
+
|
|
|
# Run mysqldump
|
|
|
try:
|
|
|
if dump_structure_for:
|
|
|
logger.info(f"Cloning structure for {len(dump_structure_for)} tables (on {len(tables)})...")
|
|
|
- self._run_piped_processes(dump_structure_cmd, restore_cmd, len(dump_structure_for))
|
|
|
+ self._run_piped_processes(
|
|
|
+ dump_structure_cmd,
|
|
|
+ restore_cmd,
|
|
|
+ len(dump_structure_for),
|
|
|
+ encoding
|
|
|
+ )
|
|
|
|
|
|
if dump_data_for:
|
|
|
logger.info(f"Cloning data for {len(dump_data_for)} tables (on {len(tables)})...")
|
|
|
- self._run_piped_processes(dump_data_cmd, restore_cmd, len(dump_data_for))
|
|
|
+ self._run_piped_processes(
|
|
|
+ dump_data_cmd,
|
|
|
+ restore_cmd,
|
|
|
+ len(dump_data_for),
|
|
|
+ encoding
|
|
|
+ )
|
|
|
|
|
|
logger.info(f"Cloning views...")
|
|
|
self.from_server.set_active_db(self.dbname)
|
|
|
@@ -466,6 +541,8 @@ def main(settings, arguments):
|
|
|
|
|
|
# Load the servers' configuration
|
|
|
servers = {}
|
|
|
+ if 'servers' not in settings:
|
|
|
+ raise RuntimeError(f'Missing section in settings.yml: {servers}')
|
|
|
for server_name, server_settings in settings['servers'].items():
|
|
|
hostname = server_settings['host']
|
|
|
|
|
|
@@ -488,15 +565,27 @@ def main(settings, arguments):
|
|
|
|
|
|
servers[server_name] = server
|
|
|
|
|
|
+ # Load the users' configuration
|
|
|
+ users = {}
|
|
|
+ for username, args in settings.get('users', {}).items():
|
|
|
+ host = args.get('host', 'localhost')
|
|
|
+ pwd = args.get('pwd', '')
|
|
|
+ users[username] = MysqlUser(username, pwd, host)
|
|
|
+
|
|
|
# Load the cloning ops' configuration
|
|
|
ops = {}
|
|
|
+ if 'operations' not in settings:
|
|
|
+ raise RuntimeError(f'Missing section in settings.yml: {servers}')
|
|
|
for name, args in settings['operations'].items():
|
|
|
dbname = args['dbname']
|
|
|
from_server = servers[args['from_server']]
|
|
|
to_server = servers[args['to_server']]
|
|
|
- kwargs = {k: v for k, v in args.items() if k not in ('dbname', 'from_server', 'to_server')}
|
|
|
+ grant = args.get('grant', [])
|
|
|
+ admins = [user for username, user in users.items() if username in grant]
|
|
|
+ kwargs = {k: v for k, v in args.items() \
|
|
|
+ if k not in ('dbname', 'from_server', 'to_server', 'grant')}
|
|
|
|
|
|
- op = CloningOperation(name, dbname, from_server, to_server, **kwargs)
|
|
|
+ op = CloningOperation(name, dbname, from_server, to_server, admins, **kwargs)
|
|
|
ops[name] = op
|
|
|
|
|
|
# Operations to launch
|
|
|
@@ -526,6 +615,10 @@ def main(settings, arguments):
|
|
|
return
|
|
|
logger.debug('> User confirmed')
|
|
|
|
|
|
+ # Create the user if they do not exist
|
|
|
+ # CREATE USER IF NOT EXISTS 'user'@'localhost' IDENTIFIED BY 'password';
|
|
|
+ # GRANT ALL ON opentalent TO 'opentalent'@'localhost';
|
|
|
+
|
|
|
# Run the cloning operations
|
|
|
for op in selected_ops:
|
|
|
op.run()
|