supervisor.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import argparse
  2. import json
  3. import logging
  4. import os.path
  5. import shutil
  6. import sys
  7. import time
  8. from terroroftinytown.release.botouploader import BotoUploaderBootstrap
  9. from terroroftinytown.release.iaupload import IAUploaderBootstrap
  10. from terroroftinytown.tracker.bootstrap import Bootstrap
  11. from terroroftinytown.tracker.export import ExporterBootstrap
  12. from terroroftinytown.tracker.model import Result
  13. logger = logging.getLogger(__name__)
  14. UPLOADER_CLASS_MAP = {
  15. 'ia': IAUploaderBootstrap,
  16. 'boto': BotoUploaderBootstrap,
  17. }
  18. def main():
  19. arg_parser = argparse.ArgumentParser()
  20. arg_parser.add_argument('config_path')
  21. arg_parser.add_argument('export_dir',
  22. default='/home/tinytown/tinytown-export/')
  23. arg_parser.add_argument('--verbose', action='store_true')
  24. arg_parser.add_argument('--uploader',
  25. choices=['ia', 'boto'], default='boto')
  26. arg_parser.add_argument('--batch-size', type=int)
  27. arg_parser.add_argument('--min-batch-size', type=int)
  28. arg_parser.add_argument('--max-batches', type=int, default=1)
  29. args = arg_parser.parse_args()
  30. if not os.path.exists(args.export_dir):
  31. os.mkdir(args.export_dir)
  32. log_filename = os.path.join(args.export_dir, 'supervisor.log')
  33. logging.basicConfig(
  34. level=logging.INFO, filename=log_filename,
  35. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  36. )
  37. if args.verbose:
  38. stream_handler = logging.StreamHandler()
  39. stream_handler.setLevel(logging.INFO)
  40. formatter = logging.Formatter('%(levelname)s:%(name)s:%(message)s')
  41. stream_handler.setFormatter(formatter)
  42. logging.getLogger().addHandler(stream_handler)
  43. logger.info('Supervisor starting up.')
  44. try:
  45. wrapper(args)
  46. except (Exception, SystemExit):
  47. logger.exception('Failure.')
  48. raise
  49. logger.info('Supervisor done.')
  50. def wrapper(args):
  51. config_path = args.config_path
  52. export_dir = args.export_dir
  53. sentinel_file = os.path.join(export_dir, 'tinytown-supervisor-sentinel')
  54. if os.path.exists(sentinel_file):
  55. raise Exception(
  56. 'The sentinel file exists. '
  57. 'Previous supervisor did not exit correctly.'
  58. )
  59. else:
  60. with open(sentinel_file, 'wb'):
  61. pass
  62. if sys.version_info[0] != 3:
  63. raise Exception('This script expects Python 3')
  64. if not os.path.isfile(config_path):
  65. raise Exception('Config path is not a file.')
  66. for batch_num in range(args.max_batches):
  67. if has_results(args):
  68. logging.info('Starting batch #%d', batch_num + 1)
  69. process_batch(args)
  70. time.sleep(2)
  71. else:
  72. logger.info('No results. Nothing to do.')
  73. break
  74. os.remove(sentinel_file)
  75. logger.info('Done')
  76. def process_batch(args):
  77. config_path = args.config_path
  78. export_dir = args.export_dir
  79. uploader_class = UPLOADER_CLASS_MAP[args.uploader]
  80. database_locked_sentinel_path = os.path.join(
  81. args.export_dir, 'database_is_locked')
  82. logger.info('Loading bootstrap.')
  83. bootstrap = Bootstrap()
  84. bootstrap.setup_args()
  85. bootstrap.parse_args(args=[config_path])
  86. bootstrap.load_config()
  87. time_struct = time.gmtime()
  88. timestamp = bootstrap.config.get(
  89. 'iaexporter', 'timestamp',
  90. ).format(
  91. year=time_struct.tm_year,
  92. month=time_struct.tm_mon,
  93. day=time_struct.tm_mday,
  94. hour=time_struct.tm_hour,
  95. minute=time_struct.tm_min,
  96. second=time_struct.tm_sec,
  97. )
  98. done_directory = os.path.join(export_dir, 'done')
  99. if not os.path.exists(done_directory):
  100. os.mkdir(done_directory)
  101. item_export_directory = os.path.join(export_dir, timestamp)
  102. logger.info('Begin export to %s.', item_export_directory)
  103. title = bootstrap.config.get('iaexporter', 'title')\
  104. .format(timestamp=timestamp)
  105. identifier = bootstrap.config.get('iaexporter', 'item')\
  106. .format(timestamp=timestamp)
  107. upload_meta_path = os.path.join(export_dir, 'current.json')
  108. upload_meta = {
  109. 'identifier': identifier,
  110. 'title': title,
  111. 'work_directory': item_export_directory,
  112. }
  113. with open(upload_meta_path, 'w') as out_file:
  114. out_file.write(json.dumps(upload_meta))
  115. os.makedirs(item_export_directory)
  116. exporter = ExporterBootstrap()
  117. exporter_args = [
  118. config_path, '--format', 'beacon',
  119. '--include-settings', '--zip',
  120. '--dir-length', '0', '--file-length', '0', '--max-right', '8',
  121. '--delete', '--zip-filename-infix', '.{}'.format(timestamp),
  122. '--database-busy-file', database_locked_sentinel_path,
  123. item_export_directory,
  124. ]
  125. if args.batch_size:
  126. exporter_args.extend(['--max-items', str(args.batch_size)])
  127. export_dir_start_size = get_dir_size(item_export_directory)
  128. exporter.start(args=exporter_args)
  129. logger.info('Export finished')
  130. export_dir_end_size = get_dir_size(item_export_directory)
  131. if export_dir_start_size == export_dir_end_size:
  132. raise Exception('Export directory size did not change: {} bytes'
  133. .format(export_dir_end_size))
  134. logger.info('Upload starting')
  135. uploader = uploader_class()
  136. args = [
  137. config_path,
  138. item_export_directory,
  139. '--title', upload_meta['title'],
  140. '--identifier', upload_meta['identifier']
  141. ]
  142. uploader.start(args=args)
  143. logger.info('Upload done.')
  144. os.remove(upload_meta_path)
  145. shutil.move(item_export_directory, done_directory)
  146. def has_results(args):
  147. bootstrap = Bootstrap()
  148. bootstrap.setup_args()
  149. bootstrap.parse_args(args=[args.config_path])
  150. bootstrap.load_config()
  151. bootstrap.setup_database()
  152. if Result.has_results():
  153. if args.min_batch_size:
  154. return Result.get_count() >= args.min_batch_size
  155. else:
  156. return True
  157. def get_dir_size(path):
  158. total = 0
  159. for root, dirs, files in os.walk(path):
  160. total += sum(os.path.getsize(os.path.join(root, name))
  161. for name in files)
  162. return total
  163. if __name__ == '__main__':
  164. main()