clonedb.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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. -v, --verbose Displays more informations
  12. -y, --yes Do not ask for confirmation
  13. -h --help Show this screen.
  14. --version Show version.
  15. @author: olivier.massot, 05-2020
  16. """
  17. import logging
  18. import sys
  19. import mysql.connector
  20. import yaml
  21. from docopt import docopt
  22. from path import Path
  23. import logging_
  24. __VERSION__ = "0.1"
  25. HERE = Path(__file__).parent
  26. LOCKFILE = HERE / '.clonedb.lock'
  27. with open(HERE / 'settings.yml', 'r') as f:
  28. SETTINGS = yaml.load(f, Loader=yaml.FullLoader)
  29. class MySqlServer:
  30. def __init__(self, host, port, username, password, description=""):
  31. self.host = host
  32. self.port = port
  33. self.username = username
  34. self.password = password
  35. self.description = description or "no description"
  36. self.cnn = None
  37. def __repr__(self):
  38. return f"{self.username}@{self.host}:{self.port} ({self.description})"
  39. def connect(self):
  40. logger.debug(f'Try to connect to {self}')
  41. self.cnn = mysql.connector.connect(
  42. host=self.host,
  43. port=self.port,
  44. user=self.username,
  45. passwd=self.password
  46. )
  47. def db_exists(self, dbname):
  48. cursor = self.cnn.cursor()
  49. cursor.execute(f"""SELECT SCHEMA_NAME
  50. FROM INFORMATION_SCHEMA.SCHEMATA
  51. WHERE SCHEMA_NAME = '{dbname}'""")
  52. row = cursor.fetchone()
  53. return row is not None
  54. def clonedb(from_server, to_server, dbname):
  55. logger.info(f"*** Cloning {dbname} ***")
  56. logger.debug(f"From {from_server}")
  57. logger.debug(f"To {to_server}")
  58. to_server.cnn.cmd_query('DROP DATABASE {dbname};')
  59. to_server.cnn.cmd_query('CREATE DATABASE {dbname};')
  60. dump_cmd = "mysqldump --single-transaction -u ${USERDBROOTREMOTE} --password=${PASSDBROOTREMOTE} $1"
  61. ssh_dump_cmd = f"ssh -i {SSHEXPLOITATIONKEY} -p {PORT} -C exploitation@${IPPROD} {dump_cmd}"
  62. restore_cmd = "mysql -h ${3} -P ${4} -u ${USERDBROOT} --password=${PASSDBROOT} -D $2"
  63. cmd = f"{ssh_dump_cmd} | {restore_cmd}"
  64. cmd = "ssh -i ${SSHEXPLOITATIONKEY} -p ${PORT} -C exploitation@${IPPROD} mysqldump --single-transaction -u ${USERDBROOTREMOTE} --password=${PASSDBROOTREMOTE} $1 | mysql -h ${3} -P ${4} -u ${USERDBROOT} --password=${PASSDBROOT} -D $2"
  65. if __name__ == '__main__':
  66. arguments = docopt(__doc__, help=__doc__, version=__VERSION__)
  67. verbose = '--verbose' in arguments
  68. logger = logging.getLogger('clonedb')
  69. logging_.start("clonedb", logging.DEBUG if verbose else logging.INFO, replace=True)
  70. if LOCKFILE.exists():
  71. logger.critical("Une opération de clonage est déjà en cours. "
  72. "veuillez patienter ou annuler le traitement existant")
  73. sys.exit(1)
  74. logger.debug(f"Arguments given: {arguments}")
  75. logger.debug(f"Settings: {SETTINGS}")
  76. if verbose:
  77. logger.debug("Mode: Verbose")
  78. remote_server = MySqlServer(**SETTINGS['remote'])
  79. remote_server.connect()
  80. local_server = MySqlServer(**SETTINGS['local'])
  81. local_server.connect()
  82. if arguments['dbname']:
  83. dbnames = [arguments['dbname']]
  84. else:
  85. dbnames = [db for db in SETTINGS['databases']]
  86. # Demande confirmation
  87. if not '--yes' in arguments:
  88. logger.debug('Ask for confirmation...')
  89. answer = ""
  90. msg = f"Les bases de données suivantes vont être clonées depuis " \
  91. f"'{remote_server} vers '{local_server}':\n' " \
  92. f"> {', '.join(dbnames)} \n" \
  93. "ATTENTION: Les bases existantes seront remplacées.\n" \
  94. "Voulez vous continuer? (oui/non)"
  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. break
  100. elif answer in ('non', 'no', 'n'):
  101. logger.info("-- Opération annulée par l'utilisateur --")
  102. sys.exit(1)
  103. else:
  104. msg = "La réponse n'a pas été comprise. Voulez vous continuer? (oui/non)"
  105. # check for databases existence
  106. missing_db = [dbname for dbname in dbnames if not remote_server.db_exists(dbname)]
  107. if missing_db:
  108. for missing in missing_db:
  109. logger.critical(
  110. f"<!> Aucune base de donnée nommée '{missing}' trouvée sur {remote_server}")
  111. logger.critical("-- Opération annulée --")
  112. sys.exit(1)
  113. # start to clone
  114. try:
  115. LOCKFILE.touch()
  116. for dbname in dbnames:
  117. clonedb(remote_server, local_server, dbname)
  118. logger.info("-- Clonage des bases de données terminé --")
  119. finally:
  120. LOCKFILE.remove()