clonedb.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. """
  2. Script de clonage des bases de données mariaDb depuis le
  3. serveur de production vers le serveur local
  4. (requiert python 3.6+)
  5. > Configuration: settings.yml
  6. Usage:
  7. clonedb.py [-v] [-y] [<dbname>]
  8. clonedb.py (-h | --help)
  9. clonedb.py --version
  10. Options:
  11. -y, --yes Do not ask for confirmation
  12. -h --help Show this screen.
  13. --version Show version.
  14. @author: olivier.massot, 05-2020
  15. """
  16. import logging
  17. import subprocess
  18. import sys
  19. import yaml
  20. from docopt import docopt
  21. from path import Path
  22. import logging_
  23. from locker import Lockfile, AlreadyRunning
  24. from mysql import MySqlServer
  25. __VERSION__ = "0.1"
  26. HERE = Path(__file__).parent
  27. # Start logger
  28. logger = logging.getLogger('clonedb')
  29. logging_.start("clonedb", replace=True)
  30. # Load settings
  31. with open(HERE / 'settings.yml', 'r') as f:
  32. SETTINGS = yaml.load(f, Loader=yaml.FullLoader)
  33. # FIX the default ascii encoding on some linux dockers...
  34. sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf8', buffering=1)
  35. # Utilities
  36. def _format_cmd(cmd):
  37. if type(cmd) is list:
  38. return " ".join(cmd)
  39. return cmd
  40. def _run_process(cmds):
  41. logger.debug("Run: %s", " ".join(map(str, cmds)))
  42. logpipe = logging_.LogPipe('clonedb')
  43. try:
  44. # noinspection PyTypeChecker
  45. p = subprocess.run(cmds, shell=False, stdin=sys.stdin, stdout=logpipe, stderr=logpipe)
  46. if p.returncode != 0:
  47. logger.error("Process was terminated by signal %s", p.returncode)
  48. raise RuntimeError()
  49. except (OSError, RuntimeError) as e:
  50. logger.error("Execution failed: %s", e)
  51. raise RuntimeError(f"An error happened at runtime: {e}")
  52. finally:
  53. logpipe.close()
  54. class SshTunnel:
  55. def __init__(self, host, port=22, user="root", key_file="~/.ssh/id_rsa"):
  56. self.host = host
  57. self.port = port
  58. self.user = user
  59. self.key_file = key_file
  60. def call(self, cmd):
  61. ssh_call = ["ssh",
  62. "-i", self.key_file,
  63. "-p", str(self.port),
  64. "-o", "StrictHostKeyChecking=no",
  65. f"{self.user}@{self.host}",
  66. ]
  67. ssh_cmd = ssh_call + cmd
  68. _run_process(ssh_cmd)
  69. def clonedb(ssh_tunnel, remote_mysql, local_mysql, dbname, options):
  70. logger.info(f"*** Cloning {dbname} ***")
  71. logger.debug(f"From {remote_mysql}")
  72. logger.debug(f"To {local_mysql}")
  73. dump_cmd = ["mysqldump",
  74. "--single-transaction",
  75. "-u", remote_mysql.username,
  76. f"--password={remote_mysql.password}",
  77. "--add-drop-database",
  78. "--compress",
  79. dbname]
  80. restore_cmd = ["mysql",
  81. "-h", local_mysql.host,
  82. "-P", str(local_mysql.port),
  83. "-u", local_mysql.username,
  84. f"--password={local_mysql.password}",
  85. "-D", dbname]
  86. cloning_cmd = dump_cmd + ['|'] + restore_cmd
  87. try:
  88. ssh_tunnel.call(cloning_cmd)
  89. logger.info("> the database was successfully cloned")
  90. except RuntimeError:
  91. logger.error(f"<!> An error happened while cloning the '{dbname}' database")
  92. def ask_confirmation(msg):
  93. logger.debug('Ask for confirmation...')
  94. msg += "\nWould you like to continue? (yes/no)"
  95. while 1:
  96. answer = input(msg)
  97. if answer in ('oui', 'yes', 'y', 'o'):
  98. logger.debug(f"> user confirmed by answering '{answer}'")
  99. return True
  100. elif answer in ('non', 'no', 'n'):
  101. logger.debug(f"> user cancelled by answering '{answer}'")
  102. return False
  103. else:
  104. msg = "The answer could'nt be understood. Continue? (yes/no)"
  105. def main(dbnames, prompt=True):
  106. ssh_tunnel = SshTunnel(**SETTINGS['ssh'])
  107. remote_mysql = MySqlServer(**SETTINGS['remote'])
  108. local_mysql = MySqlServer(**SETTINGS['local'])
  109. if prompt:
  110. # Ask for confirmation
  111. msg = f"""The followind databases will be cloned
  112. from '{remote_mysql}' to '{local_mysql}':
  113. > {', '.join(dbnames)}
  114. "WARNING: the existing local databases will be replaced"""
  115. if not ask_confirmation(msg):
  116. logger.info("-- Operation cancelled by user --")
  117. return
  118. # start to clone
  119. for dbname in dbnames:
  120. options = SETTINGS['databases'].get(dbname, None) or {}
  121. clonedb(ssh_tunnel, remote_mysql, local_mysql, dbname, options)
  122. if __name__ == '__main__':
  123. # parse CLI arguments
  124. arguments = docopt(__doc__, help=__doc__, version=__VERSION__)
  125. prompt = "--yes" in arguments
  126. dbname = arguments.get('dbname', None)
  127. print('\n')
  128. logger.info("Start db cloning utility...")
  129. logger.debug(f"Arguments: {arguments}")
  130. logger.debug(f"Settings: {SETTINGS}")
  131. # database to clone
  132. if arguments.get('<dbname>', None):
  133. dbnames = [arguments['<dbname>']]
  134. else:
  135. dbnames = list(SETTINGS['databases'])
  136. with Lockfile(path=HERE / '.clonedb.lock',
  137. on_error=lambda: logger.critical("A cloning process is already running, please wait...")):
  138. main(dbnames, prompt)