123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- """
- Frank-ci - A simple CI for Pythonistas, based on Flask + Huey.
- For more information see the docs/frank.rst
- """
- import hmac
- import os
- import subprocess
- import subprocess as sp
- import logging
- import hashlib
- import yaml
- import click
- from flask import Flask, request, abort
- import conf
- from shell import Shell
- from collections import OrderedDict, namedtuple
- import importlib
- from conf import taskq
- import types
- import pickle
- # monkey patch data store
- _list = "select * FROM {0}"
- def list_results(obj):
- with obj._db.get_connection() as conn:
- try:
- return list(conn.execute(obj._list.format(obj.name)))
- except:
- return None
- taskq.result_store._list = _list
- taskq.result_store.list_results = types.MethodType(list_results,
- taskq.result_store)
- PythonCode = namedtuple('PythonCode', ['path', 'args', 'kwargs', 'code'])
- def override_run(self, command, **kwargs):
- """
- Override Shell.run to handle exceptions and accept kwargs
- that Popen accepts
- """
- self.last_command = command
- command_bits = self._split_command(command)
- _kwargs = {
- 'stdout': subprocess.PIPE,
- 'stderr': subprocess.PIPE,
- 'universal_newlines': True,
- }
- if kwargs:
- for kw in kwargs:
- _kwargs[kw] = kwargs[kw]
- _kwargs['shell'] = True
- if self.has_input:
- _kwargs['stdin'] = subprocess.PIPE
- try:
- self._popen = subprocess.Popen(
- command_bits,
- **_kwargs
- )
- except Exception as E:
- self.exception = E
- return self
- self.pid = self._popen.pid
- if not self.has_input:
- self._communicate()
- return self
- Shell.run = override_run
- def ordered_load(stream, Loader=yaml.Loader, selfect_pairs_hook=OrderedDict):
- class OrderedLoader(Loader):
- pass
- def construct_mapping(loader, node):
- loader.flatten_mapping(node)
- return selfect_pairs_hook(loader.construct_pairs(node))
- OrderedLoader.add_constructor(
- yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
- construct_mapping)
- return yaml.load(stream, OrderedLoader)
- app = Flask(__name__)
- app.config.from_object(conf)
- def parse_branch_gh(request_json):
- """
- parse the branch to clone from a github payload
- "ref": "refs/heads/develop", -> should return develop
- """
- return request_json['ref'].split('/')[-1]
- def parse_yaml(clone_dest):
- os.chdir(clone_dest)
- if os.path.exists('.frank.yaml'):
- with open('.frank.yaml') as f:
- y = ordered_load(f, yaml.SafeLoader)
- return y
- def load_actions(yaml):
- pass
- def report_success(results):
- pass
- def report_failure(results):
- pass
- def run_action(axn):
- results = []
- # run shell or python callable object without arguments
- if isinstance(axn, list):
- if axn[0] == 'shell':
- for cmd in axn[1:]:
- sh = Shell()
- assert isinstance(cmd, str)
- sh.run(cmd)
- results.append(sh)
- if sh.code:
- break
- if axn[0] == 'python':
- for func in axn[1:]:
- mod, f = func.split(':')
- mod = importlib.import_module(mod)
- f = getattr(mod, f)
- res = f()
- results.append(PythonCode(func, None, None, res))
- # run shell or python callable object arguments
- elif isinstance(axn, OrderedDict):
- if 'shell' in axn:
- sh = Shell()
- cmd = axn['shell'].pop('cmd')
- assert isinstance(cmd, str)
- kwargs = axn['shell']
- sh.run(cmd, **kwargs)
- results.append(sh)
- if 'python' in axn:
- callables = axn['python']
- for func in callables:
- mod, f = func.split(':')
- mod = importlib.import_module(mod)
- try:
- f = getattr(mod, f)
- res = f()
- except AttributeError as E:
- res = E
- results.append(PythonCode(func, None, None, res))
- return results
- def clone(clone_url, branch, depth=1):
- cmd = ('git clone --depth={d} -b {branch} --single-branch '
- '{git_url} {dir}'.format(d=depth, branch=branch,
- git_url=clone_url, dir=branch))
- pull = sp.Popen(cmd, stderr=sp.STDOUT, shell=True)
- out, err = pull.communicate()
- return out, err
- @taskq.task()
- def count_beans():
- print "count_12"
- return "count_12"
- @taskq.task()
- def build_task(request_json):
- """
- . clone the latest commit (--depth 1)
- . parse yaml config
- . for each command in the config
- . run command
- . report success or failure
- """
- clone_dest = parse_branch_gh(request_json)
- repo_name = request_as_json["repository"]['name']
- try:
- o, e = clone(request_as_json['repository']['ssh_url'], clone_dest)
- except Exception as E:
- print E, E.message
- # parse yaml is still very crude ...
- # it could yield selfect with a run method
- # thus:
- # for action in parse_yaml(clone_dest):
- # action.run()
- #
- # this should also handle dependencies,
- # the following implementation is very crud
- failed = None
- for action in parse_yaml(clone_dest):
- # if config says we use huey, we should modiy run_action
- results = run_action(action)
- if any([result.code for result in results]):
- report_failure(results)
- else:
- report_success(results)
- @app.route('/beans')
- def do_beans():
- a = count_beans()
- ans = a.get(blocking=True)
- return ans
- @app.route('/results')
- def show_resutls():
- """
- TODO: Make a nicer web page with jinja2
- """
- res = {r[1]:r[2] for r in taskq.result_store.list_results()}
- return ''.join(['<p>'+str(k) + ': ' + str(pickle.loads(v)) + '</p>\n' for
- (k, v) in res.iteritems()])
- @app.route('/', methods=['POST'])
- def start():
- """
- main logic:
- 1. listen to post
- 2. if authenticated post do:
- enqueue task to build or test the code
- # This is authentication for github only
- # We could\should check for other hostings
- """
- ans = hmac.new(app.config['POST_KEY'], request.data,
- hashlib.sha1).hexdigest()
- secret = request.headers[app.config['SECRET_KEY_NAME']].split('=')[-1]
- if ans != secret:
- return abort(500)
- request_as_json = request.get_json()
- build_task(request_as_json)
- return "OK"
- @app.route('/status')
- def status():
- return "Frank is alive\n"
- @click.group()
- def cli():
- pass
- @cli.command('web', short_help='start the web service')
- @click.option('--port','-p', default=8080)
- @click.option('--debug', default=False, is_flag=True)
- def web(port, debug):
- click.echo("DEBUG: %s" % debug)
- app.run(host='0.0.0.0',port=port, debug=debug)
- @cli.command(context_settings=dict(
- ignore_unknown_options=True,
- allow_extra_args=True))
- @click.argument('worker_args', nargs=-1, type=click.UNPROCESSED)
- def worker(worker_args, short_help='start the consumer of tasks'):
- from huey.bin.huey_consumer import (get_option_parser, Consumer,
- setup_logger, RotatingFileHandler)
- from conf import taskq
- parser = get_option_parser()
- opts, args = parser.parse_args(list(worker_args))
- setup_logger(logging.INFO, opts.logfile)
- consumer = Consumer(taskq, 2, opts.periodic, opts.initial_delay,
- opts.backoff, opts.max_delay, opts.utc,
- opts.scheduler_interval, opts.periodic_task_interval)
- consumer.run()
|