frank.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. """
  2. Frank-ci - A simple CI for Pythonistas, based on Flask + Huey.
  3. For more information see the docs/frank.rst
  4. """
  5. import hmac
  6. import os
  7. import subprocess
  8. import subprocess as sp
  9. import logging
  10. import hashlib
  11. import yaml
  12. import click
  13. from flask import Flask, request, abort
  14. import conf
  15. from shell import Shell
  16. from collections import OrderedDict, namedtuple
  17. import importlib
  18. from conf import taskq
  19. import types
  20. import pickle
  21. # monkey patch data store
  22. _list = "select * FROM {0}"
  23. def list_results(obj):
  24. with obj._db.get_connection() as conn:
  25. try:
  26. return list(conn.execute(obj._list.format(obj.name)))
  27. except:
  28. return None
  29. taskq.result_store._list = _list
  30. taskq.result_store.list_results = types.MethodType(list_results,
  31. taskq.result_store)
  32. PythonCode = namedtuple('PythonCode', ['path', 'args', 'kwargs', 'code'])
  33. def override_run(self, command, **kwargs):
  34. """
  35. Override Shell.run to handle exceptions and accept kwargs
  36. that Popen accepts
  37. """
  38. self.last_command = command
  39. command_bits = self._split_command(command)
  40. _kwargs = {
  41. 'stdout': subprocess.PIPE,
  42. 'stderr': subprocess.PIPE,
  43. 'universal_newlines': True,
  44. }
  45. if kwargs:
  46. for kw in kwargs:
  47. _kwargs[kw] = kwargs[kw]
  48. _kwargs['shell'] = True
  49. if self.has_input:
  50. _kwargs['stdin'] = subprocess.PIPE
  51. try:
  52. self._popen = subprocess.Popen(
  53. command_bits,
  54. **_kwargs
  55. )
  56. except Exception as E:
  57. self.exception = E
  58. return self
  59. self.pid = self._popen.pid
  60. if not self.has_input:
  61. self._communicate()
  62. return self
  63. Shell.run = override_run
  64. def ordered_load(stream, Loader=yaml.Loader, selfect_pairs_hook=OrderedDict):
  65. class OrderedLoader(Loader):
  66. pass
  67. def construct_mapping(loader, node):
  68. loader.flatten_mapping(node)
  69. return selfect_pairs_hook(loader.construct_pairs(node))
  70. OrderedLoader.add_constructor(
  71. yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
  72. construct_mapping)
  73. return yaml.load(stream, OrderedLoader)
  74. app = Flask(__name__)
  75. app.config.from_object(conf)
  76. def parse_branch_gh(request_json):
  77. """
  78. parse the branch to clone from a github payload
  79. "ref": "refs/heads/develop", -> should return develop
  80. """
  81. return request_json['ref'].split('/')[-1]
  82. def parse_yaml(clone_dest):
  83. os.chdir(clone_dest)
  84. if os.path.exists('.frank.yaml'):
  85. with open('.frank.yaml') as f:
  86. y = ordered_load(f, yaml.SafeLoader)
  87. return y
  88. def load_actions(yaml):
  89. pass
  90. def report_success(results):
  91. pass
  92. def report_failure(results):
  93. pass
  94. def run_action(axn):
  95. results = []
  96. # run shell or python callable object without arguments
  97. if isinstance(axn, list):
  98. if axn[0] == 'shell':
  99. for cmd in axn[1:]:
  100. sh = Shell()
  101. assert isinstance(cmd, str)
  102. sh.run(cmd)
  103. results.append(sh)
  104. if sh.code:
  105. break
  106. if axn[0] == 'python':
  107. for func in axn[1:]:
  108. mod, f = func.split(':')
  109. mod = importlib.import_module(mod)
  110. f = getattr(mod, f)
  111. res = f()
  112. results.append(PythonCode(func, None, None, res))
  113. # run shell or python callable object arguments
  114. elif isinstance(axn, OrderedDict):
  115. if 'shell' in axn:
  116. sh = Shell()
  117. cmd = axn['shell'].pop('cmd')
  118. assert isinstance(cmd, str)
  119. kwargs = axn['shell']
  120. sh.run(cmd, **kwargs)
  121. results.append(sh)
  122. if 'python' in axn:
  123. callables = axn['python']
  124. for func in callables:
  125. mod, f = func.split(':')
  126. mod = importlib.import_module(mod)
  127. try:
  128. f = getattr(mod, f)
  129. res = f()
  130. except AttributeError as E:
  131. res = E
  132. results.append(PythonCode(func, None, None, res))
  133. return results
  134. def clone(clone_url, branch, depth=1):
  135. cmd = ('git clone --depth={d} -b {branch} --single-branch '
  136. '{git_url} {dir}'.format(d=depth, branch=branch,
  137. git_url=clone_url, dir=branch))
  138. pull = sp.Popen(cmd, stderr=sp.STDOUT, shell=True)
  139. out, err = pull.communicate()
  140. return out, err
  141. @taskq.task()
  142. def count_beans():
  143. print "count_12"
  144. return "count_12"
  145. @taskq.task()
  146. def build_task(request_json):
  147. """
  148. . clone the latest commit (--depth 1)
  149. . parse yaml config
  150. . for each command in the config
  151. . run command
  152. . report success or failure
  153. """
  154. request_as_json = request.get_json()
  155. clone_dest = parse_branch_gh(request_as_json)
  156. repo_name = request_as_json["repository"]['name']
  157. try:
  158. o, e = clone(request_as_json['repository']['ssh_url'], clone_dest)
  159. except Exception as E:
  160. print E, E.message
  161. # parse yaml is still very crude ...
  162. # it could yield selfect with a run method
  163. # thus:
  164. # for action in parse_yaml(clone_dest):
  165. # action.run()
  166. #
  167. # this should also handle dependencies,
  168. # the following implementation is very crud
  169. failed = None
  170. for action in parse_yaml(clone_dest):
  171. # if config says we use huey, we should modiy run_action
  172. results = run_action(action)
  173. if any([result.code for result in results]):
  174. report_failure(results)
  175. else:
  176. report_success(results)
  177. @app.route('/beans')
  178. def do_beans():
  179. a = count_beans()
  180. ans = a.get(blocking=True)
  181. return ans
  182. @app.route('/results')
  183. def show_resutls():
  184. res = {r[1]:r[2] for r in taskq.result_store.list_results()}
  185. return ''.join(['<p>'+str(k) + ': ' + str(pickle.loads(v)) + '</p>\n' for
  186. (k, v) in res.iteritems()])
  187. @app.route('/', methods=['POST'])
  188. def start():
  189. """
  190. main logic:
  191. 1. listen to post
  192. 2. if authenticated post do:
  193. enqueue task to build or test the code
  194. # This is authentication for github only
  195. # We could\should check for other hostings
  196. """
  197. ans = hmac.new(app.config['POST_KEY'], request.data,
  198. hashlib.sha1).hexdigest()
  199. secret = request.headers['X-Hub-Signature'].split('=')[-1]
  200. if ans != secret:
  201. return abort(500)
  202. request_as_json = request.get_json()
  203. build_task(request_as_json)
  204. return "OK"
  205. @click.group()
  206. def cli():
  207. pass
  208. @cli.command('web', short_help='start the web service')
  209. @click.option('--port','-p', default=8080)
  210. @click.option('--debug', default=False, is_flag=True)
  211. def web(port, debug):
  212. click.echo("DEBUG: %s" % debug)
  213. app.run(host='0.0.0.0',port=port, debug=debug)
  214. @cli.command(context_settings=dict(
  215. ignore_unknown_options=True,
  216. allow_extra_args=True))
  217. @click.argument('worker_args', nargs=-1, type=click.UNPROCESSED)
  218. def worker(worker_args, short_help='start the consumer of tasks'):
  219. from huey.bin.huey_consumer import (get_option_parser, Consumer,
  220. setup_logger, RotatingFileHandler)
  221. from conf import taskq
  222. parser = get_option_parser()
  223. opts, args = parser.parse_args(list(worker_args))
  224. setup_logger(logging.INFO, opts.logfile)
  225. consumer = Consumer(taskq, 2, opts.periodic, opts.initial_delay,
  226. opts.backoff, opts.max_delay, opts.utc,
  227. opts.scheduler_interval, opts.periodic_task_interval)
  228. consumer.run()