From 987cc8ed64e9852ffa78915781e3385d1c9dbe8b Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: May 26 2017 07:43:01 +0000 Subject: [PATCH 1/10] DB changes --- diff --git a/alembic/versions/104a0806520e_bulidbot_decoupling_step_1.py b/alembic/versions/104a0806520e_bulidbot_decoupling_step_1.py new file mode 100644 index 0000000..31fa989 --- /dev/null +++ b/alembic/versions/104a0806520e_bulidbot_decoupling_step_1.py @@ -0,0 +1,28 @@ +"""Bulidbot Decoupling - step 1 + +Revision ID: 104a0806520e +Revises: 3b51d15f0f87 +Create Date: 2017-02-22 15:39:40.333356 + +""" + +# revision identifiers, used by Alembic. +revision = '104a0806520e' +down_revision = '3b51d15f0f87' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('build_step', sa.Column('outcome', sa.Enum( + 'COMPLETED', 'FAILED', 'CRASHED', 'ABORTED', name='execoutcome'), nullable=True)) + op.add_column( + 'build_step', sa.Column('description', sa.Text(), nullable=True)) + + +def downgrade(): + op.drop_column('build_step', 'outcome') + op.drop_column('build_step', 'description') diff --git a/alembic/versions/3b7629fd849f_bulidbot_decoupling_step_2.py b/alembic/versions/3b7629fd849f_bulidbot_decoupling_step_2.py new file mode 100644 index 0000000..e2eccbe --- /dev/null +++ b/alembic/versions/3b7629fd849f_bulidbot_decoupling_step_2.py @@ -0,0 +1,90 @@ +"""Bulidbot Decoupling - step 2 + +Revision ID: 3b7629fd849f +Revises: 104a0806520e +Create Date: 2017-02-22 15:43:40.895604 + +""" + +# revision identifiers, used by Alembic. +revision = '3b7629fd849f' +down_revision = '104a0806520e' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relation, sessionmaker +from sqlalchemy.sql import text +import logging + +Session = sessionmaker() +Base = declarative_base() + + +EXEC_OUTCOME = ('COMPLETED', 'FAILED', 'CRASHED', 'ABORTED') + + +class BuildStep(Base): + __tablename__ = 'build_step' + id = sa.Column(sa.Integer, primary_key=True) + status = sa.Column(sa.String(10)) + outcome = sa.Column(sa.Enum(*EXEC_OUTCOME, name='execoutcome')) + + +OUTCOME_MAP = {'OK': 'COMPLETED', 'NOT OK': 'FAILED', 'INPROGRESS': None} +DESCRIPTION_MAP = { + "rm_tmp": "Cleaning-up temporary files", + "rm_log": "Cleaning-up log files", + "git": "Cloning the task repository", + "runtask": "Running the testsuite", + "MasterShellCommand": "Creating artifacts directory on master", + "upload": "Uploading the artifacts to master", + "MasterShellCommand_1": "Compressing artifacts", + "MasterShellCommand_2": "Publishing the artifacts", +} + + +def upgrade(): + logger = logging.getLogger('alembic') + connection = op.get_bind() + session = Session(bind=connection) + + # Alter buildsteps + i = 0 + for step in session.query(BuildStep).yield_per(100): + i += 1 + step.outcome = OUTCOME_MAP[step.status] + step.description = DESCRIPTION_MAP[step.name] + session.add(step) + if i % 1000 == 0: + logger.info("Traversed %s steps", i) + session.commit() + logger.info("Final steps commit") + session.commit() + + op.alter_column('build_step', 'finished_at', new_column_name='t_finished') + op.alter_column('build_step', 'started_at', new_column_name='t_started') + op.alter_column('build_step', 'name', existing_type=sa.String(20), type_=sa.Text()) + op.drop_index('buildstep_idx_status', table_name='build_step') + op.drop_column('build_step', 'status') + op.drop_column('build_step', 'data') + + op.add_column('job', sa.Column('outcome', sa.Enum( + 'COMPLETED', 'FAILED', 'CRASHED', 'ABORTED', name='execoutcome'), nullable=True)) + op.alter_column('job', 'link_build_log', new_column_name='ref_url') + op.alter_column('job', 't_build_ended', new_column_name='t_finished') + op.alter_column('job', 't_build_started', new_column_name='t_started') + op.alter_column('job', 'taskname', existing_type=sa.String(20), type_=sa.Text()) + op.alter_column('job', 'item_type', existing_type=sa.String(20), type_=sa.Text()) + op.create_index('job_idx_type', 'job', [ + 'item_type'], unique=False, postgresql_ops={'item_type': 'text_pattern_ops'}) + op.drop_column('job', 'fedmsg_data') + op.drop_column('job', 'slavename') + op.drop_column('job', 'arch') + + +def downgrade(): + pass + # FIXME - put together later on diff --git a/execdb/models/job.py b/execdb/models/job.py index 2e48f88..a9b9bf6 100644 --- a/execdb/models/job.py +++ b/execdb/models/job.py @@ -23,118 +23,118 @@ import datetime import uuid +EXEC_OUTCOME = ('COMPLETED', 'FAILED', 'CRASHED', 'ABORTED') + class Job(db.Model): id = db.Column(db.Integer, primary_key=True) uuid = db.Column(db.String(36), unique=True) t_triggered = db.Column(db.DateTime, default=datetime.datetime.utcnow) - t_build_started = db.Column(db.DateTime) - t_build_ended = db.Column(db.DateTime) - - fedmsg_data = db.Column(db.Text) + t_started = db.Column(db.DateTime) + t_finished = db.Column(db.DateTime) taskname = db.Column(db.Text) item = db.Column(db.Text) - item_type = db.Column(db.String(20)) - arch = db.Column(db.String(10)) - - slavename = db.Column(db.String(20)) - # slave_env_details = db.Column(db.Text) # ??? - - # task_git_hash = db.Column(db.String(40)) # ??? + item_type = db.Column(db.Text) - # link_machine_log = db.Column(db.Text) # ??? - link_build_log = db.Column(db.Text) # $buildbot_url/builders/$builder_name/builds/$build_id - # link_resultsdb = db.Column(db.Text) # Should be UUID-based + outcome = db.Column(db.Enum(*EXEC_OUTCOME, name='execoutcome')) + ref_url = db.Column(db.Text) - build_steps = db.relation('BuildStep', backref='job', order_by="BuildStep.id") + steps = db.relation('BuildStep', backref='job', order_by="BuildStep.id") __table_args__ = ( db.Index('job_idx_item', 'item', postgresql_ops={'item': 'text_pattern_ops'}, ), + db.Index('job_idx_type', 'item_type', + postgresql_ops={'item_type': 'text_pattern_ops'}, + ), db.Index('job_idx_taskname', 'taskname', postgresql_ops={'taskname': 'text_pattern_ops'}, ), ) - def __init__(self, fedmsg_data=None): + def __init__(self, item=None, item_type=None, ref_url=None): self.uuid = str(uuid.uuid1()) - self.fedmsg_data = fedmsg_data + self.item = item + self.item_type = item_type + self.ref_url = None def start(self): - self.t_build_started = datetime.datetime.utcnow() + self.t_started = datetime.datetime.utcnow() - def finish(self): - self.t_build_ended = datetime.datetime.utcnow() + def finish(self, outcome="COMPLETED"): + self.t_finished = datetime.datetime.utcnow() + self.outcome = outcome @property - def current_state(self): - if self.t_build_started and not self.t_build_ended: - return "Running" - if self.t_build_ended: - return "Finished" - return "Triggered" + def status(self): + if self.t_started and not self.t_finished: + return "RUNNING" + if self.t_finished: + return "FINISHED" + return "SCHEDULED" @property def started_after(self): try: - return self.t_build_started - self.t_triggered + return self.t_started - self.t_triggered except TypeError: return None @property - def build_took(self): + def execution_took(self): try: - return self.t_build_ended - self.t_build_started + return self.t_finished - self.t_started except TypeError: - return None - - def get_build_step(self, name): - for step in self.build_steps: - if step.name == name: - return step - raise KeyError("Step %r not found" % name) + try: + return datetime.datetime.utcnow() - self.t_started + except TypeError: + return datetime.timedelta() class BuildStep(db.Model): id = db.Column(db.Integer, primary_key=True) job_id = db.Column(db.Integer, db.ForeignKey('job.id')) - name = db.Column(db.String(20)) - status = db.Column(db.String(10)) - started_at = db.Column(db.DateTime) - finished_at = db.Column(db.DateTime) - data = db.Column(db.Text) + name = db.Column(db.Text) + description = db.Column(db.Text) + outcome = db.Column(db.Enum(*EXEC_OUTCOME, name='execoutcome')) + t_started = db.Column(db.DateTime, default=datetime.datetime.utcnow) + t_finished = db.Column(db.DateTime) __table_args__ = ( - db.Index('buildstep_idx_status', 'status', - postgresql_ops={'status': 'text_pattern_ops'}, - ), db.Index('buildstep_fk_job_id', 'job_id'), ) def __repr__(self): - return "(%s, %s)" % (self.name, self.status) + return "(%s, %s, %s)" % (self.name, self.description, self.status) - def __init__(self, name): + def __init__(self, name=None, description=None, outcome=None): self.name = name + self.description = description + self.outcome = outcome def start(self): - self.started_at = datetime.datetime.utcnow() + self.t_started = datetime.datetime.utcnow() - def finish(self): - self.finished_at = datetime.datetime.utcnow() + def finish(self, outcome="COMPLETED"): + self.t_finished = datetime.datetime.utcnow() + self.outcome = outcome @property - def step_took(self): + def status(self): + if self.t_started and not self.t_finished: + return "RUNNING" + if self.t_finished: + return "FINISHED" + return "SCHEDULED" + + @property + def execution_took(self): try: - return self.finished_at - self.started_at + return self.t_finished - self.t_started except TypeError: try: - return datetime.datetime.utcnow() - self.started_at + return datetime.datetime.utcnow() - self.t_started except TypeError: return datetime.timedelta() - - @property - def log_url(self): - return "%s/steps/%s" % (self.job.link_build_log, self.name) From e413b2bfc080ffc1909de59cb027e1600f47712e Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: May 26 2017 07:43:01 +0000 Subject: [PATCH 2/10] API - first steps --- diff --git a/execdb/controllers/main.py b/execdb/controllers/main.py index f4abcde..2e0e044 100644 --- a/execdb/controllers/main.py +++ b/execdb/controllers/main.py @@ -32,21 +32,31 @@ from sqlalchemy import desc import json import re +import datetime from pprint import pformat main = Blueprint('main', __name__) -BB_URL = app.config['BUILDBOT_FRONTPAGE_URL'] RESULTSDB_URL = app.config['RESULTSDB_FRONTPAGE_URL'] +QUERY_LIMIT = 20 + RE_PAGE = re.compile(r"([?&])page=([0-9]+)") +RE_CALLBACK = re.compile(r"([?&])callback=[^&]*&?") +RE_CLEAN_AMPERSANDS = re.compile(r'&+') + RP = {} -RP['get_jobs'] = reqparse.RequestParser() -RP['get_jobs'].add_argument('page', default=0, type=int, location='args') -RP['get_jobs'].add_argument('limit', default=30, type=int, location='args') +# ============================================================================= +# GLOBAL METHODS +# ============================================================================= def pagination(q, page, limit): + """ + Sets the offset/limit for the DB query. + limit+1 is purposely set as 'limit' so we can later on decide whether 'next' + page link should be provided or set to None. + """ # pagination offset try: page = int(page) @@ -56,111 +66,263 @@ def pagination(q, page, limit): except (TypeError, ValueError): pass - # apply the query limit - try: - limit = int(limit) - except (ValueError, TypeError): - limit = QUERY_LIMIT - - q = q.limit(limit) + q = q.limit(limit + 1) return q -# TODO: find a better way to do this - -def prev_next_urls(): +def prev_next_urls(data, limit=QUERY_LIMIT): global RE_PAGE + try: match = RE_PAGE.findall(request.url) flag, page = match[0][0], int(match[0][1]) except IndexError: # page not found - if '?' in request.url: - return None, "%s&page=1" % request.url - else: - return None, "%s?page=1" % request.url + page = None prev = None next = None - prevpage = page - 1 - nextpage = page + 1 + placeholder = "[!@#$%^&*PLACEHOLDER*&^%$#@!]" + + if page is None: + if "?" in request.url: + baseurl = "%s&page=%s" % (request.url, placeholder) + else: + baseurl = "%s?page=%s" % (request.url, placeholder) + page = 0 + else: + baseurl = RE_PAGE.sub("%spage=%s" % (flag, placeholder), request.url) + + baseurl = RE_CALLBACK.sub(r"\1", baseurl) + baseurl = RE_CLEAN_AMPERSANDS.sub('&', baseurl) if page > 0: - prev = RE_PAGE.sub("%spage=%s" % (flag, prevpage), request.url) - next = RE_PAGE.sub("%spage=%s" % (flag, nextpage), request.url) + prev = baseurl.replace(placeholder, str(page - 1)) + if len(data) > limit: + next = baseurl.replace(placeholder, str(page + 1)) + data = data[:limit] - return prev, next + return data, prev, next -@main.route('/') -@main.route('/index') -@main.route('/jobs', methods=['GET']) -def index(): +# ============================================================================= +# API +# ============================================================================= + +RP['create_job'] = reqparse.RequestParser() +RP['create_job'].add_argument('taskname', required=True, location='json') +RP['create_job'].add_argument('item', required=True, location='json') +RP['create_job'].add_argument('item_type', location='json') +RP['create_job'].add_argument('ref_url', location='json') + +@main.route('/api/v1/jobs', methods=['POST']) +def create_job(): try: - args = RP['get_jobs'].parse_args() + args = RP['create_job'].parse_args() except JSONBadRequest as error: - return jsonify({"message": "Bad Request"}), error.code + return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code except HTTPException as error: return jsonify(error.data), error.code - query = db.session.query(Job).order_by(desc(Job.t_triggered)) - query = pagination(query, args['page'], args['limit']) + job = Job(**args) + db.session.add(job) + db.session.commit() + # FIXME - add resultsdb-like serializer + retval = { + 'uuid': job.uuid, + 'taskname': job.taskname, + 'item': job.item, + 'item_type': job.item_type, + 'ref_url': job.ref_url, + 'outcome': job.outcome, + 'status': job.status + } - prev, next = prev_next_urls() - jobs = query.all() + return jsonify(retval), 201 - return render_template('index.html', - jobs=jobs, - buildbot_url=BB_URL, - prev=prev, - next=next) +RP['update_job'] = reqparse.RequestParser() +RP['update_job'].add_argument('ref_url', location='json') +RP['update_job'].add_argument('start', type=bool, default=False, location='json') +RP['update_job'].add_argument('outcome', location='json') -@main.route('/jobs/', methods=['GET']) -def show_job(uuid): +@main.route('/api/v1/jobs/', methods=['POST']) +def update_job(uuid): + try: + args = RP['update_job'].parse_args() + except JSONBadRequest as error: + return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code + except HTTPException as error: + return jsonify(error.data), error.code + job = Job.query.filter_by(uuid=args['uuid']).first() + if not job: + return jsonify({'message': 'Job not found'}, 404) + + if args['outcome'] and args['outcome'] not in EXEC_OUTCOME: + return jsonify({'message': "outcome %r not one of %r" % (args['outcome'], EXEC_OUTCOME,)}), 400) + + if args['start']: + job.start() + + if args['outcome']]: + try: + # mark last step as finished, if not done already + last_step = job.steps[-1] + if last_step.status != 'FINISHED': + lasts_step.finish() + db.session.add(last_step) + except IndexError: + pass + job.finish(args['outcome']) + + db.session.add(job) + db.session.commit() + + # FIXME - add resultsdb-like serializer + retval = { + 'uuid': job.uuid, + 'taskname': job.taskname, + 'item': job.item, + 'item_type': job.item_type, + 'ref_url': job.ref_url, + 'outcome': job.outcome, + 'status': job.status + } + + return jsonify(retval), 200 + + +RP['create_step'] = reqparse.RequestParser() +RP['create_step'].add_argument('name', location='json') +RP['create_step'].add_argument('description', required=True, location='json') +RP['create_step'].add_argument('duration', type=int, location='json') +RP['create_step'].add_argument('outcome', location='json') + +@main.route('/api/v1/jobs//steps', methods=['POST']) +def create_step(uuid): try: - job = db.session.query(Job).filter(Job.uuid == uuid).one() - except orm_exc.NoResultFound: - return 'UUID not found', 404 - job.t_triggered = str(job.t_triggered).split('.')[0] - return render_template('show_job.html', - job=job, - buildbot_url=BB_URL, - resultsdb_url=RESULTSDB_URL, - artifacts_base_url=app.config['ARTIFACTS_BASE_URL']) + args = RP['update_job'].parse_args() + except JSONBadRequest as error: + return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code + except HTTPException as error: + return jsonify(error.data), error.code + job = Job.query.filter_by(uuid=args['uuid']).first() + if not job: + return jsonify({'message': 'Job not found'}, 404) + + try: + # mark last step as finished, if not done already + last_step = job.steps[-1] + if last_step.status != 'FINISHED': + lasts_step.finish() + db.session.add(last_step) + except IndexError: + pass + + step = BuildStep(args['name'], args['description'], args['outcome']) + step.start() + if args['outcome']: + if step.status != 'FINISHED': + step.finish(args['outcome']) + step.outcome = args['outcome'] + if args['duration']: + if step.status != 'FINISHED': + return jsonify({'message': "Can not set duration on unfinished step without `outcome`"}, 400) + + step.t_finished = step.t_started + datetime.timedelta(seconds=args['duration']) + + db.session.add(step) + db.session.commit() + + # FIXME - add resultsdb-like serializer + retval = { + 'name': step.name, + 'description': step.description, + 'status': step.status(), + 'outcome': step.outcome, + } + + return jsonify(retval), 201 + + +RP['update_step'] = reqparse.RequestParser() +RP['update_step'].add_argument('duration', type=int, location='json') +RP['update_step'].add_argument('outcome', default='COMPLETED' location='json') + +@main.route('/api/v1/jobs//steps/', methods=['POST']) +def update_step(uuid, id): + try: + args = RP['update_job'].parse_args() + except JSONBadRequest as error: + return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code + except HTTPException as error: + return jsonify(error.data), error.code + job = Job.query.filter_by(uuid=args['uuid']).first() + if not job: + return jsonify({'message': 'Job not found'}, 404) + + if id == 'last': + try: + step = job.steps[-1] + except IndexError: + return jsonify({'message': "Step not found"}, 404) + else: + try: + id = int(id) + except ValueError: + return jsonify({'message': "id %r could not be converted to int" % id}, 400) + try: + step = job.steps[id] + except IndexError: + return jsonify({'message': "Step not found"}, 404) + + if args['outcome'] and args['outcome'] not in EXEC_OUTCOME: + return jsonify({'message': "outcome %r not one of %r" % (args['outcome'], EXEC_OUTCOME,)}), 400) + + if args['outcome']: + if step.status != 'FINISHED': + step.finish(args['outcome']) + step.outcome = args['outcome'] + if args['duration']: + if step.status != 'FINISHED': + return jsonify({'message': "Can not set duration on unfinished step without `outcome`"}, 400) + + step.t_finished = step.t_started + datetime.timedelta(seconds=args['duration']) + + db.session.add(step) + db.session.add(job) + db.session.commit() + + # FIXME - add resultsdb-like serializer + retval = { + 'name': step.name, + 'description': step.description, + 'status': step.status(), + 'outcome': step.outcome, + } + + return jsonify(retval), 200 -@main.route('/jobs//steps', methods=['GET']) -def show_steps(uuid): +@main.route('/api/v1/jobs//steps', methods=['GET']) +def get_steps(uuid): try: job = db.session.query(Job).filter(Job.uuid == uuid).one() except orm_exc.NoResultFound: return 'UUID not found', 404 steps = dict( - buildbot_url=BB_URL, steps=[], job_status=job.current_state, job_duration=str(job.build_took), ) - name_map = { - "rm_tmp": "Cleaning-up temporary files", - "rm_log": "Cleaning-up log files", - "git": "Cloning the task repository", - "runtask": "Running the testsuite", - "MasterShellCommand": "Creating artifacts directory on master", - "upload": "Uploading the artifacts to master", - "MasterShellCommand_1": "Compressing artifacts", - "MasterShellCommand_2": "Publishing the artifacts", - } for step in job.build_steps: - if step.status not in ['OK', 'NOT OK', 'INPROGRESS']: - continue s = {} s['name'] = step.name - s['description'] = name_map.get(step.name, step.name) + s['description'] = step.description s['status'] = step.status + s['outcome'] = step.outcome s['log_url'] = step.log_url - duration = step.step_took + duration = step.execution_took minutes = duration.seconds / 60 seconds = duration.seconds - (minutes*60) duration = [] @@ -173,167 +335,46 @@ def show_steps(uuid): return jsonify(steps) +# ============================================================================= +# FRONTEND +# ============================================================================= -@main.route('/jobs', methods=['POST']) -def create_job(): - job = Job() - - data = request.json - job.fedmsg_data = json.dumps(data) - - # FIXME - add validation - job.taskname = data['taskname'] - job.item = data['item'] - job.item_type = data['item_type'] - job.arch = data['arch'] - - db.session.add(job) - db.session.commit() - # FIXME - add resultsdb-like serializer - retval = { - 'id': job.id, - 'uuid': job.uuid, - 't_triggered': job.t_triggered.isoformat() - } - - return jsonify(retval), 201 - - -def process_event(data): - - def bb_convert_properties(prop): - """Converts list of lists to dict""" - return dict([(key, value) for key, value, _ in prop]) +RP['index'] = reqparse.RequestParser() +RP['index'].add_argument('page', default=0, type=int, location='args') +RP['index'].add_argument('limit', default=30, type=int, location='args') - # at the moment, we act just on these events - event = data['event'] - known_events = ['changeAdded', 'buildStarted', 'stepStarted', - 'stepFinished', 'buildFinished'] - - if event not in known_events: - # FIXME remove - if 'uuid' in json.dumps(data): - app.logger.debug("UUID found in %s", event) +@main.route('/') +@main.route('/index') +@main.route('/jobs', methods=['GET']) +def index(): + try: + args = RP['index'].parse_args() + except JSONBadRequest as error: + return jsonify({"message": "Bad Request"}), error.code + except HTTPException as error: + return jsonify(error.data), error.code - return 'Skipping event', 204 + query = db.session.query(Job).order_by(desc(Job.t_triggered)) + query = pagination(query, args['page'], args['limit']) - # grab the 'properties' field - if event == 'changeAdded': - properties = bb_convert_properties(data['payload']['change']['properties']) - elif event in ['buildStarted', 'buildFinished']: - properties = bb_convert_properties(data['payload']['build']['properties']) - elif event in ['stepStarted', 'stepFinished']: - properties = bb_convert_properties(data['payload']['properties']) + prev, next = prev_next_urls() + jobs = query.all() - # abort if uuid is not provided - try: - uuid = properties['uuid'] - except KeyError: - return 'Missing `uuid` field in properties', 400 + return render_template('index.html', + jobs=jobs, + prev=prev, + next=next) - if uuid is None: - return 'UUID set to None', 400 +@main.route('/jobs/', methods=['GET']) +def show_job(uuid): try: job = db.session.query(Job).filter(Job.uuid == uuid).one() except orm_exc.NoResultFound: - return 'UUID not found', 400 - - if event == 'changeAdded': - # FIXME ? - pass - - elif event == 'buildStarted' and job.current_state == 'Triggered': - job.start() - - job.taskname = properties['taskname'] - job.item = properties['item'] - job.item_type = properties['item_type'] - job.arch = properties['arch'] - job.slavename = properties['slavename'] - job.link_build_log = '/builders/%s/builds/%s' % ( - data['payload']['build']['builderName'], - properties['buildnumber']) - - db.session.add(job) - - # add 'empty' steps for the build (since we know them already) -# app.logger.debug("%s: %s" % (uuid, data['payload']['build']['steps'])) -# app.logger.debug("%s - Build Started" % uuid) - for step_info in data['payload']['build']['steps']: - # app.logger.debug("%s -- adding step %s"% (uuid, step_info['name'])) - step = BuildStep(name=step_info['name']) - step.job = job - db.session.add(step) - - db.session.commit() - - elif event == 'stepStarted' and job.current_state == 'Running': - step_info = data['payload']['step'] -# app.logger.debug("%s - Step Started - %s"% (uuid, step_info['name'])) - try: - step = job.get_build_step(step_info['name']) - except KeyError: - app.logger.debug("Job %s had missing step %s", job.uuid, step_info) - step = BuildStep(name=step_info['name']) - step.job = job - - step.start() - step.status = 'INPROGRESS' - step.data = json.dumps(data['payload']) # FIXME - store sensible subset of data - db.session.add(step) - db.session.commit() -# app.logger.debug("%s - Step Started - %s - written to db"% (uuid, step_info['name'])) - - elif event == 'stepFinished' and job.current_state == 'Running': - step_info = data['payload']['step'] -# app.logger.debug("%s - Step Finished - %s"% (uuid, step_info['name'])) - try: - step = job.get_build_step(step_info['name']) - except KeyError: - return 'StepFinished received for non-existing step: %r' % step_info['name'], 400 - - step.finish() - - step.status = 'OK' - # results key is only present for non-ok results - if 'results' in step_info.keys(): - step.status = 'NOT OK' - step.data = json.dumps(data['payload']) # FIXME - store sensible subset of data - - db.session.add(step) - db.session.commit() -# app.logger.debug("%s - Step Finished - %s - written to db" % (uuid, step_info['name'])) - - elif event == 'buildFinished' and job.current_state == 'Running': - job.finish() - db.session.add(job) - db.session.commit() -# app.logger.debug("%s - Build Finished " % uuid) - + return 'UUID not found', 404 + job.t_triggered = str(job.t_triggered).split('.')[0] + return render_template('show_job.html', + job=job, + resultsdb_url=RESULTSDB_URL, + artifacts_base_url=app.config['ARTIFACTS_BASE_URL']) -@main.route('/buildbottest', methods=['POST']) -def bb_push(): - """ - Receives the post-push notifications from buildbot and fills in - the steps for the job. - """ - # data are embedded in form field 'packets' - data = request.form - try: - data = request.form['packets'] - except werkzeug.exceptions.BadRequestKeyError: - return 'Field `packets` missing in request form.', 400 - data = json.loads(data) - - # app.logger.debug(pformat(data)) - - # multiple messages may be present in one 'packet' - for entry in data: - process_event(entry) -# app.logger.debug("%s %s, %s", entry['id'], entry['event'], process_event(entry)) - - # plain 200 code needs to be returned - otherwise buildbot is - # endlessly trying to re-send the message. - # FIXME - add logging for non-200 responses - return '', 200 diff --git a/execdb/models/job.py b/execdb/models/job.py index a9b9bf6..44a7505 100644 --- a/execdb/models/job.py +++ b/execdb/models/job.py @@ -54,11 +54,12 @@ class Job(db.Model): ), ) - def __init__(self, item=None, item_type=None, ref_url=None): + def __init__(self, taskname=None, item=None, item_type=None, ref_url=None): self.uuid = str(uuid.uuid1()) + self.taskname = taskname self.item = item self.item_type = item_type - self.ref_url = None + self.ref_url = ref_url def start(self): self.t_started = datetime.datetime.utcnow() From 2f80d52da4a3daaadcaf305a286a51305cd76690 Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: May 26 2017 07:43:01 +0000 Subject: [PATCH 3/10] Moar API --- diff --git a/execdb/controllers/main.py b/execdb/controllers/main.py index 2e0e044..fae86ab 100644 --- a/execdb/controllers/main.py +++ b/execdb/controllers/main.py @@ -28,6 +28,7 @@ from werkzeug.exceptions import BadRequest as JSONBadRequest from execdb import app, db from execdb.models.job import Job, BuildStep +from execdb.serializers.api_v1 import Serializer from sqlalchemy import desc import json @@ -46,6 +47,7 @@ RE_CALLBACK = re.compile(r"([?&])callback=[^&]*&?") RE_CLEAN_AMPERSANDS = re.compile(r'&+') RP = {} +SERIALIZE = Serializer().serialize # ============================================================================= # GLOBAL METHODS @@ -108,6 +110,42 @@ def prev_next_urls(data, limit=QUERY_LIMIT): # API # ============================================================================= +# ============================================================================= +# JOBS +# ============================================================================= + +@main.route('/api/v1/jobs/', methods=['GET']) +def get_job(uuid): + job = Job.query.filter_by(uuid=uuid).first() + if not job: + return jsonify({'message': 'Job not found'}, 404) + return jsonify(SERIALIZE(job)) + + +RP['get_jobs'] = reqparse.RequestParser() +RP['get_jobs'].add_argument('page', default=0, type=int, location='args') +RP['get_jobs'].add_argument('limit', default=QUERY_LIMIT, type=int, location='args') + +@main.route('/api/v1/jobs', methods=['GET']) +def get_jobs(): + try: + args = RP['get_jobs'].parse_args() + except JSONBadRequest as error: + return jsonify({"message": "Bad Request"}), error.code + except HTTPException as error: + return jsonify(error.data), error.code + + query = db.session.query(Job).order_by(desc(Job.t_triggered)) + + query = pagination(query, args['page'], args['limit']) + data, prev, next = prev_next_urls(query.all(), args['limit']) + + return jsonify(dict( + prev=prev, + next=next, + data=[SERIALIZE(o) for o in data], + )) + RP['create_job'] = reqparse.RequestParser() RP['create_job'].add_argument('taskname', required=True, location='json') RP['create_job'].add_argument('item', required=True, location='json') @@ -137,12 +175,12 @@ def create_job(): 'status': job.status } - return jsonify(retval), 201 + return jsonify(SERIALIZE(job)), 201 RP['update_job'] = reqparse.RequestParser() -RP['update_job'].add_argument('ref_url', location='json') RP['update_job'].add_argument('start', type=bool, default=False, location='json') +RP['update_job'].add_argument('ref_url', location='json') RP['update_job'].add_argument('outcome', location='json') @main.route('/api/v1/jobs/', methods=['POST']) @@ -153,7 +191,7 @@ def update_job(uuid): return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code except HTTPException as error: return jsonify(error.data), error.code - job = Job.query.filter_by(uuid=args['uuid']).first() + job = Job.query.filter_by(uuid=uuid).first() if not job: return jsonify({'message': 'Job not found'}, 404) @@ -177,19 +215,12 @@ def update_job(uuid): db.session.add(job) db.session.commit() - # FIXME - add resultsdb-like serializer - retval = { - 'uuid': job.uuid, - 'taskname': job.taskname, - 'item': job.item, - 'item_type': job.item_type, - 'ref_url': job.ref_url, - 'outcome': job.outcome, - 'status': job.status - } + return jsonify(SERIALIZE(job)) - return jsonify(retval), 200 +# ============================================================================= +# STEPS +# ============================================================================= RP['create_step'] = reqparse.RequestParser() RP['create_step'].add_argument('name', location='json') @@ -205,7 +236,7 @@ def create_step(uuid): return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code except HTTPException as error: return jsonify(error.data), error.code - job = Job.query.filter_by(uuid=args['uuid']).first() + job = Job.query.filter_by(uuid=uuid).first() if not job: return jsonify({'message': 'Job not found'}, 404) @@ -226,22 +257,17 @@ def create_step(uuid): step.outcome = args['outcome'] if args['duration']: if step.status != 'FINISHED': - return jsonify({'message': "Can not set duration on unfinished step without `outcome`"}, 400) + return jsonify({'message': "Can not set duration on unfinished step."}, 400) + step.t_finished = datetime.datetime.utcnow() + step.t_started = step.t_finished - datetime.timedelta(seconds=args['duration']) - step.t_finished = step.t_started + datetime.timedelta(seconds=args['duration']) + job.steps.append(step) db.session.add(step) + db.session.add(job) db.session.commit() - # FIXME - add resultsdb-like serializer - retval = { - 'name': step.name, - 'description': step.description, - 'status': step.status(), - 'outcome': step.outcome, - } - - return jsonify(retval), 201 + return jsonify(SERIALIZE(step)), 201 RP['update_step'] = reqparse.RequestParser() @@ -256,7 +282,7 @@ def update_step(uuid, id): return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code except HTTPException as error: return jsonify(error.data), error.code - job = Job.query.filter_by(uuid=args['uuid']).first() + job = Job.query.filter_by(uuid=uuid).first() if not job: return jsonify({'message': 'Job not found'}, 404) @@ -285,54 +311,14 @@ def update_step(uuid, id): if args['duration']: if step.status != 'FINISHED': return jsonify({'message': "Can not set duration on unfinished step without `outcome`"}, 400) - - step.t_finished = step.t_started + datetime.timedelta(seconds=args['duration']) + step.t_finished = datetime.datetime.utcnow() + step.t_started = step.t_finished - datetime.timedelta(seconds=args['duration']) db.session.add(step) db.session.add(job) db.session.commit() - # FIXME - add resultsdb-like serializer - retval = { - 'name': step.name, - 'description': step.description, - 'status': step.status(), - 'outcome': step.outcome, - } - - return jsonify(retval), 200 - - -@main.route('/api/v1/jobs//steps', methods=['GET']) -def get_steps(uuid): - try: - job = db.session.query(Job).filter(Job.uuid == uuid).one() - except orm_exc.NoResultFound: - return 'UUID not found', 404 - - steps = dict( - steps=[], - job_status=job.current_state, - job_duration=str(job.build_took), - ) - for step in job.build_steps: - s = {} - s['name'] = step.name - s['description'] = step.description - s['status'] = step.status - s['outcome'] = step.outcome - s['log_url'] = step.log_url - duration = step.execution_took - minutes = duration.seconds / 60 - seconds = duration.seconds - (minutes*60) - duration = [] - if minutes: - duration.append('%s minutes' % minutes) - duration.append('%s seconds' % seconds) - s['duration'] = ' '.join(duration) - steps['steps'].append(s) - - return jsonify(steps) + return jsonify(SERIALIZE(step)) # ============================================================================= @@ -341,7 +327,7 @@ def get_steps(uuid): RP['index'] = reqparse.RequestParser() RP['index'].add_argument('page', default=0, type=int, location='args') -RP['index'].add_argument('limit', default=30, type=int, location='args') +RP['index'].add_argument('limit', default=QUERY_LIMIT, type=int, location='args') @main.route('/') @main.route('/index') @@ -355,10 +341,11 @@ def index(): return jsonify(error.data), error.code query = db.session.query(Job).order_by(desc(Job.t_triggered)) + query = pagination(query, args['page'], args['limit']) + data, prev, next = prev_next_urls(query.all(), args['limit']) - prev, next = prev_next_urls() - jobs = query.all() + jobs = [SERIALIZE(d) for d in data] return render_template('index.html', jobs=jobs, @@ -372,7 +359,9 @@ def show_job(uuid): job = db.session.query(Job).filter(Job.uuid == uuid).one() except orm_exc.NoResultFound: return 'UUID not found', 404 - job.t_triggered = str(job.t_triggered).split('.')[0] + + job = SERIALIZE(job) + return render_template('show_job.html', job=job, resultsdb_url=RESULTSDB_URL, diff --git a/execdb/models/job.py b/execdb/models/job.py index 44a7505..b27f5b5 100644 --- a/execdb/models/job.py +++ b/execdb/models/job.py @@ -18,6 +18,7 @@ # Josef Skladanka from execdb import db, app +from execdb.serializers import DBSerialize import datetime import uuid @@ -25,7 +26,7 @@ import uuid EXEC_OUTCOME = ('COMPLETED', 'FAILED', 'CRASHED', 'ABORTED') -class Job(db.Model): +class Job(db.Model, DBSerialize): id = db.Column(db.Integer, primary_key=True) uuid = db.Column(db.String(36), unique=True) @@ -84,7 +85,7 @@ class Job(db.Model): return None @property - def execution_took(self): + def duration(self): try: return self.t_finished - self.t_started except TypeError: @@ -94,7 +95,7 @@ class Job(db.Model): return datetime.timedelta() -class BuildStep(db.Model): +class BuildStep(db.Model, DBSerialize): id = db.Column(db.Integer, primary_key=True) job_id = db.Column(db.Integer, db.ForeignKey('job.id')) name = db.Column(db.Text) @@ -131,7 +132,7 @@ class BuildStep(db.Model): return "SCHEDULED" @property - def execution_took(self): + def duration(self): try: return self.t_finished - self.t_started except TypeError: diff --git a/execdb/serializers/__init__.py b/execdb/serializers/__init__.py new file mode 100644 index 0000000..a2190e7 --- /dev/null +++ b/execdb/serializers/__init__.py @@ -0,0 +1,71 @@ +# Copyright 2013, Red Hat, Inc +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Authors: +# Josef Skladanka + +from datetime import date, datetime, timedelta + + +class DBSerialize(object): + pass + + +class BaseSerializer(object): + + def serialize(self, value, **kwargs): + # serialize the database objects + # the specific serializer needs to implement serialize_CLASSNAME methods + if DBSerialize in value.__class__.__bases__: + return getattr(self, '_serialize_%s' % value.__class__.__name__)(value, **kwargs) + + if type(value) == timedelta: + ret = "" + s = value.seconds + hours = s // 3600 + s -= hours*3600 + if hours: + ret += '%s hours ' % hours + + minutes = s // 60 + s -= minutes*60 + if minutes: + ret += '%s minutes ' % minutes + + if s: + ret += '%s second%s' % (s, 's' if s > 1 else '') + return s + + + + # convert datetimes to the right format + if type(value) in (datetime, date): + return value.isoformat() + + if isinstance(value, dict): + ret = {} + for k, v in value.iteritems(): + ret[k] = self.serialize(v, **kwargs) + return ret + + # convert iterables to list of serialized stuff + if hasattr(value, '__iter__'): + ret = [] + for v in value: + ret.append(self.serialize(v, **kwargs)) + return ret + + return value diff --git a/execdb/serializers/api_v1.py b/execdb/serializers/api_v1.py new file mode 100644 index 0000000..c641346 --- /dev/null +++ b/execdb/serializers/api_v1.py @@ -0,0 +1,60 @@ +# Copyright 2013, Red Hat, Inc +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Authors: +# Josef Skladanka + +from flask import url_for +from execbd.serializers import BaseSerializer + + +class Serializer(BaseSerializer): + + def _serialize_Job(self, o, **kwargs): + rv = dict( + uuid=o.uuid, + t_triggered=o.t_triggered, + t_started=o.t_started, + t_finished=o.t_finished, + taskname=o.taskname, + item=o.item, + item_type=o.item_type, + outcome=o.outcome, + ref_url=o.ref_url, + status=o.status, + duration=o.duration, + steps=o.steps, + + href=url_for('main.get_job', uuid=o.uuid, _external=True), + ) + + return {key: self.serialize(value) for key, value in rv.iteritems()} + + def _serialize_BuildStep(self, o, **kwargs): + rv = dict( + id=o.job.steps.index(o), + name=o.name, + description=o.description, + outcome=o.outcome, + t_started=o.t_started, + t_finished=o.t_finished, + status=o.status, + duration=o.duration, + ) + + return {key: self.serialize(value) for key, value in rv.iteritems()} + + From ef32b248e63fc55d09861a249908326ad9f50ee2 Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: May 26 2017 07:43:01 +0000 Subject: [PATCH 4/10] API working (tested for common usage) --- diff --git a/execdb/cli.py b/execdb/cli.py index 8e1e1ca..1fd9906 100644 --- a/execdb/cli.py +++ b/execdb/cli.py @@ -121,6 +121,19 @@ def mock_data_live(destructive): import time import datetime import random + + dsc_map = { + "rm_tmp": "Cleaning-up temporary files", + "rm_log": "Cleaning-up log files", + "git": "Cloning the task repository", + "runtask": "Running the testsuite", + "MasterShellCommand": "Creating artifacts directory on master", + "upload": "Uploading the artifacts to master", + "MasterShellCommand_1": "Compressing artifacts", + "MasterShellCommand_2": "Publishing the artifacts", + } + + if destructive or not db.session.query(Job).count(): job = Job() job.uuid = '249cb6f4-e471-11e6-a77f-525400cb0b45' @@ -128,24 +141,20 @@ def mock_data_live(destructive): job.taskname = 'rpmgrill' job.item = 'libtaskotron-0.4.18-1.fc25' job.item_type = 'koji_build' - job.arch = 'x86_64' - job.slavename = 'qa11.qa-2' db.session.add(job) db.session.commit() - time.sleep(10) + time.sleep(5) job.start() db.session.add(job) db.session.commit() time.sleep(3) for step_name, sleep_time in (('rm_tmp', 2), ('git', 8), ('runtask', 17), ('MasterShellCommand', 1), ('upload', 6), ('MasterShellCommand_1', 3), ('MasterShellCommand_2', 5)): - step = BuildStep(step_name) + step = BuildStep(dsc_map[step_name], step_name) step.job = job - step.status = 'INPROGRESS' step.start() db.session.add(step) db.session.commit() time.sleep(sleep_time) - step.status = 'OK' step.finish() db.session.add(step) db.session.commit() diff --git a/execdb/controllers/main.py b/execdb/controllers/main.py index fae86ab..2d29be9 100644 --- a/execdb/controllers/main.py +++ b/execdb/controllers/main.py @@ -27,7 +27,7 @@ from werkzeug.exceptions import BadRequest as JSONBadRequest from execdb import app, db -from execdb.models.job import Job, BuildStep +from execdb.models.job import Job, BuildStep, EXEC_OUTCOME from execdb.serializers.api_v1 import Serializer from sqlalchemy import desc @@ -118,7 +118,7 @@ def prev_next_urls(data, limit=QUERY_LIMIT): def get_job(uuid): job = Job.query.filter_by(uuid=uuid).first() if not job: - return jsonify({'message': 'Job not found'}, 404) + return jsonify({'message': 'Job not found'}), 404 return jsonify(SERIALIZE(job)) @@ -182,36 +182,54 @@ RP['update_job'] = reqparse.RequestParser() RP['update_job'].add_argument('start', type=bool, default=False, location='json') RP['update_job'].add_argument('ref_url', location='json') RP['update_job'].add_argument('outcome', location='json') +RP['update_job'].add_argument('duration', location='json') @main.route('/api/v1/jobs/', methods=['POST']) def update_job(uuid): - try: + try: args = RP['update_job'].parse_args() except JSONBadRequest as error: return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code except HTTPException as error: return jsonify(error.data), error.code + + if args['duration']: + try: + args['duration'] = int(args['duration']) + except ValueError: + return jsonify({'message': 'duration must be int or None'}), 400 + job = Job.query.filter_by(uuid=uuid).first() if not job: - return jsonify({'message': 'Job not found'}, 404) + return jsonify({'message': 'Job not found'}), 404 if args['outcome'] and args['outcome'] not in EXEC_OUTCOME: - return jsonify({'message': "outcome %r not one of %r" % (args['outcome'], EXEC_OUTCOME,)}), 400) + return jsonify({'message': "outcome %r not one of %r" % (args['outcome'], EXEC_OUTCOME,)}), 400 if args['start']: job.start() - if args['outcome']]: + if args['outcome']: try: # mark last step as finished, if not done already last_step = job.steps[-1] if last_step.status != 'FINISHED': - lasts_step.finish() + last_step.finish() db.session.add(last_step) except IndexError: pass job.finish(args['outcome']) + if args['duration'] is not None: + if job.status != 'FINISHED': + job.finish() + job.t_finished = datetime.datetime.utcnow() + job.t_started = job.t_finished - datetime.timedelta(seconds=args['duration']) + + + if args['ref_url']: + job.ref_url = args['ref_url'] + db.session.add(job) db.session.commit() @@ -225,26 +243,33 @@ def update_job(uuid): RP['create_step'] = reqparse.RequestParser() RP['create_step'].add_argument('name', location='json') RP['create_step'].add_argument('description', required=True, location='json') -RP['create_step'].add_argument('duration', type=int, location='json') +RP['create_step'].add_argument('duration', location='json') RP['create_step'].add_argument('outcome', location='json') @main.route('/api/v1/jobs//steps', methods=['POST']) def create_step(uuid): try: - args = RP['update_job'].parse_args() + args = RP['create_step'].parse_args() except JSONBadRequest as error: return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code except HTTPException as error: return jsonify(error.data), error.code + + if args['duration']: + try: + args['duration'] = int(args['duration']) + except ValueError: + return jsonify({'message': 'duration must be int or None'}), 400 + job = Job.query.filter_by(uuid=uuid).first() if not job: - return jsonify({'message': 'Job not found'}, 404) + return jsonify({'message': 'Job not found'}), 404 try: # mark last step as finished, if not done already last_step = job.steps[-1] if last_step.status != 'FINISHED': - lasts_step.finish() + last_step.finish() db.session.add(last_step) except IndexError: pass @@ -255,9 +280,9 @@ def create_step(uuid): if step.status != 'FINISHED': step.finish(args['outcome']) step.outcome = args['outcome'] - if args['duration']: + if args['duration'] is not None: if step.status != 'FINISHED': - return jsonify({'message': "Can not set duration on unfinished step."}, 400) + step.finish() step.t_finished = datetime.datetime.utcnow() step.t_started = step.t_finished - datetime.timedelta(seconds=args['duration']) @@ -271,46 +296,54 @@ def create_step(uuid): RP['update_step'] = reqparse.RequestParser() -RP['update_step'].add_argument('duration', type=int, location='json') -RP['update_step'].add_argument('outcome', default='COMPLETED' location='json') +RP['update_step'].add_argument('duration', location='json') +RP['update_step'].add_argument('outcome', default='COMPLETED', location='json') @main.route('/api/v1/jobs//steps/', methods=['POST']) def update_step(uuid, id): try: - args = RP['update_job'].parse_args() + args = RP['update_step'].parse_args() except JSONBadRequest as error: return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code except HTTPException as error: return jsonify(error.data), error.code + + if args['duration']: + try: + args['duration'] = int(args['duration']) + except ValueError: + return jsonify({'message': 'duration must be int or None'}), 400 + job = Job.query.filter_by(uuid=uuid).first() if not job: - return jsonify({'message': 'Job not found'}, 404) + return jsonify({'message': 'Job not found'}), 404 if id == 'last': try: step = job.steps[-1] except IndexError: - return jsonify({'message': "Step not found"}, 404) + return jsonify({'message': "Step not found"}), 404 else: try: id = int(id) except ValueError: - return jsonify({'message': "id %r could not be converted to int" % id}, 400) + return jsonify({'message': "id %r could not be converted to int" % id}), 400 try: step = job.steps[id] except IndexError: - return jsonify({'message': "Step not found"}, 404) + return jsonify({'message': "Step not found"}), 404 if args['outcome'] and args['outcome'] not in EXEC_OUTCOME: - return jsonify({'message': "outcome %r not one of %r" % (args['outcome'], EXEC_OUTCOME,)}), 400) + return jsonify({'message': "outcome %r not one of %r" % (args['outcome'], EXEC_OUTCOME,)}), 400 if args['outcome']: if step.status != 'FINISHED': step.finish(args['outcome']) step.outcome = args['outcome'] - if args['duration']: + + if args['duration'] is not None: if step.status != 'FINISHED': - return jsonify({'message': "Can not set duration on unfinished step without `outcome`"}, 400) + step.finish() step.t_finished = datetime.datetime.utcnow() step.t_started = step.t_finished - datetime.timedelta(seconds=args['duration']) diff --git a/execdb/models/job.py b/execdb/models/job.py index b27f5b5..17cc651 100644 --- a/execdb/models/job.py +++ b/execdb/models/job.py @@ -111,7 +111,7 @@ class BuildStep(db.Model, DBSerialize): def __repr__(self): return "(%s, %s, %s)" % (self.name, self.description, self.status) - def __init__(self, name=None, description=None, outcome=None): + def __init__(self, description=None, name=None, outcome=None): self.name = name self.description = description self.outcome = outcome diff --git a/execdb/serializers/__init__.py b/execdb/serializers/__init__.py index a2190e7..1b9b0ef 100644 --- a/execdb/serializers/__init__.py +++ b/execdb/serializers/__init__.py @@ -33,21 +33,20 @@ class BaseSerializer(object): return getattr(self, '_serialize_%s' % value.__class__.__name__)(value, **kwargs) if type(value) == timedelta: - ret = "" + ret = [] s = value.seconds hours = s // 3600 s -= hours*3600 if hours: - ret += '%s hours ' % hours + ret.append('%s hours' % hours) minutes = s // 60 s -= minutes*60 if minutes: - ret += '%s minutes ' % minutes + ret.append('%s minutes' % minutes) - if s: - ret += '%s second%s' % (s, 's' if s > 1 else '') - return s + ret.append('%s second%s' % (s, '' if s == 1 else 's')) + return ' '.join(ret) diff --git a/execdb/serializers/api_v1.py b/execdb/serializers/api_v1.py index c641346..df5ad28 100644 --- a/execdb/serializers/api_v1.py +++ b/execdb/serializers/api_v1.py @@ -18,7 +18,7 @@ # Josef Skladanka from flask import url_for -from execbd.serializers import BaseSerializer +from execdb.serializers import BaseSerializer class Serializer(BaseSerializer): diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..14745e6 --- /dev/null +++ b/test_api.py @@ -0,0 +1,230 @@ +# Copyright 2013, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Author: Josef Skladanka + +import requests +import json +import inspect +import simplejson +import logging + +logger = logging.getLogger('execdb_api') +logger.addHandler(logging.NullHandler()) + +_KEEP = object() + + +def _fparams(expand_kwargs=True): + """Gets the parameters of the function, from which _fparams is called + and returns the list of params, minus `self`""" + frame = inspect.currentframe().f_back + args, varargs, keywords, values = inspect.getargvalues(frame) + + params = {} + + for key in args: + if key == 'self': + continue + params[key] = values[key] + + if keywords: + if expand_kwargs: + for key, value in values[keywords].iteritems(): + params[key] = value + else: + params[keywords] = values[keywords] + + # RequestParser fucks up None for strings, and converts it to u'None'... + cleaned_up = {} + for key, value in params.iteritems(): + if value is None: + continue + cleaned_up[key] = value + + return cleaned_up + + +class ExecDBapiException(Exception): + + def __init__(self, message='', response=None): + ''':param response: :class:`requests.Response` object''' + self.message = message + self.response = response + + def __str__(self): + return repr(self.message) + + +class ExecDBapi(object): + + def __init__(self, api_url): + # remove trailing slash(es), so we don't generate + # urls with a double slash which breaks werkzeug + # https://github.com/mitsuhiko/werkzeug/issues/491 + self.url = api_url.rstrip('/') + + def __raise_on_error(self, r): + if r.ok: + return + + try: + logger.warn('Received HTTP failure status code %s for request: %s', + r.status_code, r.url) + raise ExecDBapiException( + '%s (HTTP %s)' % (r.json()['message'], r.status_code), r) + except simplejson.JSONDecodeError as e: + logger.debug('Received invalid JSON data: %s\n%s', e, r.text) + raise ExecDBapiException( + 'Invalid JSON (HTTP %s): %s' % (r.status_code, e), r) + except KeyError: + raise ExecDBapiException('HTTP %s Error' % r.status_code, r) + + def __prepare_params(self, params_all): + params = {} + for key, value in params_all.iteritems(): + if value is None: + continue + if key == 'raw_params': + continue + + # if a param's name ends with _like, we treat it as if :like filter should be applied + # for the rare case, where it really is supposed to be the name, user should provide + # it in the 'raw_params' dict + if key.endswith('_like'): + key = "%s:like" % key[:-len('_like')] + + if type(value) in (list, tuple): + params[key] = ','.join([unicode(v) for v in value]) + else: + params[key] = unicode(value) + + if 'raw_params' in params_all.keys() and params_all['raw_params']: + raw_params = { + key: unicode(value) for key, value in params_all['raw_params'].iteritems()} + params.update(raw_params) + return params + + def create_job(self, taskname, item, item_type=None, ref_url=None): + url = "%s/jobs" % self.url + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + r = requests.post(url, data=json.dumps(_fparams()), headers=headers) + self.__raise_on_error(r) + + return r.json() + + def update_job(self, uuid, start=None, ref_url=None, outcome=None, duration=None): + url = "%s/jobs/%s" % (self.url, uuid) + headers = {'content-type': 'application/json', 'accept': 'text/plain'} + params = _fparams() + del(params['uuid']) + r = requests.post(url, data=json.dumps(params), headers=headers) + self.__raise_on_error(r) + + return r.json() + + def get_job(self, uuid): + url = "%s/jobs/%s" % (self.url, uuid) + r = requests.get(url) + self.__raise_on_error(r) + + return r.json() + + def get_jobs(self, page=None, limit=None): + url = "%s/jobs" % self.url + r = requests.get(url, params=self.__prepare_params(_fparams())) + self.__raise_on_error(r) + + return r.json() + + def create_step(self, job_uuid, description, name=None, outcome=None, duration=None): + url = "%s/jobs/%s/steps" % (self.url, job_uuid) + data = _fparams() + del(data['job_uuid']) + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + r = requests.post(url, data=json.dumps(data), headers=headers) + self.__raise_on_error(r) + + return r.json() + + def update_step(self, job_uuid, step_id='last', outcome=None, duration=None): + url = "%s/jobs/%s/steps/%s" % (self.url, job_uuid, step_id) + headers = {'content-type': 'application/json', 'accept': 'text/plain'} + data = _fparams() + del(data['job_uuid']) + del(data['step_id']) + r = requests.post(url, data=json.dumps(data), headers=headers) + self.__raise_on_error(r) + + return r.json() + + +if __name__ == '__main__': + import time + import datetime + import random + + dsc_map = { + "rm_tmp": "Cleaning-up temporary files", + "rm_log": "Cleaning-up log files", + "git": "Cloning the task repository", + "runtask": "Running the testsuite", + "MasterShellCommand": "Creating artifacts directory on master", + "upload": "Uploading the artifacts to master", + "MasterShellCommand_1": "Compressing artifacts", + "MasterShellCommand_2": "Publishing the artifacts", + } + + api = ExecDBapi('http://localhost:5003/api/v1') + + # Create job + job = api.create_job('rpmgrill', 'libtaskotron-0.4.18-1.fc25', 'koji_build') + time.sleep(5) + # Start job + api.update_job(job['uuid'], start=True, ref_url="http://example.com") + time.sleep(3) + + # Test step autofinishing + for step_name, sleep_time in (('rm_tmp', 2), ('git', 8)): + step = api.create_step(job['uuid'], dsc_map[step_name], step_name) + time.sleep(sleep_time) + # Test manual finish + api.update_step(job['uuid'], step['id'], outcome="FAILED") + + # Test duration + step_name = 'runtask' + time.sleep(17) + step = api.create_step(job['uuid'], dsc_map[step_name], step_name, duration=17, outcome="COMPLETED") + + step_name = 'MasterShellCommand' + step = api.create_step(job['uuid'], dsc_map[step_name], step_name) + time.sleep(3) + api.update_step(job['uuid'], step['id'], duration=1) + + # Test 'last' as step id + step_name = 'upload' + step = api.create_step(job['uuid'], dsc_map[step_name], step_name) + time.sleep(6) + api.update_step(job['uuid'], 'last', outcome='ABORTED') + + # Do the ret of the steps + for step_name, sleep_time in (('MasterShellCommand_1', 3), ('MasterShellCommand_2', 5)): + step = api.create_step(job['uuid'], dsc_map[step_name], step_name) + time.sleep(sleep_time) + + # Finish Job + api.update_job(job['uuid'], outcome='COMPLETED') + From 071e3c258dc1f873eae6c1a876681d51f552fa64 Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: May 26 2017 07:43:01 +0000 Subject: [PATCH 5/10] Make me nice, will'ya? --- diff --git a/execdb/controllers/main.py b/execdb/controllers/main.py index 2d29be9..ae2b6a3 100644 --- a/execdb/controllers/main.py +++ b/execdb/controllers/main.py @@ -203,8 +203,8 @@ def update_job(uuid): if not job: return jsonify({'message': 'Job not found'}), 404 - if args['outcome'] and args['outcome'] not in EXEC_OUTCOME: - return jsonify({'message': "outcome %r not one of %r" % (args['outcome'], EXEC_OUTCOME,)}), 400 + if args['outcome'] and args['outcome'] not in EXEC_OUTCOME+('_COMPUTED_', ): + return jsonify({'message': "outcome %r not one of %r" % (args['outcome'], EXEC_OUTCOME+['_COMPUTE_'],)}), 400 if args['start']: job.start() @@ -274,7 +274,7 @@ def create_step(uuid): except IndexError: pass - step = BuildStep(args['name'], args['description'], args['outcome']) + step = BuildStep(args['description'], args['name'], args['outcome']) step.start() if args['outcome']: if step.status != 'FINISHED': diff --git a/execdb/models/job.py b/execdb/models/job.py index 17cc651..fa84647 100644 --- a/execdb/models/job.py +++ b/execdb/models/job.py @@ -24,7 +24,7 @@ import datetime import uuid -EXEC_OUTCOME = ('COMPLETED', 'FAILED', 'CRASHED', 'ABORTED') +EXEC_OUTCOME = ('COMPLETED', 'FAILED', 'ABORTED', 'CRASHED') class Job(db.Model, DBSerialize): id = db.Column(db.Integer, primary_key=True) @@ -65,8 +65,13 @@ class Job(db.Model, DBSerialize): def start(self): self.t_started = datetime.datetime.utcnow() - def finish(self, outcome="COMPLETED"): + def finish(self, outcome="COMPLETE"): self.t_finished = datetime.datetime.utcnow() + if outcome == '_COMPUTED_': + outcome = 'COMPLETED' + for step in self.steps: + if EXEC_OUTCOME.index(step.outcome) > EXEC_OUTCOME.index(outcome): + outcome = step.outcome self.outcome = outcome @property diff --git a/execdb/templates/index.html b/execdb/templates/index.html index e01857b..4bdcec8 100644 --- a/execdb/templates/index.html +++ b/execdb/templates/index.html @@ -7,7 +7,7 @@ Job Item State - Build Steps + Progress Moar @@ -17,16 +17,15 @@ {{job.taskname}} {{job.item|truncate(50, True)}} - {{job.current_state}} + {{job.status}} - {% for step in job.build_steps %} - {# #} - {# #} {% endfor %} - + diff --git a/execdb/templates/show_job.html b/execdb/templates/show_job.html index 7faecec..b0fc2f3 100644 --- a/execdb/templates/show_job.html +++ b/execdb/templates/show_job.html @@ -7,44 +7,42 @@ function toggle_text_visibility(e){ } function show_data(){ - $.get("/jobs/{{job.uuid}}/steps", function(data){ + $.get("/api/v1/jobs/{{job.uuid}}", function(data){ console.log(data); $('#job_progress').empty(); - var computed_job_status = "OK"; data.steps.forEach(function(value){ var step_url = ''; var icon = ''; var row_bgcolor = 'none'; - if (value.status == 'INPROGRESS'){ + if (value.status == 'RUNNING'){ icon = 'In progress'; row_bgcolor = '#577591'; } - else if (value.status == 'OK'){ + else if (value.outcome == 'COMPLETED'){ icon = 'OK'; row_bgcolor = '#639a81'; } - else if (value.status == 'NOT OK'){ + else { icon = 'Not OK'; - computed_job_status = "NOT OK"; row_bgcolor = '#cc6168'; } var field = '
'+icon+' '+value.description+'
'+value.duration+'
'; $('#job_progress').append(field); }); var job_icon = ''; - var job_duration = ''+data.job_duration; - if (data.job_status == 'Triggered'){ + var job_duration = data.duration; + if (data.status == 'SCHEDULED'){ job_duration = 'Scheduled to run soon'; job_icon = ''; $('#data_envelope').css({'background-color':'#535452'}); } - if (data.job_status == 'Running'){ - job_duration = 'The job is running'; + if (data.status == 'RUNNING'){ + job_duration = data.duration; job_icon = ''; $('#data_envelope').css({'background-color':'#577591'}); } - if (data.job_status == 'Finished'){ - if (computed_job_status == 'OK'){ + if (data.status == 'FINISHED'){ + if (data.outcome == 'COMPLETED'){ //job_icon = ''; job_icon = ''; $('#data_envelope').css({'background-color':'#639a81'}); @@ -58,8 +56,8 @@ function show_data(){ $('#job_status').html(job_icon); $('#job_duration').text(job_duration); - if (data.job_status != 'Finished'){ - setTimeout(show_data, 5000); + if (data.status != 'FINISHED'){ + setTimeout(show_data, 1000); } else { @@ -99,13 +97,7 @@ $('document').ready(function() {
Type
{{job.item_type}}
-
- -
-
Arch
-
{{job.arch}}
-
-
+
{{job.duration}}
@@ -121,127 +113,4 @@ $('document').ready(function() {
-{# - - - - - - - - - - -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - -

The job is {{job.current_state}}

-
-
-
-
-
- -

Details

- -
- -
-
Taskname
-
{{job.taskname}}
-
- -
-
Item
-
{{job.item}}
-
- -
-
Item type
-
{{job.item_type}}
-
- -
-
Arch
-
{{job.arch}}
-
- -
- -
-
Triggered at
-
{{job.t_triggered}}
-
- -
-
Started after
-
{{job.started_after}}
-
- -
-
Finished in
-
{{job.build_took}}
-
- -
- -
-
FedMsg data
-
{{job.fedmsg_data}}
-
- - -
- -
-
Logs
-
-
- -
-
Artifacts
-
-
- -
- -
-
ResultsDB
-
-
- -
-#} {% endblock %} diff --git a/test_api.py b/test_api.py index 14745e6..f25622b 100644 --- a/test_api.py +++ b/test_api.py @@ -226,5 +226,5 @@ if __name__ == '__main__': time.sleep(sleep_time) # Finish Job - api.update_job(job['uuid'], outcome='COMPLETED') + api.update_job(job['uuid'], outcome='_COMPUTED_') From c340bf928e17c2460cfd1ec5f398a7a778de338c Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: Jul 13 2017 12:31:40 +0000 Subject: [PATCH 6/10] wip --- diff --git a/alembic/versions/104a0806520e_bulidbot_decoupling_step_1.py b/alembic/versions/104a0806520e_bulidbot_decoupling_step_1.py index 31fa989..6b8ede4 100644 --- a/alembic/versions/104a0806520e_bulidbot_decoupling_step_1.py +++ b/alembic/versions/104a0806520e_bulidbot_decoupling_step_1.py @@ -8,7 +8,7 @@ Create Date: 2017-02-22 15:39:40.333356 # revision identifiers, used by Alembic. revision = '104a0806520e' -down_revision = '3b51d15f0f87' +down_revision = '6fc76719cc5f' branch_labels = None depends_on = None diff --git a/execdb/controllers/main.py b/execdb/controllers/main.py index ae2b6a3..11d69d0 100644 --- a/execdb/controllers/main.py +++ b/execdb/controllers/main.py @@ -178,24 +178,10 @@ def create_job(): return jsonify(SERIALIZE(job)), 201 -RP['update_job'] = reqparse.RequestParser() -RP['update_job'].add_argument('start', type=bool, default=False, location='json') -RP['update_job'].add_argument('ref_url', location='json') -RP['update_job'].add_argument('outcome', location='json') -RP['update_job'].add_argument('duration', location='json') - -@main.route('/api/v1/jobs/', methods=['POST']) -def update_job(uuid): - try: - args = RP['update_job'].parse_args() - except JSONBadRequest as error: - return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code - except HTTPException as error: - return jsonify(error.data), error.code - - if args['duration']: +def _update_job(uuid, start=False, ref_url=None, outcome=None, duration=None): + if duration: try: - args['duration'] = int(args['duration']) + duration = int(duration) except ValueError: return jsonify({'message': 'duration must be int or None'}), 400 @@ -203,13 +189,13 @@ def update_job(uuid): if not job: return jsonify({'message': 'Job not found'}), 404 - if args['outcome'] and args['outcome'] not in EXEC_OUTCOME+('_COMPUTED_', ): - return jsonify({'message': "outcome %r not one of %r" % (args['outcome'], EXEC_OUTCOME+['_COMPUTE_'],)}), 400 + if outcome and outcome not in EXEC_OUTCOME+('_COMPUTED_', ): + return jsonify({'message': "outcome %r not one of %r" % (outcome, EXEC_OUTCOME+['_COMPUTE_'],)}), 400 - if args['start']: + if start: job.start() - if args['outcome']: + if outcome: try: # mark last step as finished, if not done already last_step = job.steps[-1] @@ -218,17 +204,17 @@ def update_job(uuid): db.session.add(last_step) except IndexError: pass - job.finish(args['outcome']) + job.finish(outcome) - if args['duration'] is not None: + if duration is not None: if job.status != 'FINISHED': job.finish() job.t_finished = datetime.datetime.utcnow() - job.t_started = job.t_finished - datetime.timedelta(seconds=args['duration']) + job.t_started = job.t_finished - datetime.timedelta(seconds=duration) - if args['ref_url']: - job.ref_url = args['ref_url'] + if ref_url: + job.ref_url = ref_url db.session.add(job) db.session.commit() @@ -236,13 +222,69 @@ def update_job(uuid): return jsonify(SERIALIZE(job)) +RP['update_job'] = reqparse.RequestParser() +RP['update_job'].add_argument('start', type=bool, default=False, location='json') +RP['update_job'].add_argument('ref_url', location='json') +RP['update_job'].add_argument('outcome', location='json') +RP['update_job'].add_argument('duration', location='json') + +@main.route('/api/v1/jobs/', methods=['POST']) +def update_job(uuid): + try: + args = RP['update_job'].parse_args() + except JSONBadRequest as error: + return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code + except HTTPException as error: + return jsonify(error.data), error.code + + return _update_job(uuid, **args) + + +# ============================================================================= +# JENKINS WORKAROUND +# ============================================================================= + +RP['jenkins_notification'] = reqparse.RequestParser() +RP['jenkins_notification'].add_argument('build', type=dict, required=True, location='json') +@main.route('/api/v1/jobs/jenkins_notification', methods=['POST']) +def jenkins_notification(): + try: + args = RP['jenkins_notification'].parse_args() + except JSONBadRequest as error: + return jsonify({"message": "Malformed Request: %s" % error.data['message']}), error.code + except HTTPException as error: + return jsonify(error.data), error.code + + try: + data = args['build'] + phase = data['phase'] + ref_url = data['full_url'] + uuid = data['parameters']['uuid'] + except KeyError: + return jsonify({"message": "Malformed Request, missing one of build, phase, full_url, parameters->uuid"}), 400 + + if phase == 'SCHEDULED': + return _update_job(uuid, start=False, ref_url=ref_url) + + if phase == 'STARTED': + return _update_job(uuid, start=True, ref_url=ref_url) + + if phase == 'FINALIZED': + try: + status = data['status'] + except KeyError: + return jsonify({"message": "Malformed Request, missing status"}), 400 + outcome = {'SUCCESS': 'COMPLETED', 'FAILURE': 'FAILED', 'ABORTED': 'ABORTED'}.get(status, 'FAILED') + return _update_job(uuid, ref_url=ref_url, outcome=outcome) + + # ============================================================================= # STEPS # ============================================================================= RP['create_step'] = reqparse.RequestParser() -RP['create_step'].add_argument('name', location='json') -RP['create_step'].add_argument('description', required=True, location='json') +RP['create_step'].add_argument('name', required=True, location='json') +RP['create_step'].add_argument('description', location='json') RP['create_step'].add_argument('duration', location='json') RP['create_step'].add_argument('outcome', location='json') @@ -297,7 +339,7 @@ def create_step(uuid): RP['update_step'] = reqparse.RequestParser() RP['update_step'].add_argument('duration', location='json') -RP['update_step'].add_argument('outcome', default='COMPLETED', location='json') +RP['update_step'].add_argument('outcome', default='FINISHED', location='json') @main.route('/api/v1/jobs//steps/', methods=['POST']) def update_step(uuid, id): diff --git a/execdb/templates/index.html b/execdb/templates/index.html index 4bdcec8..48f7caa 100644 --- a/execdb/templates/index.html +++ b/execdb/templates/index.html @@ -20,7 +20,7 @@ {{job.status}} {% for step in job.steps %} -
'+icon+' '+value.description+'
'+value.duration+'
'; + var field = '
'+icon+' '+value.name+' - '+value.description+'
'+value.duration+'
'; $('#job_progress').append(field); }); var job_icon = ''; From 6f400a17d41f399dcc8aa9381ea0a4db3d64ff9a Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: Jul 13 2017 22:42:40 +0000 Subject: [PATCH 7/10] worky! --- diff --git a/execdb/controllers/main.py b/execdb/controllers/main.py index 11d69d0..995c4aa 100644 --- a/execdb/controllers/main.py +++ b/execdb/controllers/main.py @@ -277,6 +277,8 @@ def jenkins_notification(): outcome = {'SUCCESS': 'COMPLETED', 'FAILURE': 'FAILED', 'ABORTED': 'ABORTED'}.get(status, 'FAILED') return _update_job(uuid, ref_url=ref_url, outcome=outcome) + return jsonify({"message": "Nothing happened"}), 200 + # ============================================================================= # STEPS @@ -338,8 +340,8 @@ def create_step(uuid): RP['update_step'] = reqparse.RequestParser() -RP['update_step'].add_argument('duration', location='json') -RP['update_step'].add_argument('outcome', default='FINISHED', location='json') +RP['update_step'].add_argument('duration', default=None, location='json') +RP['update_step'].add_argument('outcome', default='COMPLETED', location='json') @main.route('/api/v1/jobs//steps/', methods=['POST']) def update_step(uuid, id): diff --git a/execdb/templates/show_job.html b/execdb/templates/show_job.html index 9e85bb4..6a69423 100644 --- a/execdb/templates/show_job.html +++ b/execdb/templates/show_job.html @@ -31,6 +31,11 @@ function show_data(){ }); var job_icon = ''; var job_duration = data.duration; + var job_logs = 'Logs'; + if (data.ref_url){ + job_logs = 'Logs →' + } + if (data.status == 'SCHEDULED'){ job_duration = 'Scheduled to run soon'; job_icon = ''; @@ -55,6 +60,7 @@ function show_data(){ } $('#job_status').html(job_icon); $('#job_duration').text(job_duration); + $('#job_logs').html(job_logs); if (data.status != 'FINISHED'){ setTimeout(show_data, 1000); @@ -103,8 +109,8 @@ $('document').ready(function() { From 09a04933962be6d93089c609b9706cb6ba2b3c4a Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: Jul 13 2017 22:45:31 +0000 Subject: [PATCH 8/10] worky! --- diff --git a/execdb/templates/show_job.html b/execdb/templates/show_job.html index 6a69423..b1a2af7 100644 --- a/execdb/templates/show_job.html +++ b/execdb/templates/show_job.html @@ -32,8 +32,10 @@ function show_data(){ var job_icon = ''; var job_duration = data.duration; var job_logs = 'Logs'; + var job_artifacts = 'Artifacts'; if (data.ref_url){ job_logs = 'Logs →' + job_artifacts = 'Artifacts →' } if (data.status == 'SCHEDULED'){ @@ -61,6 +63,7 @@ function show_data(){ $('#job_status').html(job_icon); $('#job_duration').text(job_duration); $('#job_logs').html(job_logs); + $('#job_artifacts').html(job_artifacts); if (data.status != 'FINISHED'){ setTimeout(show_data, 1000); @@ -110,7 +113,7 @@ $('document').ready(function() { From d4a6c6a2ea9585b24ce53d9e5560eba54cad9006 Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: Jul 14 2017 10:56:00 +0000 Subject: [PATCH 9/10] me worky mucho macho --- diff --git a/execdb/templates/show_job.html b/execdb/templates/show_job.html index b1a2af7..6d95f9b 100644 --- a/execdb/templates/show_job.html +++ b/execdb/templates/show_job.html @@ -35,9 +35,10 @@ function show_data(){ var job_artifacts = 'Artifacts'; if (data.ref_url){ job_logs = 'Logs →' + } + if (data.ref_url && data.status == 'FINISHED'){ job_artifacts = 'Artifacts →' } - if (data.status == 'SCHEDULED'){ job_duration = 'Scheduled to run soon'; job_icon = ''; @@ -114,7 +115,7 @@ $('document').ready(function() {
Progress
- +
From 1bd06a3b519cfda4664b86d843a0c39e6a9b5272 Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: Aug 07 2017 11:22:57 +0000 Subject: [PATCH 10/10] Fix api path in JS --- diff --git a/execdb/templates/show_job.html b/execdb/templates/show_job.html index 6d95f9b..d5df9cb 100644 --- a/execdb/templates/show_job.html +++ b/execdb/templates/show_job.html @@ -7,7 +7,7 @@ function toggle_text_visibility(e){ } function show_data(){ - $.get("/api/v1/jobs/{{job.uuid}}", function(data){ + $.get(window.location.pathname.replace(/\/jobs\//, '/api/v1/jobs/'), function(data){ console.log(data); $('#job_progress').empty(); data.steps.forEach(function(value){