[IMP] runbot: add wake up option on builds

This commit is contained in:
Xavier-Do 2019-06-28 12:17:04 +02:00
parent dbe44e2a76
commit 87c794a9aa
12 changed files with 87 additions and 59 deletions

View File

@ -6,7 +6,7 @@
'author': "Odoo SA",
'website': "http://runbot.odoo.com",
'category': 'Website',
'version': '4.3',
'version': '4.4',
'depends': ['website', 'base'],
'data': [
'security/runbot_security.xml',

View File

@ -45,10 +45,7 @@ class Runbot(Controller):
build_ids = []
if repo:
# FIXME or removeme (filters are broken)
filters = {key: kwargs.get(key, '1') for key in ['waiting', 'pending', 'testing', 'running', 'done', 'deathrow']}
domain = [('repo_id', '=', repo.id)]
domain += [('global_state', '!=', key) for key, value in iter(filters.items()) if value == '0']
if search:
search_domain = []
for to_search in search.split("|"):
@ -113,8 +110,7 @@ class Runbot(Controller):
'testing': build_obj.search_count([('repo_id', '=', repo.id), ('local_state', '=', 'testing')]),
'running': build_obj.search_count([('repo_id', '=', repo.id), ('local_state', '=', 'running')]),
'pending': build_obj.search_count([('repo_id', '=', repo.id), ('local_state', '=', 'pending')]),
'qu': QueryURL('/runbot/repo/' + slug(repo), search=search, refresh=refresh, **filters),
'filters': filters,
'qu': QueryURL('/runbot/repo/' + slug(repo), search=search, refresh=refresh),
'fqdn': fqdn(),
})
@ -138,6 +134,12 @@ class Runbot(Controller):
build._ask_kill()
return werkzeug.utils.redirect('/runbot/repo/%s' % build.repo_id.id + ('?search=%s' % search if search else ''))
@route(['/runbot/build/<int:build_id>/wakeup'], type='http', auth="user", methods=['POST'], csrf=False)
def build_wake_up(self, build_id, search=None, **post):
build = request.env['runbot.build'].sudo().browse(build_id)
build._wake_up()
return werkzeug.utils.redirect('/runbot/repo/%s' % build.repo_id.id + ('?search=%s' % search if search else ''))
@route([
'/runbot/build/<int:build_id>/force',
'/runbot/build/<int:build_id>/force/<int:exact>',

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
def migrate(cr, version):
cr.execute("UPDATE runbot_build SET requested_action='deathrow', local_state='testing' WHERE local_state = 'deathrow'")

View File

@ -21,7 +21,7 @@ from subprocess import CalledProcessError
_logger = logging.getLogger(__name__)
result_order = ['ok', 'warn', 'ko', 'skipped', 'killed', 'manually_killed']
state_order = ['pending', 'testing', 'waiting', 'running', 'deathrow', 'duplicate', 'done']
state_order = ['pending', 'testing', 'waiting', 'running', 'duplicate', 'done']
def make_selection(array):
@ -57,6 +57,8 @@ class runbot_build(models.Model):
local_result = fields.Selection(make_selection(result_order), string='Build Result', oldname='result')
triggered_result = fields.Selection(make_selection(result_order), string='Triggered Result') # triggered by db only
requested_action = fields.Selection([('wake_up', 'To wake up'), ('deathrow', 'To kill')], string='Action requested', index=True)
nb_pending = fields.Integer("Number of pending in queue", default=0)
nb_testing = fields.Integer("Number of test slot use", default=0)
nb_running = fields.Integer("Number of test slot use", default=0)
@ -402,7 +404,7 @@ class runbot_build(models.Model):
else:
sequence = self.search([], order='id desc', limit=1)[0].id
# Force it now
if build.local_state in ['running', 'done', 'duplicate', 'deathrow']:
if build.local_state in ['running', 'done', 'duplicate']:
values = {
'sequence': sequence,
'branch_id': build.branch_id.id,
@ -533,10 +535,21 @@ class runbot_build(models.Model):
for build in self:
self.env.cr.commit() # commit between each build to minimise transactionnal errors due to state computations
self.invalidate_cache()
if build.local_state == 'deathrow':
if build.requested_action == 'deathrow':
build._kill(result='manually_killed')
continue
if build.requested_action == 'wake_up':
if docker_is_running(build._get_docker_name()):
build.write({'requested_action': False, 'local_state': 'running'})
build._log('wake_up', 'Waking up failed, docker is already running', level='SEPARATOR')
else:
log_path = build._path('logs', 'wake_up.txt')
build.write({'job_start': now(), 'job_end': False, 'active_step': False, 'requested_action': False, 'local_state': 'running'})
build._log('wake_up', 'Waking up build', level='SEPARATOR')
self.env['runbot.build.config.step']._run_odoo_run(build, log_path)
continue
if build.local_state == 'pending':
# allocate port and schedule first job
port = self._find_port()
@ -789,7 +802,7 @@ class runbot_build(models.Model):
continue
build._log('kill', 'Kill build %s' % build.dest)
docker_stop(build._get_docker_name())
v = {'local_state': 'done', 'active_step': False, 'duplicate_id': False, 'build_end': now()} # what if duplicate? state done?
v = {'local_state': 'done', 'requested_action': False, 'active_step': False, 'duplicate_id': False, 'build_end': now()} # what if duplicate? state done?
if not build.job_end:
v['job_end'] = now()
if result:
@ -815,12 +828,19 @@ class runbot_build(models.Model):
build._skip()
build._log('_ask_kill', 'Skipping build %s, requested by %s (user #%s)' % (build.dest, user.name, uid))
elif build.local_state in ['testing', 'running']:
build.write({'local_state': 'deathrow'})
build.requested_action = 'deathrow'
build._log('_ask_kill', 'Killing build %s, requested by %s (user #%s)' % (build.dest, user.name, uid))
for child in build.children_ids: # should we filter build that are target of a duplicate_id?
if not child.duplicate_id:
child._ask_kill()
def _wake_up(self):
build = self.real_build
if build.local_state != 'done':
build._log('wake_up', 'Impossibe to wake up, state is not done')
else:
build.requested_action = 'wake_up'
def _get_all_commit(self):
return [Commit(self.repo_id, self.name)] + [Commit(dep._get_repo(), dep.dependency_hash) for dep in self.dependency_ids]
@ -939,6 +959,10 @@ class runbot_build(models.Model):
if not step_ids: # no job to do, build is done
return {'active_step': False, 'local_state': 'done'}
if not self.active_step and self.local_state != 'pending':
# means that a step has been run manually without using config
return {'active_step': False, 'local_state': 'done'}
next_index = step_ids.index(self.active_step) + 1 if self.active_step else 0
if next_index >= len(step_ids): # final job, build is done
return {'active_step': False, 'local_state': 'done'}

View File

@ -303,7 +303,7 @@ class runbot_repo(models.Model):
])
for btk in builds_to_kill:
btk._log('repo._update_git', 'Build automatically killed, newer build found.', level='WARNING')
builds_to_kill.write({'local_state': 'deathrow'})
builds_to_kill.write({'requested_action': 'deathrow'})
new_build = Build.create(build_info)
# create a reverse dependency build if needed
@ -409,7 +409,7 @@ class runbot_repo(models.Model):
domain_host = domain + [('host', '=', host)]
# schedule jobs (transitions testing -> running, kill jobs, ...)
build_ids = Build.search(domain_host + [('local_state', 'in', ['testing', 'running', 'deathrow'])])
build_ids = Build.search(domain_host + ['|', ('local_state', 'in', ['testing', 'running']), ('requested_action', 'in', ['wake_up', 'deathrow'])])
build_ids._schedule()
self.env.cr.commit()
self.invalidate_cache()
@ -467,7 +467,7 @@ class runbot_repo(models.Model):
pending_build._schedule()
# terminate and reap doomed build
build_ids = Build.search(domain_host + [('local_state', '=', 'running')]).ids
build_ids = Build.search(domain_host + [('local_state', '=', 'running')], order='job_start desc').ids
# sort builds: the last build of each sticky branch then the rest
sticky = {}
non_sticky = []

View File

@ -31,5 +31,15 @@
return false;
});
});
$(function() {
$('a.runbot-wakeup').click(function() {
var $f = $('<form method="POST">'),
url = _.str.sprintf('/runbot/build/%s/wakeup', $(this).data('runbot-build')) + window.location.search;
$f.attr('action', url);
$f.appendTo($('body'));
$f.submit();
return false;
});
});
})(jQuery);

View File

@ -2,26 +2,28 @@
<odoo>
<data>
<template id="runbot.build_name">
<t t-if="bu.global_state=='deathrow'"><i class="text-info fa fa-crosshairs"/> killing</t>
<t t-if="bu.global_state=='pending'"><i class="text-default fa fa-pause"/> pending</t>
<t t-if="bu.global_state in ('testing', 'waiting')">
<t t-set="textklass" t-value="dict(ko='danger', warn='warning').get(bu.global_result, 'info')"/>
<t t-if="bu.global_state == 'waiting'">
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small t-if="not hide_time">time: <t t-esc="bu.get_formated_build_time()"/></small>
<t t-if="bu.real_build.requested_action=='deathrow'"><i class="text-info fa fa-crosshairs"/> killing</t>
<t t-if="bu.real_build.requested_action=='wake_up'"><i class="text-info fa fa-coffee"/> waking up</t>
<t t-if="not bu.requested_action">
<t t-if="bu.global_state=='pending'"><i class="text-default fa fa-pause"/> pending</t>
<t t-if="bu.global_state in ('testing', 'waiting')">
<t t-set="textklass" t-value="dict(ko='danger', warn='warning').get(bu.global_result, 'info')"/>
<t t-if="bu.global_state == 'waiting'">
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small t-if="not hide_time">time: <t t-esc="bu.get_formated_build_time()"/></small>
</t>
<t t-else="">
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small> step <t t-esc="bu['job']"/>: </small><small t-if="not hide_time"><t t-esc="bu.get_formated_job_time()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small>
</t>
</t>
<t t-else="">
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small> step <t t-esc="bu['job']"/>: </small><small t-if="not hide_time"><t t-esc="bu.get_formated_job_time()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small>
<t t-if="bu.global_state in ('running', 'done')">
<t t-if="bu.global_result=='ok'"><i class="text-success fa fa-thumbs-up" title="Success" aria-label="Success"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
<t t-if="bu.global_result=='ko'"><i class="text-danger fa fa-thumbs-down" title="Failed" aria-label="Failed"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
<t t-if="bu.global_result=='warn'"><i class="text-warning fa fa-warning" title="Some warnings" aria-label="Some warnings"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
<t t-if="bu.global_result=='skipped'"><i class="text-danger fa fa-ban"/> skipped</t>
<t t-if="bu.global_result=='killed'"><i class="text-danger fa fa-times"/> killed</t>
<t t-if="bu.global_result=='manually_killed'"><i class="text-danger fa fa-times"/> manually killed</t>
</t>
</t>
<t t-if="bu.global_state in ('running', 'done')">
<t t-if="bu.global_result=='ok'"><i class="text-success fa fa-thumbs-up" title="Success" aria-label="Success"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
<t t-if="bu.global_result=='ko'"><i class="text-danger fa fa-thumbs-down" title="Failed" aria-label="Failed"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
<t t-if="bu.global_result=='warn'"><i class="text-warning fa fa-warning" title="Some warnings" aria-label="Some warnings"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
<t t-if="bu.global_result=='skipped'"><i class="text-danger fa fa-ban"/> skipped</t>
<t t-if="bu.global_result=='killed'"><i class="text-danger fa fa-times"/> killed</t>
<t t-if="bu.global_result=='manually_killed'"><i class="text-danger fa fa-times"/> manually killed</t>
</t>
<t t-if="bu.revdep_build_ids">
<small class="pull-right">Dep builds:
<t t-foreach="bu.sorted_revdep_build_ids()" t-as="rbu">
@ -32,7 +34,6 @@
<t t-if="rbu.global_result=='skipped'"><i class="text-danger fa fa-ban"/></t>
<t t-if="rbu.global_result=='killed'"><i class="text-danger fa fa-times"/>&#x1f480;</t>
<t t-if="rbu.global_result=='manually_killed'"><i class="text-danger fa fa-times"/>&#x1f52b;</t>
<t t-if="rbu.global_state=='deathrow'"><i class="fa fa-crosshairs" style="color: #666666;"/></t>
<t t-if="rbu.global_state=='pending'"><i class="fa fa-pause" style="color: #666666;"/></t>
<t t-if="rbu.global_state in ('testing', 'waiting')"><i class="fa fa-spinner fa-spin" style="color: #666666;"/></t>
</a>
@ -63,11 +64,11 @@
<li><a t-attf-href="http://{{bu['domain']}}/?db={{bu['real_build'].dest}}-base">Connect base <i class="fa fa-sign-in"></i></a></li>
<li><a t-attf-href="http://{{bu['domain']}}/">Connect <i class="fa fa-sign-in"></i></a></li>
</t>
<li t-if="bu.global_state in ['done','running','deathrow']" groups="base.group_user">
<li t-if="bu.global_state in ['done','running'] or requested_action == 'deathrow'" groups="base.group_user">
<t t-if="bu_index==0">
<a href="#" class="runbot-rebuild" t-att-data-runbot-build="bu['id']"
title="Create a new build keeping build commit head, but will recompute all other info (config, dependencies, extra_params)">
Default Rebuild <i class="fa fa-refresh"/>
Rebuild <i class="fa fa-refresh"/>
</a>
</t>
<a href="#" class="runbot-rebuild-exact" t-att-data-runbot-build="bu['id']"
@ -75,9 +76,15 @@
Exact Rebuild <i class="fa fa-refresh"/>
</a>
</li>
<li t-if="bu.global_state in ['pending','testing', 'waiting', 'running']" groups="base.group_user">
<a href="#" class="runbot-kill" t-att-data-runbot-build="bu['id']">Kill <i class="fa fa-crosshairs"/></a>
<li t-if="bu.global_state != 'done'" groups="base.group_user">
<a t-if="bu.real_build.requested_action != 'deathrow'" href="#" class="runbot-kill" t-att-data-runbot-build="bu['id']">Kill <i class="fa fa-crosshairs"/></a>
<span t-else="" class="runbot-kill" > Killing <i class="fa fa-spinner fa-spin"/> <i class="fa fa-crosshairs"/></span>
</li>
<li t-if="bu.global_state == 'done'" groups="base.group_user">
<a t-if="bu.real_build.requested_action != 'wake_up'" href="#" class="runbot-wakeup" t-att-data-runbot-build="bu['id']">Wake up <i class="fa fa-coffee"/></a>
<span t-else="" class="runbot-wakeup" > Waking up <i class="fa fa-spinner fa-spin"/> <i class="fa fa-crosshairs"/></span>
</li>
<li t-if="bu.global_state not in ('testing', 'waiting', 'pending')" class="divider"></li>
<li><a t-attf-href="/runbot/build/{{bu['id']}}">Logs <i class="fa fa-file-text-o"/></a></li>
<t t-set="log_url" t-value="'http://%s' % bu.real_build.host if bu.real_build.host != fqdn else ''"/>

View File

@ -47,7 +47,6 @@
<t t-foreach="br['builds']" t-as="bu">
<t t-if="bu.global_state=='pending'"><t t-set="klass">default</t></t>
<t t-if="bu.global_state in ('testing', 'waiting')"><t t-set="klass">info</t></t>
<t t-if="bu.global_state=='deathrow'"><t t-set="klass">default</t></t>
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'ko'"><t t-set="klass">danger</t></t>
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'warn'"><t t-set="klass">warning</t></t>
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'ok'"><t t-set="klass">success</t></t>

View File

@ -68,23 +68,6 @@
<button type="submit" class="btn btn-default">Search</button>
</div>
</form>
<form class="navbar-form navbar-right form-inline">
<div class="btn-group" t-if="repo">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
Filter <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li t-if="filters['pending']=='0'"><a t-att-href="qu(pending=1)">Pending</a></li>
<li t-if="filters['pending']=='1'"><a t-att-href="qu(pending='0')"><i class="fa fa-check"/> Pending</a></li>
<li t-if="filters['testing']=='0'"><a t-att-href="qu(testing=1)">Testing</a></li>
<li t-if="filters['testing']=='1'"><a t-att-href="qu(testing='0')"><i class="fa fa-check"/> Testing</a></li>
<li t-if="filters['running']=='0'"><a t-att-href="qu(running=1)">Running</a></li>
<li t-if="filters['running']=='1'"><a t-att-href="qu(running='0')"><i class="fa fa-check"/> Running</a></li>
<li t-if="filters['done']=='0'"><a t-att-href="qu(done=1)">Done</a></li>
<li t-if="filters['done']=='1'"><a t-att-href="qu(done='0')"><i class="fa fa-check"/> Done</a></li>
</ul>
</div>
</form>
</t>
</div>
<p class="text-center">
@ -144,7 +127,6 @@
<t t-foreach="br['builds']" t-as="bu">
<t t-if="bu.global_state=='pending'"><t t-set="klass">default</t></t>
<t t-if="bu.global_state in ('testing', 'waiting')"><t t-set="klass">info</t></t>
<t t-if="bu.global_state=='deathrow'"><t t-set="klass">default</t></t>
<t t-if="bu.global_state in ['running','done']">
<t t-if="bu.global_result == 'ko'"><t t-set="klass">danger</t></t>
<t t-if="bu.global_result == 'warn'"><t t-set="klass">warning</t></t>

View File

@ -32,11 +32,11 @@ class Test_Frontend(common.HttpCase):
mock_request._cr = self.cr
controller = frontend.Runbot()
states = ['done', 'pending', 'testing', 'running', 'deathrow']
states = ['done', 'pending', 'testing', 'running']
branches = [self.branch, self.sticky_branch]
names = ['deadbeef', 'd0d0caca', 'deadface', 'cacafeed']
# create 5 builds in each branch
for i, state, branch, name in zip(range(10), cycle(states), cycle(branches), cycle(names)):
for i, state, branch, name in zip(range(8), cycle(states), cycle(branches), cycle(names)):
name = '%s%s' % (name, i)
build = self.Build.create({
'branch_id': branch.id,

View File

@ -42,7 +42,7 @@ class TestSchedule(common.TransactionCase):
})
domain = [('repo_id', 'in', (self.repo.id, ))]
domain_host = domain + [('host', '=', 'runbotxx')]
build_ids = self.Build.search(domain_host + [('local_state', 'in', ['testing', 'running', 'deathrow'])])
build_ids = self.Build.search(domain_host + [('local_state', 'in', ['testing', 'running'])])
mock_running.return_value = False
self.assertEqual(build.local_state, 'testing')
build_ids._schedule() # too fast, docker not started

View File

@ -87,7 +87,6 @@
<filter string="Running" domain="[('global_state','=', 'running')]"/>
<filter string="Done" domain="[('global_state','=','done')]"/>
<filter string="Duplicate" domain="[('global_state','=', 'duplicate')]"/>
<filter string="Deathrow" domain="[('global_state','=', 'deathrow')]"/>
<separator />
<group expand="0" string="Group By...">
<filter string="Repo" domain="[]" context="{'group_by':'repo_id'}"/>