Guest User

Untitled

a guest
Apr 6th, 2018
125
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.07 KB | None | 0 0
  1. #!/usr/bin/python3
  2. import argparse
  3. import logging
  4. import subprocess
  5. import os
  6. import tempfile
  7. from tempfile import mkstemp
  8.  
  9. import configparser
  10. import gzip
  11. import boto3
  12. import psycopg2
  13. from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
  14.  
  15. # Amazon S3 settings.
  16. # AWS_ACCESS_KEY_ID in ~/.aws/credentials
  17. # AWS_SECRET_ACCESS_KEY in ~/.aws/credentials
  18. import datetime
  19.  
  20. from shutil import move
  21.  
  22. AWS_BUCKET_NAME = 'backup.mydomain.com'
  23. AWS_BUCKET_PATH = 'postgres/'
  24. BACKUP_PATH = '/tmp/'
  25.  
  26.  
  27. def upload_to_s3(file_full_path, dest_file):
  28. """
  29. Upload a file to an AWS S3 bucket.
  30. """
  31. s3_client = boto3.client('s3')
  32. try:
  33. s3_client.upload_file(file_full_path, AWS_BUCKET_NAME, AWS_BUCKET_PATH + dest_file)
  34. os.remove(file_full_path)
  35. except boto3.exceptions.S3UploadFailedError as exc:
  36. print(exc)
  37. exit(1)
  38.  
  39.  
  40. def download_from_s3(backup_s3_key, dest_file):
  41. """
  42. Upload a file to an AWS S3 bucket.
  43. """
  44. s3_client = boto3.resource('s3')
  45. try:
  46. s3_client.meta.client.download_file(AWS_BUCKET_NAME, backup_s3_key, dest_file)
  47. except Exception as e:
  48. print(e)
  49. exit(1)
  50.  
  51.  
  52. def list_available_backup():
  53. key_list = []
  54. s3_client = boto3.client('s3')
  55. s3_objects = s3_client.list_objects_v2(Bucket=AWS_BUCKET_NAME, Prefix=AWS_BUCKET_PATH)
  56.  
  57. for key in s3_objects['Contents']:
  58. key_list.append(key['Key'])
  59. return key_list
  60.  
  61.  
  62. def list_postgres_databases(host, database_name, port, user, password):
  63. try:
  64. process = subprocess.Popen(
  65. ['psql',
  66. '--dbname=postgresql://{}:{}@{}:{}/{}'.format(user, password, host, port, database_name),
  67. '--list'],
  68. stdout=subprocess.PIPE
  69. )
  70. output = process.communicate()[0]
  71. if int(process.returncode) != 0:
  72. print('Command failed. Return code : {}'.format(process.returncode))
  73. exit(1)
  74. return output
  75. except Exception as e:
  76. print(e)
  77. exit(1)
  78.  
  79.  
  80. def backup_postgres_db(host, database_name, port, user, password, dest_file, verbose):
  81. """
  82. Backup postgres db to a file.
  83. """
  84. if verbose:
  85. try:
  86. process = subprocess.Popen(
  87. ['pg_dump',
  88. '--dbname=postgresql://{}:{}@{}:{}/{}'.format(user, password, host, port, database_name),
  89. '-Fc',
  90. '-f', dest_file,
  91. '-v'],
  92. stdout=subprocess.PIPE
  93. )
  94. output = process.communicate()[0]
  95. if int(process.returncode) != 0:
  96. print('Command failed. Return code : {}'.format(process.returncode))
  97. exit(1)
  98. return output
  99. except Exception as e:
  100. print(e)
  101. exit(1)
  102. else:
  103.  
  104. try:
  105. process = subprocess.Popen(
  106. ['pg_dump',
  107. '--dbname=postgresql://{}:{}@{}:{}/{}'.format(user, password, host, port, database_name),
  108. '-f', dest_file],
  109. stdout=subprocess.PIPE
  110. )
  111. output = process.communicate()[0]
  112. if process.returncode != 0:
  113. print('Command failed. Return code : {}'.format(process.returncode))
  114. exit(1)
  115. return output
  116. except Exception as e:
  117. print(e)
  118. exit(1)
  119.  
  120.  
  121. def compress_file(src_file):
  122. compressed_file = "{}.gz".format(str(src_file))
  123. with open(src_file, 'rb') as f_in:
  124. with gzip.open(compressed_file, 'wb') as f_out:
  125. for line in f_in:
  126. f_out.write(line)
  127. return compressed_file
  128.  
  129.  
  130. def extract_file(src_file):
  131. extracted_file, extension = os.path.splitext(src_file)
  132. print(extracted_file)
  133. with gzip.open(src_file, 'rb') as f_in:
  134. with open(extracted_file, 'wb') as f_out:
  135. for line in f_in:
  136. f_out.write(line)
  137. return extracted_file
  138.  
  139. def remove_faulty_statement_from_dump(src_file):
  140.  
  141. temp_file, _ = tempfile.mkstemp()
  142.  
  143. try:
  144. with open(temp_file, 'w+') as dump_temp:
  145. process = subprocess.Popen(
  146. ['pg_restore',
  147. '-l'
  148. '-v',
  149. src_file],
  150. stdout=subprocess.PIPE
  151. )
  152. output = subprocess.check_output(('grep','-v','"EXTENSION - plpgsql"'), stdin=process.stdout)
  153. process.wait()
  154. if int(process.returncode) != 0:
  155. print('Command failed. Return code : {}'.format(process.returncode))
  156. exit(1)
  157.  
  158. os.remove(src_file)
  159. with open(src_file, 'w+') as cleaned_dump:
  160. subprocess.call(
  161. ['pg_restore',
  162. '-L'],
  163. stdin=output,
  164. stdout=cleaned_dump
  165. )
  166.  
  167. except Exception as e:
  168. print("Issue when modifying dump : {}".format(e))
  169.  
  170.  
  171. def change_user_from_dump(source_dump_path, old_user, new_user):
  172. fh, abs_path = mkstemp()
  173. with os.fdopen(fh, 'w') as new_file:
  174. with open(source_dump_path) as old_file:
  175. for line in old_file:
  176. new_file.write(line.replace(old_user, new_user))
  177. # Remove original file
  178. os.remove(source_dump_path)
  179. # Move new file
  180. move(abs_path, source_dump_path)
  181.  
  182.  
  183. def restore_postgres_db(db_host, db, port, user, password, backup_file, verbose):
  184. """
  185. Restore postgres db from a file.
  186. """
  187.  
  188. if verbose:
  189. try:
  190. print(user,password,db_host,port, db)
  191. process = subprocess.Popen(
  192. ['pg_restore',
  193. '--no-owner',
  194. '--dbname=postgresql://{}:{}@{}:{}/{}'.format(user,
  195. password,
  196. db_host,
  197. port, db),
  198. '-v',
  199. backup_file],
  200. stdout=subprocess.PIPE
  201. )
  202. output = process.communicate()[0]
  203. if int(process.returncode) != 0:
  204. print('Command failed. Return code : {}'.format(process.returncode))
  205.  
  206. return output
  207. except Exception as e:
  208. print("Issue with the db restore : {}".format(e))
  209. else:
  210. try:
  211. process = subprocess.Popen(
  212. ['pg_restore',
  213. '--no-owner',
  214. '--dbname=postgresql://{}:{}@{}:{}/{}'.format(user,
  215. password,
  216. db_host,
  217. port, db),
  218. backup_file],
  219. stdout=subprocess.PIPE
  220. )
  221. output = process.communicate()[0]
  222. if int(process.returncode) != 0:
  223. print('Command failed. Return code : {}'.format(process.returncode))
  224.  
  225. return output
  226. except Exception as e:
  227. print("Issue with the db restore : {}".format(e))
  228.  
  229.  
  230. def create_db(db_host, database, db_port, user_name, user_password):
  231. try:
  232. con = psycopg2.connect(dbname='postgres', port=db_port,
  233. user=user_name, host=db_host,
  234. password=user_password)
  235.  
  236. except Exception as e:
  237. print(e)
  238. exit(1)
  239.  
  240. con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
  241. cur = con.cursor()
  242. try:
  243. cur.execute("DROP DATABASE {} ;".format(database))
  244. except Exception as e:
  245. print('DB does not exist, nothing to drop')
  246. cur.execute("CREATE DATABASE {} ;".format(database))
  247. cur.execute("GRANT ALL PRIVILEGES ON DATABASE {} TO {} ;".format(database, user_name))
  248. return database
  249.  
  250.  
  251. def swap_restore_active(db_host, restore_database, active_database, db_port, user_name, user_password):
  252. try:
  253. con = psycopg2.connect(dbname='postgres', port=db_port,
  254. user=user_name, host=db_host,
  255. password=user_password)
  256. con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
  257. cur = con.cursor()
  258. cur.execute("SELECT pg_terminate_backend( pid ) "
  259. "FROM pg_stat_activity "
  260. "WHERE pid <> pg_backend_pid( ) "
  261. "AND datname = '{}'".format(active_database))
  262. cur.execute("DROP DATABASE {}".format(active_database))
  263. cur.execute('ALTER DATABASE "{}" RENAME TO "{}";'.format(restore_database, active_database))
  264. except Exception as e:
  265. print(e)
  266. exit(1)
  267.  
  268. def swap_restore_new(db_host, restore_database, new_database, db_port, user_name, user_password):
  269. try:
  270. con = psycopg2.connect(dbname='postgres', port=db_port,
  271. user=user_name, host=db_host,
  272. password=user_password)
  273. con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
  274. cur = con.cursor()
  275. cur.execute('ALTER DATABASE "{}" RENAME TO "{}";'.format(restore_database, new_database))
  276. except Exception as e:
  277. print(e)
  278. exit(1)
  279.  
  280.  
  281. def main():
  282. logger = logging.getLogger(__name__)
  283. logger.setLevel(logging.INFO)
  284. handler = logging.StreamHandler()
  285. formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  286. handler.setFormatter(formatter)
  287. logger.addHandler(handler)
  288. args_parser = argparse.ArgumentParser(description='Postgres database management')
  289. args_parser.add_argument("--action",
  290. metavar="action",
  291. choices=['list', 'list_dbs', 'restore', 'backup'],
  292. required=True)
  293. args_parser.add_argument("--date",
  294. metavar="YYYY-MM-dd",
  295. help="Date to use for restore (show with --action list)")
  296. args_parser.add_argument("--dest-db",
  297. metavar="dest_db",
  298. default=None,
  299. help="Name of the new restored database")
  300. args_parser.add_argument("--verbose",
  301. default=True,
  302. help="verbose output")
  303. args_parser.add_argument("--configfile",
  304. required=True,
  305. help="Database configuration file")
  306. args = args_parser.parse_args()
  307.  
  308. config = configparser.ConfigParser()
  309. config.read(args.configfile)
  310.  
  311. postgres_host = config.get('postgresql', 'host')
  312. postgres_port = config.get('postgresql', 'port')
  313. postgres_db = config.get('postgresql', 'db')
  314. postgres_restore = "{}_restore".format(postgres_db)
  315. postgres_user = config.get('postgresql', 'user')
  316. postgres_password = config.get('postgresql', 'password')
  317. timestr = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
  318. filename = 'backup-{}-{}.dump'.format(timestr, postgres_db)
  319. filename_compressed = '{}.gz'.format(filename)
  320. restore_filename = '/tmp/restore.dump.gz'
  321. restore_uncompressed = '/tmp/restore.dump'
  322. local_file_path = '{}{}'.format(BACKUP_PATH, filename)
  323.  
  324. # list task
  325. if args.action == "list":
  326. logger.info('Listing S3 bucket s3://{}/{} content :'.format(AWS_BUCKET_NAME,
  327. AWS_BUCKET_PATH))
  328. s3_backup_objects = list_available_backup()
  329. for key in s3_backup_objects:
  330. logger.info("Key : {}".format(key))
  331. # list databases task
  332. elif args.action == "list_dbs":
  333. result = list_postgres_databases(postgres_host,
  334. postgres_db,
  335. postgres_port,
  336. postgres_user,
  337. postgres_password)
  338. for line in result.splitlines():
  339. logger.info(line)
  340. # backup task
  341. elif args.action == "backup":
  342. logger.info('Backing up {} database to {}'.format(postgres_db, local_file_path))
  343. result = backup_postgres_db(postgres_host,
  344. postgres_db,
  345. postgres_port,
  346. postgres_user,
  347. postgres_password,
  348. local_file_path, args.verbose)
  349. for line in result.splitlines():
  350. logger.info(line)
  351.  
  352. logger.info("Backup complete")
  353. logger.info("Compressing {}".format(local_file_path))
  354. comp_file = compress_file(local_file_path)
  355. logger.info('Uploading {} to Amazon S3...'.format(comp_file))
  356. upload_to_s3(comp_file, filename_compressed)
  357. logger.info("Uploaded to {}".format(filename_compressed))
  358. # restore task
  359. elif args.action == "restore":
  360. if not args.date:
  361. logger.warn('No date was chosen for restore. Run again with the "list" '
  362. 'action to see available restore dates')
  363. else:
  364. try:
  365. os.remove(restore_filename)
  366. except Exception as e:
  367. logger.info(e)
  368. all_backup_keys = list_available_backup()
  369. backup_match = [s for s in all_backup_keys if args.date in s]
  370. if backup_match:
  371. logger.info("Found the following backup : {}".format(backup_match))
  372. else:
  373. logger.error("No match found for backups with date : {}".format(args.date))
  374. logger.info("Available keys : {}".format([s for s in all_backup_keys]))
  375. exit(1)
  376.  
  377. logger.info("Downloading {} from S3 into : {}".format(backup_match[0], restore_filename))
  378. download_from_s3(backup_match[0], restore_filename)
  379. logger.info("Download complete")
  380. logger.info("Extracting {}".format(restore_filename))
  381. ext_file = extract_file(restore_filename)
  382. # cleaned_ext_file = remove_faulty_statement_from_dump(ext_file)
  383. logger.info("Extracted to : {}".format(ext_file))
  384. logger.info("Creating temp database for restore : {}".format(postgres_restore))
  385. tmp_database = create_db(postgres_host,
  386. postgres_restore,
  387. postgres_port,
  388. postgres_user,
  389. postgres_password)
  390. logger.info("Created temp database for restore : {}".format(tmp_database))
  391. logger.info("Restore starting")
  392. result = restore_postgres_db(postgres_host,
  393. postgres_restore,
  394. postgres_port,
  395. postgres_user,
  396. postgres_password,
  397. restore_uncompressed,
  398. args.verbose)
  399. for line in result.splitlines():
  400. logger.info(line)
  401. logger.info("Restore complete")
  402. if args.dest_db is not None:
  403. restored_db_name = args.dest_db
  404. logger.info("Switching restored database with new one : {} > {}".format(
  405. postgres_restore, restored_db_name
  406. ))
  407. swap_restore_new(postgres_host,
  408. postgres_restore,
  409. restored_db_name,
  410. postgres_port,
  411. postgres_user,
  412. postgres_password)
  413. else:
  414. restored_db_name = postgres_db
  415. logger.info("Switching restored database with active one : {} > {}".format(
  416. postgres_restore, restored_db_name
  417. ))
  418. swap_restore_active(postgres_host,
  419. postgres_restore,
  420. restored_db_name,
  421. postgres_port,
  422. postgres_user,
  423. postgres_password)
  424. logger.info("Database restored and active.")
  425. else:
  426. logger.warn("No valid argument was given.")
  427. logger.warn(args)
  428.  
  429.  
  430. if __name__ == '__main__':
  431. main()
Add Comment
Please, Sign In to add comment