mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 23:45:44 +07:00
bzr import runbot and document_fs
This commit is contained in:
commit
5423ea2394
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# dotfiles
|
||||
.*
|
||||
# compiled python files
|
||||
*.py[co]
|
||||
# emacs backup files
|
||||
*~
|
1
runbot/__init__.py
Normal file
1
runbot/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
import runbot
|
14
runbot/__openerp__.py
Normal file
14
runbot/__openerp__.py
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
'name': 'Runbot',
|
||||
'category': 'Website',
|
||||
'summary': 'Runbot',
|
||||
'version': '1.0',
|
||||
'description': "Runbot",
|
||||
'author': 'OpenERP SA',
|
||||
'depends': ['website'],
|
||||
'data': [
|
||||
'runbot.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
835
runbot/runbot.py
Normal file
835
runbot/runbot.py
Normal file
@ -0,0 +1,835 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
import fcntl
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import resource
|
||||
import shutil
|
||||
import signal
|
||||
import simplejson
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import dateutil.parser
|
||||
import requests
|
||||
import werkzeug
|
||||
|
||||
import openerp
|
||||
from openerp import http
|
||||
from openerp.http import request
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.addons.website.models.website import slug
|
||||
from openerp.addons.website_sale.controllers.main import QueryURL
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# RunBot helpers
|
||||
#----------------------------------------------------------
|
||||
|
||||
def log(*l, **kw):
|
||||
out = []
|
||||
for i in l:
|
||||
if not isinstance(i, basestring):
|
||||
i = repr(i)
|
||||
out.append(i)
|
||||
out += ["%s=%r" % (k, v) for k, v in kw.items()]
|
||||
_logger.debug(' '.join(out))
|
||||
|
||||
def dashes(s):
|
||||
for i in '~":\'':
|
||||
s = s.replace(i, "")
|
||||
for i in '/_. ':
|
||||
s = s.replace(i, "-")
|
||||
return s
|
||||
|
||||
def mkdirs(dirs):
|
||||
for i in dirs:
|
||||
if not os.path.exists(i):
|
||||
os.makedirs(i)
|
||||
|
||||
def grep(filename, s):
|
||||
if os.path.isfile(filename):
|
||||
return open(filename).read().find(s) != -1
|
||||
return False
|
||||
|
||||
def lock(name):
|
||||
fd = os.open(name, os.O_CREAT | os.O_RDWR, 0600)
|
||||
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
|
||||
def locked(name):
|
||||
r = False
|
||||
try:
|
||||
fd = os.open(name, os.O_CREAT | os.O_RDWR, 0600)
|
||||
try:
|
||||
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except IOError:
|
||||
r = True
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
r = False
|
||||
return r
|
||||
|
||||
def nowait():
|
||||
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
|
||||
|
||||
def run(l, env=None):
|
||||
log("run", l)
|
||||
env = dict(os.environ, **env) if env else None
|
||||
if isinstance(l, list):
|
||||
if env:
|
||||
rc = os.spawnvpe(os.P_WAIT, l[0], l, env)
|
||||
else:
|
||||
rc = os.spawnvp(os.P_WAIT, l[0], l)
|
||||
elif isinstance(l, str):
|
||||
tmp = ['sh', '-c', l]
|
||||
if env:
|
||||
rc = os.spawnvpe(os.P_WAIT, tmp[0], tmp, env)
|
||||
else:
|
||||
rc = os.spawnvp(os.P_WAIT, tmp[0], tmp)
|
||||
log("run", rc=rc)
|
||||
return rc
|
||||
|
||||
def kill(pid, sig=signal.SIGKILL):
|
||||
try:
|
||||
os.kill(pid, sig)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def now():
|
||||
return time.strftime(openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
def dt2time(dt):
|
||||
return time.mktime(time.strptime(dt, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT))
|
||||
|
||||
def s2human(t):
|
||||
for m,u in [(86400,'d'),(3600,'h'),(60,'m')]:
|
||||
if t>=m:
|
||||
return str(int(t/m))+u
|
||||
return str(int(t))+"s"
|
||||
|
||||
#----------------------------------------------------------
|
||||
# RunBot Models
|
||||
#----------------------------------------------------------
|
||||
|
||||
class runbot_repo(osv.osv):
|
||||
_name = "runbot.repo"
|
||||
_order = 'name'
|
||||
|
||||
def _get_path(self, cr, uid, ids, field_name, arg, context=None):
|
||||
wd = self.root(cr, uid)
|
||||
r = {}
|
||||
for repo in self.browse(cr, uid, ids, context=context):
|
||||
name = repo.name
|
||||
for i in '@:/':
|
||||
name = name.replace(i, '_')
|
||||
r[repo.id] = os.path.join(wd, 'repo', name)
|
||||
return r
|
||||
|
||||
def _get_base(self, cr, uid, ids, field_name, arg, context=None):
|
||||
r = {}
|
||||
for repo in self.browse(cr, uid, ids, context=context):
|
||||
name = re.sub('.+@', '', repo.name)
|
||||
name = name.replace(':','/')
|
||||
r[repo.id] = name
|
||||
return r
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Repository', required=True),
|
||||
'path': fields.function(_get_path, type='char', string='Directory', readonly=1),
|
||||
'base': fields.function(_get_base, type='char', string='Base URL', readonly=1),
|
||||
'testing': fields.integer('Concurrent Testing'),
|
||||
'running': fields.integer('Concurrent Running'),
|
||||
'jobs': fields.char('Jobs'),
|
||||
'nginx': fields.boolean('Nginx'),
|
||||
'auto': fields.boolean('Auto'),
|
||||
'fallback_id': fields.many2one('runbot.repo', 'Fallback repo'),
|
||||
'modules': fields.char("Modules to Install"),
|
||||
'token': fields.char("Github token"),
|
||||
}
|
||||
_defaults = {
|
||||
'testing': 1,
|
||||
'running': 1,
|
||||
'auto': True,
|
||||
}
|
||||
|
||||
def domain(self, cr, uid, context=None):
|
||||
domain = self.pool.get('ir.config_parameter').get_param(cr, uid, 'runbot.domain', 'runbot.odoo.com')
|
||||
return domain
|
||||
|
||||
def root(self, cr, uid, context=None):
|
||||
default = os.path.join(os.path.dirname(__file__), 'static')
|
||||
root = self.pool.get('ir.config_parameter').get_param(cr, uid, 'runbot.root', default)
|
||||
return root
|
||||
|
||||
def git(self, cr, uid, ids, cmd, context=None):
|
||||
for repo in self.browse(cr, uid, ids, context=context):
|
||||
cmd = ['git', '--git-dir=%s' % repo.path] + cmd
|
||||
_logger.info("git: %s", ' '.join(cmd))
|
||||
return subprocess.check_output(cmd)
|
||||
|
||||
def git_export(self, cr, uid, ids, treeish, dest, context=None):
|
||||
for repo in self.browse(cr, uid, ids, context=context):
|
||||
_logger.debug('checkout %s %s %s', repo.name, treeish, dest)
|
||||
p1 = subprocess.Popen(['git', '--git-dir=%s' % repo.path, 'archive', treeish], stdout=subprocess.PIPE)
|
||||
p2 = subprocess.Popen(['tar', '-xC', dest], stdin=p1.stdout, stdout=subprocess.PIPE)
|
||||
p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits.
|
||||
p2.communicate()[0]
|
||||
|
||||
def github(self, cr, uid, ids, url, payload=None, context=None):
|
||||
for repo in self.browse(cr, uid, ids, context=context):
|
||||
mo = re.search('([^/]+)/([^/]+)/([^/]+)', repo.base)
|
||||
if mo:
|
||||
url = url.replace(':owner', mo.group(2))
|
||||
url = url.replace(':repo', mo.group(3))
|
||||
url = 'https://api.%s%s' % (mo.group(1),url)
|
||||
s = requests.Session()
|
||||
s.auth = (repo.token,'x-oauth-basic')
|
||||
s.headers.update({'Accept': 'application/vnd.github.she-hulk-preview+json'})
|
||||
if payload:
|
||||
r = s.post(url, data=simplejson.dumps(payload))
|
||||
else:
|
||||
r = s.get(url)
|
||||
return r.json()
|
||||
|
||||
def update(self, cr, uid, ids, context=None):
|
||||
for repo in self.browse(cr, uid, ids, context=context):
|
||||
self.update_git(cr, uid, repo)
|
||||
|
||||
def update_git(self, cr, uid, repo, context=None):
|
||||
_logger.debug('repo %s updating branches', repo.name)
|
||||
if not os.path.isdir(os.path.join(repo.path)):
|
||||
os.makedirs(repo.path)
|
||||
if not os.path.isdir(os.path.join(repo.path, 'refs')):
|
||||
run(['git', 'clone', '--bare', repo.name, repo.path])
|
||||
else:
|
||||
repo.git(['fetch', '-p', 'origin', '+refs/heads/*:refs/heads/*'])
|
||||
repo.git(['fetch', '-p', 'origin', '+refs/pull/*/merge:refs/pull/*'])
|
||||
out = repo.git(['for-each-ref', '--format', '["%(refname)","%(objectname)","%(authordate:iso8601)"]', '--sort=-committerdate', 'refs/heads'])
|
||||
refs = [simplejson.loads(i) for i in out.split('\n') if i]
|
||||
out = repo.git(['for-each-ref', '--format', '["%(refname)","%(objectname)","%(authordate:iso8601)"]', '--sort=-committerdate', 'refs/pull'])
|
||||
refs += [simplejson.loads(i) for i in out.split('\n') if i]
|
||||
for name, sha, date in refs:
|
||||
# create or get branch
|
||||
branch_ids = self.pool['runbot.branch'].search(cr, uid, [('repo_id', '=', repo.id), ('name', '=', name)])
|
||||
if branch_ids:
|
||||
branch_id = branch_ids[0]
|
||||
else:
|
||||
_logger.debug('repo %s found new branch %s', repo.name, name)
|
||||
branch_id = self.pool['runbot.branch'].create(cr, uid, {'repo_id': repo.id, 'name': name})
|
||||
branch = self.pool['runbot.branch'].browse(cr, uid, [branch_id], context=context)[0]
|
||||
# skip build for old branches
|
||||
if dateutil.parser.parse(date[:19]) + datetime.timedelta(30) < datetime.datetime.now():
|
||||
continue
|
||||
# create build if not found
|
||||
build_ids = self.pool['runbot.build'].search(cr, uid, [('branch_id', '=', branch.id), ('name', '=', sha)])
|
||||
if not build_ids:
|
||||
_logger.debug('repo %s branch %s new build found revno %s', branch.repo_id.name, branch.name, sha)
|
||||
self.pool['runbot.build'].create(cr, uid, {'branch_id': branch.id, 'name': sha})
|
||||
|
||||
def scheduler(self, cr, uid, ids=None, context=None):
|
||||
for repo in self.browse(cr, uid, ids, context=context):
|
||||
bo = self.pool['runbot.build']
|
||||
dom = [('repo_id', '=', repo.id)]
|
||||
|
||||
# schedule jobs
|
||||
build_ids = bo.search(cr, uid, dom + [('state', 'in', ['testing', 'running'])])
|
||||
bo.schedule(cr, uid, build_ids)
|
||||
|
||||
# launch new tests
|
||||
testing = bo.search(cr, uid, dom + [('state', '=', 'testing')], count=True)
|
||||
while testing < repo.testing:
|
||||
# select the next build to process
|
||||
pending_ids = bo.search(cr, uid, dom + [('state', '=', 'pending')])
|
||||
if pending_ids:
|
||||
pending = bo.browse(cr, uid, pending_ids[0])
|
||||
else:
|
||||
break
|
||||
|
||||
# gather information about currently running builds
|
||||
running_ids = bo.search(cr, uid, dom + [('state', '=', 'running')])
|
||||
running_len = len(running_ids)
|
||||
running_max = 0
|
||||
if running_ids:
|
||||
running_max = bo.browse(cr, uid, running_ids[0]).sequence
|
||||
|
||||
# determine if pending one should be launched
|
||||
if running_len < repo.running or pending.sequence >= running_max:
|
||||
pending.schedule()
|
||||
else:
|
||||
break
|
||||
|
||||
# compute the number of testing job again
|
||||
testing = bo.search(cr, uid, dom + [('state', '=', 'testing')], count=True)
|
||||
|
||||
# kill and reap doomed build
|
||||
build_ids = bo.search(cr, uid, dom + [('state', '=', 'running')])
|
||||
# sort builds: the last build of each sticky branch then the rest
|
||||
sticky = {}
|
||||
non_sticky = []
|
||||
for build in bo.browse(cr, uid, build_ids):
|
||||
if build.branch_id.sticky and build.branch_id.id not in sticky:
|
||||
sticky[build.branch_id.id] = build.id
|
||||
else:
|
||||
non_sticky.append(build.id)
|
||||
build_ids = sticky.values()
|
||||
build_ids += non_sticky
|
||||
# kill extra running builds
|
||||
bo.kill(cr, uid, build_ids[repo.running:])
|
||||
bo.reap(cr, uid, build_ids)
|
||||
|
||||
def nginx(self, cr, uid, context=None):
|
||||
v = {}
|
||||
v['port'] = openerp.tools.config['xmlrpc_port']
|
||||
nginx_dir = os.path.join(self.root(cr, uid), 'nginx')
|
||||
v['nginx_dir'] = nginx_dir
|
||||
ids = self.search(cr, uid, [('nginx','=',True)], order='id')
|
||||
if ids:
|
||||
build_ids = self.pool['runbot.build'].search(cr, uid, [('repo_id','in',ids), ('state','=','running')])
|
||||
v['builds'] = self.pool['runbot.build'].browse(cr, uid, build_ids)
|
||||
|
||||
nginx_config = self.pool['ir.ui.view'].render(cr, uid, "runbot.nginx_config", v)
|
||||
mkdirs([nginx_dir])
|
||||
open(os.path.join(nginx_dir, 'nginx.conf'),'w').write(nginx_config)
|
||||
try:
|
||||
_logger.debug('reload nginx')
|
||||
pid = int(open(os.path.join(nginx_dir, 'nginx.pid')).read().strip(' \n'))
|
||||
os.kill(pid, signal.SIGHUP)
|
||||
except Exception:
|
||||
_logger.debug('start nginx')
|
||||
run(['/usr/sbin/nginx', '-p', nginx_dir, '-c', 'nginx.conf'])
|
||||
|
||||
def killall(self, cr, uid, ids=None, context=None):
|
||||
# kill switch
|
||||
bo = self.pool['runbot.build']
|
||||
build_ids = bo.search(cr, uid, [('state', 'not in', ['done', 'pending'])])
|
||||
bo.kill(cr, uid, build_ids)
|
||||
bo.reap(cr, uid, build_ids)
|
||||
|
||||
def cron(self, cr, uid, ids=None, context=None):
|
||||
ids = self.search(cr, uid, [('auto', '=', True)])
|
||||
self.update(cr, uid, ids)
|
||||
self.scheduler(cr, uid, ids)
|
||||
self.nginx(cr, uid)
|
||||
|
||||
class runbot_branch(osv.osv):
|
||||
_name = "runbot.branch"
|
||||
_order = 'name'
|
||||
|
||||
def _get_branch_name(self, cr, uid, ids, field_name, arg, context=None):
|
||||
r = {}
|
||||
for branch in self.browse(cr, uid, ids, context=context):
|
||||
r[branch.id] = branch.name.split('/')[-1]
|
||||
return r
|
||||
|
||||
def _get_branch_url(self, cr, uid, ids, field_name, arg, context=None):
|
||||
r = {}
|
||||
for branch in self.browse(cr, uid, ids, context=context):
|
||||
if re.match('^[0-9]+$', branch.branch_name):
|
||||
r[branch.id] = "https://%s/pull/%s" % (branch.repo_id.base, branch.branch_name)
|
||||
else:
|
||||
r[branch.id] = "https://%s/tree/%s" % (branch.repo_id.base, branch.branch_name)
|
||||
return r
|
||||
|
||||
_columns = {
|
||||
'repo_id': fields.many2one('runbot.repo', 'Repository', required=True, ondelete='cascade'),
|
||||
'name': fields.char('Ref Name', required=True),
|
||||
'branch_name': fields.function(_get_branch_name, type='char', string='Branch', readonly=1, store=True),
|
||||
'branch_url': fields.function(_get_branch_url, type='char', string='Branch url', readonly=1),
|
||||
'sticky': fields.boolean('Sticky'),
|
||||
'coverage': fields.boolean('Coverage'),
|
||||
'state': fields.char('Status'),
|
||||
}
|
||||
|
||||
class runbot_build(osv.osv):
|
||||
_name = "runbot.build"
|
||||
_order = 'sequence desc'
|
||||
|
||||
def _get_dest(self, cr, uid, ids, field_name, arg, context=None):
|
||||
r = {}
|
||||
for build in self.browse(cr, uid, ids, context=context):
|
||||
nickname = dashes(build.branch_id.name.split('/')[2])[:32]
|
||||
r[build.id] = "%05d-%s-%s" % (build.id, nickname, build.name[:6])
|
||||
return r
|
||||
|
||||
def _get_time(self, cr, uid, ids, field_name, arg, context=None):
|
||||
r = {}
|
||||
for build in self.browse(cr, uid, ids, context=context):
|
||||
r[build.id] = 0
|
||||
if build.job_end:
|
||||
r[build.id] = int(dt2time(build.job_end) - dt2time(build.job_start))
|
||||
elif build.job_start:
|
||||
r[build.id] = int(time.time() - dt2time(build.job_start))
|
||||
return r
|
||||
|
||||
def _get_age(self, cr, uid, ids, field_name, arg, context=None):
|
||||
r = {}
|
||||
for build in self.browse(cr, uid, ids, context=context):
|
||||
r[build.id] = 0
|
||||
if build.job_start:
|
||||
r[build.id] = int(time.time() - dt2time(build.job_start))
|
||||
return r
|
||||
|
||||
def _get_domain(self, cr, uid, ids, field_name, arg, context=None):
|
||||
r = {}
|
||||
domain = self.pool['runbot.repo'].domain(cr, uid)
|
||||
for build in self.browse(cr, uid, ids, context=context):
|
||||
if build.repo_id.nginx:
|
||||
r[build.id] = "%s.%s" % (build.dest, domain)
|
||||
else:
|
||||
r[build.id] = "%s:%s" % (domain, build.port)
|
||||
return r
|
||||
|
||||
_columns = {
|
||||
'branch_id': fields.many2one('runbot.branch', 'Branch', required=True, ondelete='cascade'),
|
||||
'repo_id': fields.related('branch_id', 'repo_id', type="many2one", relation="runbot.repo", string="Repository", readonly=True, store=True, ondelete='cascade'),
|
||||
'name': fields.char('Revno', required=True),
|
||||
'port': fields.integer('Port'),
|
||||
'dest': fields.function(_get_dest, type='char', string='Dest', readonly=1, store=True),
|
||||
'domain': fields.function(_get_domain, type='char', string='URL'),
|
||||
'date': fields.datetime('Commit date'),
|
||||
'committer': fields.char('Comitter'),
|
||||
'log': fields.text('Commit log'),
|
||||
'sequence': fields.integer('Sequence'),
|
||||
'result': fields.char('Result'), # ok, ko
|
||||
'pid': fields.integer('Pid'),
|
||||
'state': fields.char('Status'), # pending, testing, running, done
|
||||
'job': fields.char('Job'), # job_*
|
||||
'job_start': fields.datetime('Job start'),
|
||||
'job_end': fields.datetime('Job end'),
|
||||
'job_time': fields.function(_get_time, type='integer', string='Job time'),
|
||||
'job_age': fields.function(_get_age, type='integer', string='Job age'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'state': 'pending',
|
||||
}
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
bid = super(runbot_build, self).create(cr, uid, values, context=context)
|
||||
self.write(cr, uid, [bid], {'sequence' : bid}, context=context)
|
||||
|
||||
def reset(self, cr, uid, ids, context=None):
|
||||
self.write(cr, uid, ids, { 'state' : 'pending' }, context=context)
|
||||
|
||||
def logger(self, cr, uid, ids, *l, **kw):
|
||||
l = list(l)
|
||||
for build in self.browse(cr, uid, ids, **kw):
|
||||
l[0] = "%s %s" % (build.dest , l[0])
|
||||
_logger.debug(*l)
|
||||
|
||||
def list_jobs(self):
|
||||
jobs = [i for i in dir(self) if i.startswith('job')]
|
||||
jobs.sort()
|
||||
return jobs
|
||||
|
||||
def find_port(self, cr, uid):
|
||||
# currently used port
|
||||
ids = self.search(cr, uid, [('state','not in',['pending','done'])])
|
||||
ports = set(i['port'] for i in self.read(cr, uid, ids, ['port']))
|
||||
|
||||
# starting port
|
||||
# TODO take ir.config.parameters or 9000
|
||||
port = 2000
|
||||
|
||||
# find next free port
|
||||
while port in ports:
|
||||
port += 2
|
||||
|
||||
return port
|
||||
|
||||
def path(self, cr, uid, ids, *l, **kw):
|
||||
for build in self.browse(cr, uid, ids, context=None):
|
||||
root = self.pool['runbot.repo'].root(cr, uid)
|
||||
return os.path.join(root, 'build', build.dest, *l)
|
||||
|
||||
def checkout(self, cr, uid, ids, context=None):
|
||||
for build in self.browse(cr, uid, ids, context=context):
|
||||
# starts from scratch
|
||||
if os.path.isdir(build.path()):
|
||||
shutil.rmtree(build.path())
|
||||
|
||||
# runbot log path
|
||||
mkdirs([build.path("logs"), build.path('openerp/addons')])
|
||||
|
||||
# checkout branch
|
||||
build.branch_id.repo_id.git_export(build.name, build.path())
|
||||
|
||||
# v6 rename bin -> openerp
|
||||
if os.path.isdir(build.path('bin/addons')):
|
||||
shutil.move(build.path('bin'), build.path('openerp'))
|
||||
|
||||
# fallback for addons-only community/projet branches
|
||||
if not os.path.isfile(build.path('openerp/__init__.py')):
|
||||
l = glob.glob(build.path('*/__openerp__.py'))
|
||||
for i in l:
|
||||
shutil.move(os.path.dirname(i), build.path('openerp/addons'))
|
||||
name = build.branch_id.branch_name.split('-',1)[0]
|
||||
if build.repo_id.fallback_id:
|
||||
build.repo_id.fallback_id.git_export(name, build.path())
|
||||
|
||||
# move all addons to server addons path
|
||||
for i in glob.glob(build.path('addons/*')):
|
||||
shutil.move(i, build.path('openerp/addons'))
|
||||
|
||||
def pg_dropdb(self, cr, uid, dbname):
|
||||
pid_col = 'pid' if cr._cnx.server_version >= 90200 else 'procpid'
|
||||
cr.execute("select pg_terminate_backend(%s) from pg_stat_activity where datname = %%s" % pid_col, (dbname,))
|
||||
time.sleep(1)
|
||||
try:
|
||||
openerp.service.db.exp_drop(dbname)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def pg_createdb(self, cr, uid, dbname):
|
||||
self.pg_dropdb(cr, uid, dbname)
|
||||
_logger.debug("createdb %s",dbname)
|
||||
openerp.service.db._create_empty_database(dbname)
|
||||
|
||||
def cmd(self, cr, uid, ids, context=None):
|
||||
for build in self.browse(cr, uid, ids, context=context):
|
||||
# Server
|
||||
server_path = build.path("openerp-server")
|
||||
# for 7.0
|
||||
if not os.path.isfile(server_path):
|
||||
server_path = build.path("openerp-server.py")
|
||||
# for 6.0 branches
|
||||
if not os.path.isfile(server_path):
|
||||
server_path = build.path("bin/openerp-server.py")
|
||||
|
||||
# modules
|
||||
if build.repo_id.modules:
|
||||
mods = build.repo_id.modules
|
||||
else:
|
||||
l = glob.glob(build.path('openerp/addons/*/__init__.py'))
|
||||
mods = set([os.path.basename(os.path.dirname(i)) for i in l])
|
||||
mods = mods - set(['auth_ldap', 'document_ftp', 'hw_escpos', 'hw_proxy', 'hw_scanner'])
|
||||
mods = ",".join(list(mods))
|
||||
|
||||
# commandline
|
||||
cmd = [
|
||||
server_path,
|
||||
"--no-xmlrpcs",
|
||||
"--xmlrpc-port=%d" % build.port,
|
||||
]
|
||||
# options
|
||||
if grep(build.path("openerp/tools/config.py"), "no-netrpc"):
|
||||
cmd.append("--no-netrpc")
|
||||
if grep(build.path("openerp/tools/config.py"), "log-db"):
|
||||
cmd += ["--log-db=%s" % cr.dbname]
|
||||
|
||||
# coverage
|
||||
#coverage_file_path=os.path.join(log_path,'coverage.pickle')
|
||||
#coverage_base_path=os.path.join(log_path,'coverage-base')
|
||||
#coverage_all_path=os.path.join(log_path,'coverage-all')
|
||||
#cmd = ["coverage","run","--branch"] + cmd
|
||||
#self.run_log(cmd, logfile=self.test_all_path)
|
||||
#run(["coverage","html","-d",self.coverage_base_path,"--ignore-errors","--include=*.py"],env={'COVERAGE_FILE': self.coverage_file_path})
|
||||
|
||||
return cmd, mods
|
||||
|
||||
def spawn(self, cmd, lock_path, log_path, cpu_limit=None, shell=False):
|
||||
def preexec_fn():
|
||||
if cpu_limit:
|
||||
# set soft cpulimit
|
||||
soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
|
||||
r = resource.getrusage(resource.RUSAGE_SELF)
|
||||
cpu_time = r.ru_utime + r.ru_stime
|
||||
resource.setrlimit(resource.RLIMIT_CPU, (cpu_time + cpu_limit, hard))
|
||||
# close parent files
|
||||
os.closerange(3, os.sysconf("SC_OPEN_MAX"))
|
||||
lock(lock_path)
|
||||
out=open(log_path,"w")
|
||||
_logger.debug("spawn: %s stdout: %s", ' '.join(cmd), log_path)
|
||||
p=subprocess.Popen(cmd, stdout=out, stderr=out, preexec_fn=preexec_fn, shell=shell)
|
||||
return p.pid
|
||||
|
||||
def job_10_test_base(self, cr, uid, build, lock_path, log_path):
|
||||
# checkout source
|
||||
build.checkout()
|
||||
# run base test
|
||||
self.pg_createdb(cr, uid, "%s-base" % build.dest)
|
||||
cmd, mods = build.cmd()
|
||||
if grep(build.path("openerp/tools/config.py"), "test-enable"):
|
||||
cmd.append("--test-enable")
|
||||
cmd += ['-d', '%s-base' % build.dest, '-i', 'base', '--stop-after-init', '--log-level=test']
|
||||
return self.spawn(cmd, lock_path, log_path, cpu_limit=300)
|
||||
|
||||
def job_20_test_all(self, cr, uid, build, lock_path, log_path):
|
||||
self.pg_createdb(cr, uid, "%s-all" % build.dest)
|
||||
cmd, mods = build.cmd()
|
||||
if grep(build.path("openerp/tools/config.py"), "test-enable"):
|
||||
cmd.append("--test-enable")
|
||||
cmd += ['-d', '%s-all' % build.dest, '-i', mods, '--stop-after-init', '--log-level=test']
|
||||
# reset job_start to an accurate job_20 job_time
|
||||
build.write({'job_start': now()})
|
||||
return self.spawn(cmd, lock_path, log_path, cpu_limit=1800)
|
||||
|
||||
def job_30_run(self, cr, uid, build, lock_path, log_path):
|
||||
# adjust job_end to record an accurate job_20 job_time
|
||||
log_all = build.path('logs', 'job_20_test_all.txt')
|
||||
log_time = time.localtime(os.path.getmtime(log_all))
|
||||
v = {
|
||||
'job_end': time.strftime(openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT, log_time),
|
||||
'result': 'ko',
|
||||
}
|
||||
# TODO mark as failed too long post_install (after Modules loaded.)
|
||||
if grep(log_all, "openerp.modules.loading: Modules loaded."):
|
||||
if not grep(log_all, "FAIL"):
|
||||
v['result'] = "ok"
|
||||
build.write(v)
|
||||
|
||||
# try to update github
|
||||
try:
|
||||
state = "success" if v['result'] == 'ok' else "failure"
|
||||
status = {
|
||||
"state": state,
|
||||
"target_url": "http://runbot.odoo.com/runbot/build/%s" % build.name,
|
||||
"description": "runbot build %s (runtime %ss)" % (build.dest, build.job_time),
|
||||
"context": "continuous-integration/runbot"
|
||||
}
|
||||
build.repo_id.github('/repos/:owner/:repo/statuses/%s' % build.name, status)
|
||||
_logger.debug("github status %s update to %s", build.name, state)
|
||||
except Exception, e:
|
||||
_logger.exception("github status error")
|
||||
|
||||
# run server
|
||||
cmd, mods = build.cmd()
|
||||
if os.path.exists(build.path('openerp/addons/im')):
|
||||
cmd += ["--workers", "2"]
|
||||
cmd += ["--longpolling-port", "%d" % (build.port + 1)]
|
||||
cmd += ["--max-cron-threads", "1"]
|
||||
else:
|
||||
# not sure, to avoid old server to check other dbs
|
||||
cmd += ["--max-cron-threads", "0"]
|
||||
|
||||
cmd += ['--log-level=debug']
|
||||
cmd += ['-d', "%s-all" % build.dest]
|
||||
|
||||
if grep(build.path("openerp/tools/config.py"), "db-filter"):
|
||||
if build.repo_id.nginx:
|
||||
cmd += ['--db-filter','%d.*$']
|
||||
else:
|
||||
cmd += ['--db-filter','%s.*$' % build.dest]
|
||||
|
||||
## Web60
|
||||
#self.client_web_path=os.path.join(self.running_path,"client-web")
|
||||
#self.client_web_bin_path=os.path.join(self.client_web_path,"openerp-web.py")
|
||||
#self.client_web_doc_path=os.path.join(self.client_web_path,"doc")
|
||||
#webclient_config % (self.client_web_port+port,self.server_net_port+port,self.server_net_port+port)
|
||||
#cfgs = [os.path.join(self.client_web_path,"doc","openerp-web.cfg"), os.path.join(self.client_web_path,"openerp-web.cfg")]
|
||||
#for i in cfgs:
|
||||
# f=open(i,"w")
|
||||
# f.write(config)
|
||||
# f.close()
|
||||
#cmd=[self.client_web_bin_path]
|
||||
|
||||
return self.spawn(cmd, lock_path, log_path, cpu_limit=None)
|
||||
|
||||
def force(self, cr, uid, ids, context=None):
|
||||
for build in self.browse(cr, uid, ids, context=context):
|
||||
if build.state in ['running']:
|
||||
build.kill()
|
||||
max_ids = self.search(cr, uid, [('repo_id','=',build.repo_id.id)], order='id desc', limit=1)
|
||||
build.write({
|
||||
'sequence':max_ids[0],
|
||||
'state':'pending',
|
||||
'result':'',
|
||||
})
|
||||
return build.repo_id.id
|
||||
|
||||
def schedule(self, cr, uid, ids, context=None):
|
||||
jobs = self.list_jobs()
|
||||
for build in self.browse(cr, uid, ids, context=context):
|
||||
if build.state == 'pending':
|
||||
# allocate port and schedule first job
|
||||
port = self.find_port(cr, uid)
|
||||
values = {
|
||||
'port': port,
|
||||
'state': 'testing',
|
||||
'job': jobs[0],
|
||||
'job_start': now(),
|
||||
'job_end': False,
|
||||
}
|
||||
build.write(values)
|
||||
else:
|
||||
# check if current job is finished
|
||||
lock_path = build.path('logs', '%s.lock' % build.job)
|
||||
if locked(lock_path):
|
||||
# kill if overpassed
|
||||
if build.job != jobs[-1] and build.job_time > 1800:
|
||||
build.logger('%s time exceded (%ss)', build.job, build.job_time)
|
||||
kill(build.pid)
|
||||
continue
|
||||
build.logger('%s finished', build.job)
|
||||
# schedule
|
||||
v = {}
|
||||
# testing -> running
|
||||
if build.job == jobs[-2]:
|
||||
v['state'] = 'running'
|
||||
v['job'] = jobs[-1]
|
||||
v['job_end'] = now(),
|
||||
# running -> done
|
||||
elif build.job == jobs[-1]:
|
||||
v['state'] = 'done'
|
||||
v['job'] = ''
|
||||
# testing
|
||||
else:
|
||||
v['job'] = jobs[jobs.index(build.job) + 1]
|
||||
build.write(v)
|
||||
build.refresh()
|
||||
|
||||
# run job
|
||||
if build.state != 'done':
|
||||
build.logger('running %s', build.job)
|
||||
job_method = getattr(self,build.job)
|
||||
lock_path = build.path('logs', '%s.lock' % build.job)
|
||||
log_path = build.path('logs', '%s.txt' % build.job)
|
||||
pid = job_method(cr, uid, build, lock_path, log_path)
|
||||
build.write({'pid': pid})
|
||||
# needed to prevent losing pids if multiple jobs are started and one them raise an exception
|
||||
cr.commit()
|
||||
|
||||
def kill(self, cr, uid, ids, context=None):
|
||||
for build in self.browse(cr, uid, ids, context=context):
|
||||
build.logger('killing %s', build.pid)
|
||||
kill(build.pid)
|
||||
build.write({'state':'done'})
|
||||
cr.commit()
|
||||
self.pg_dropdb(cr, uid, "%s-base" % build.dest)
|
||||
self.pg_dropdb(cr, uid, "%s-all" % build.dest)
|
||||
if os.path.isdir(build.path()):
|
||||
shutil.rmtree(build.path())
|
||||
|
||||
def reap(self, cr, uid, ids):
|
||||
while True:
|
||||
try:
|
||||
pid, status, rusage = os.wait3(os.WNOHANG)
|
||||
except OSError:
|
||||
break
|
||||
if pid == 0:
|
||||
break
|
||||
_logger.debug('reaping: pid: %s status: %s', pid, status)
|
||||
|
||||
class runbot_event(osv.osv):
|
||||
_inherit = 'ir.logging'
|
||||
_order = 'id desc'
|
||||
_columns = {
|
||||
'build_id': fields.many2one('runbot.build', 'Build'),
|
||||
}
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Runbot Controller
|
||||
#----------------------------------------------------------
|
||||
|
||||
class RunbotController(http.Controller):
|
||||
|
||||
def common(self, cr, uid):
|
||||
registry, cr, uid, context = request.registry, request.cr, request.uid, request.context
|
||||
repo_obj = registry['runbot.repo']
|
||||
v = {}
|
||||
ids = repo_obj.search(cr, uid, [], order='id')
|
||||
v['repos'] = repo_obj.browse(cr, uid, ids)
|
||||
v['s2h'] = s2human
|
||||
return v
|
||||
|
||||
@http.route(['/runbot', '/runbot/repo/<model("runbot.repo"):repo>'], type='http', auth="public", website=True)
|
||||
def repo(self, repo=None, search='', limit='100', refresh='', **post):
|
||||
registry, cr, uid, context = request.registry, request.cr, 1, request.context
|
||||
branch_obj = registry['runbot.branch']
|
||||
build_obj = registry['runbot.build']
|
||||
v = self.common(cr, uid)
|
||||
# repo
|
||||
if not repo and v['repos']:
|
||||
repo = v['repos'][0]
|
||||
if repo:
|
||||
# filters
|
||||
dom = [('repo_id','=',repo.id)]
|
||||
filters = {}
|
||||
for k in ['pending','testing','running','done']:
|
||||
filters[k] = post.get(k, '1')
|
||||
if filters[k] == '0':
|
||||
dom += [('state','!=',k)]
|
||||
if search:
|
||||
dom += [('dest','ilike',search)]
|
||||
v['filters'] = filters
|
||||
qu = QueryURL('/runbot/repo/'+slug(repo), search=search, limit=limit, refresh=refresh, **filters)
|
||||
v['qu'] = qu
|
||||
build_ids = build_obj.search(cr, uid, dom + [('branch_id.sticky','=',True)])
|
||||
build_ids += build_obj.search(cr, uid, dom + [('branch_id.sticky','=',False)], limit=int(limit))
|
||||
|
||||
branch_ids = []
|
||||
# builds and branches, order on join SQL is needed
|
||||
q = """
|
||||
SELECT br.id FROM runbot_branch br INNER JOIN runbot_build bu ON br.id=bu.branch_id WHERE bu.id in %s
|
||||
ORDER BY br.sticky DESC, CASE WHEN br.sticky THEN br.branch_name END, bu.sequence DESC
|
||||
"""
|
||||
if build_ids:
|
||||
cr.execute(q, (tuple(build_ids),))
|
||||
for br in cr.fetchall():
|
||||
if br[0] not in branch_ids:
|
||||
branch_ids.append(br[0])
|
||||
|
||||
branches = branch_obj.browse(cr, uid, branch_ids, context=context)
|
||||
for branch in branches:
|
||||
build_ids = build_obj.search(cr, uid, [('branch_id','=',branch.id)], limit=4)
|
||||
branch.builds = build_obj.browse(cr, uid, build_ids, context=context)
|
||||
v['branches'] = branches
|
||||
|
||||
# stats
|
||||
v['testing'] = build_obj.search(cr, uid, [('repo_id','=',repo.id), ('state','=','testing')], count=True)
|
||||
v['running'] = build_obj.search(cr, uid, [('repo_id','=',repo.id), ('state','=','running')], count=True)
|
||||
v['pending'] = build_obj.search(cr, uid, [('repo_id','=',repo.id), ('state','=','pending')], count=True)
|
||||
|
||||
v.update({
|
||||
'search': search,
|
||||
'limit': limit,
|
||||
'refresh': refresh,
|
||||
'repo': repo,
|
||||
})
|
||||
return request.render("runbot.repo", v)
|
||||
|
||||
@http.route(['/runbot/build/<sha>'], type='http', auth="public", website=True)
|
||||
def build(self, sha=None, search=None, **post):
|
||||
registry, cr, uid, context = request.registry, request.cr, 1, request.context
|
||||
|
||||
build_ids = registry['runbot.build'].search(cr, uid, [('name', '=', sha)])
|
||||
build = registry['runbot.build'].browse(cr, uid, build_ids)[0]
|
||||
|
||||
# other builds
|
||||
build_ids = registry['runbot.build'].search(cr, uid, [('branch_id', '=', build.branch_id.id)])
|
||||
other_builds = registry['runbot.build'].browse(cr, uid, build_ids)
|
||||
|
||||
domain = [('dbname', '=', '%s-all' % build.dest)]
|
||||
#if type:
|
||||
# domain.append(('type', '=', type))
|
||||
#if level:
|
||||
# domain.append(('level', '=', level))
|
||||
if search:
|
||||
domain.append(('name', 'ilike', search))
|
||||
logging_ids = registry['ir.logging'].search(cr, uid, domain)
|
||||
logs = registry['ir.logging'].browse(cr, uid, logging_ids)
|
||||
|
||||
v = self.common(cr, uid)
|
||||
#v['type'] = type
|
||||
#v['level'] = level
|
||||
v['build'] = build
|
||||
v['other_builds'] = other_builds
|
||||
v['logs'] = logs
|
||||
return request.render("runbot.build", v)
|
||||
|
||||
@http.route(['/runbot/build/<build_id>/force'], type='http', auth="public", website=True)
|
||||
def build_force(self, build_id, **post):
|
||||
registry, cr, uid, context = request.registry, request.cr, 1, request.context
|
||||
repo_id = registry['runbot.build'].force(cr, 1, [int(build_id)])
|
||||
return werkzeug.utils.redirect('/runbot/repo/%s' % repo_id)
|
||||
|
||||
# kill ` ps faux | grep ./static | awk '{print $2}' `
|
||||
# ps faux| grep Cron | grep -v '_' | awk '{print $2}' | xargs kill
|
||||
# psql -l | grep " 000" | awk '{print $1}' | xargs -n1 dropdb
|
||||
# TODO
|
||||
# - host field in build
|
||||
# vim:
|
534
runbot/runbot.xml
Normal file
534
runbot/runbot.xml
Normal file
@ -0,0 +1,534 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<menuitem id="menu_runbot_top" name="Runbot"/>
|
||||
<menuitem id="menu_runbot" name="Runbot" parent="menu_runbot_top"/>
|
||||
|
||||
<!-- repos -->
|
||||
<record id="view_repo_form" model="ir.ui.view">
|
||||
<field name="model">runbot.repo</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="repos" version="7.0">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" class="oe_inline"/></h1>
|
||||
<button name="update" type="object" string="Update"/>
|
||||
<button name="cron" type="object" string="Cron"/>
|
||||
<button name="killall" type="object" string="Killall"/>
|
||||
</div>
|
||||
<group string="Params">
|
||||
<field name="testing"/>
|
||||
<field name="running"/>
|
||||
<field name="auto"/>
|
||||
<field name="jobs"/>
|
||||
<field name="nginx"/>
|
||||
<field name="fallback_id"/>
|
||||
<field name="modules"/>
|
||||
<field name="token"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_repo_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.repo</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="repos">
|
||||
<field name="name"/>
|
||||
<field name="testing"/>
|
||||
<field name="running"/>
|
||||
<field name="auto"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_repo_form" model="ir.actions.act_window">
|
||||
<field name="name">Repositories</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.repo</field>
|
||||
<field name="view_type">form</field>
|
||||
</record>
|
||||
<menuitem id="menu_repo_form" action="action_repo_form" parent="menu_runbot"/>
|
||||
|
||||
<!-- Branches -->
|
||||
<record id="view_branch_form" model="ir.ui.view">
|
||||
<field name="model">runbot.branch</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Branch" version="7.0">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="repo_id"/>
|
||||
<field name="name"/>
|
||||
<field name="branch_name"/>
|
||||
<field name="branch_url"/>
|
||||
<field name="sticky"/>
|
||||
<field name="state"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_branch_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.branch</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Branches">
|
||||
<field name="repo_id"/>
|
||||
<field name="name"/>
|
||||
<field name="sticky"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_branch_search" model="ir.ui.view">
|
||||
<field name="model">runbot.branch</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search builds">
|
||||
<field name="name"/>
|
||||
<field name="state"/>
|
||||
<filter string="Sticky" domain="[('sticky','=', True)]"/>
|
||||
<separator />
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Repo" domain="[]" context="{'group_by':'repo_id'}"/>
|
||||
<filter string="Status" domain="[]" context="{'group_by':'state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_branch" model="ir.actions.act_window">
|
||||
<field name="name">Branches</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.branch</field>
|
||||
<field name="view_type">form</field>
|
||||
</record>
|
||||
<menuitem id="menu_branch" action="action_branch" parent="menu_runbot"/>
|
||||
|
||||
<!-- Builds -->
|
||||
<record id="view_build_form" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Build" version="7.0">
|
||||
<header>
|
||||
<button name="reset" type="object" string="Reset"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="repo_id"/>
|
||||
<field name="branch_id"/>
|
||||
<field name="sequence"/>
|
||||
<field name="name"/>
|
||||
<field name="date"/>
|
||||
<field name="committer"/>
|
||||
<field name="log"/>
|
||||
<field name="port"/>
|
||||
<field name="dest"/>
|
||||
<field name="state"/>
|
||||
<field name="result"/>
|
||||
<field name="pid"/>
|
||||
<field name="job_start"/>
|
||||
<field name="job_end"/>
|
||||
<field name="job_time"/>
|
||||
<field name="job_age"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_build_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Builds">
|
||||
<field name="sequence"/>
|
||||
<field name="repo_id"/>
|
||||
<field name="dest"/>
|
||||
<field name="date"/>
|
||||
<field name="committer"/>
|
||||
<field name="state"/>
|
||||
<field name="port"/>
|
||||
<field name="job"/>
|
||||
<field name="result"/>
|
||||
<field name="pid"/>
|
||||
<field name="job_start"/>
|
||||
<field name="job_time"/>
|
||||
<field name="job_age"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_build_search" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search builds">
|
||||
<field name="branch_id"/>
|
||||
<field name="name"/>
|
||||
<field name="state"/>
|
||||
<field name="dest"/>
|
||||
<separator/>
|
||||
<filter string="Pending" domain="[('state','=', 'pending')]"/>
|
||||
<filter string="Testing" domain="[('state','=', 'testing')]"/>
|
||||
<filter string="Running" domain="[('state','=', 'running')]"/>
|
||||
<filter string="Done" domain="[('state','=','done')]"/>
|
||||
<separator />
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Repo" domain="[]" context="{'group_by':'repo_id'}"/>
|
||||
<filter string="Branch" domain="[]" context="{'group_by':'branch_id'}"/>
|
||||
<filter string="Status" domain="[]" context="{'group_by':'state'}"/>
|
||||
<filter string="Result" domain="[]" context="{'group_by':'result'}"/>
|
||||
<filter string="Start" domain="[]" context="{'group_by':'job_start'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_build" model="ir.actions.act_window">
|
||||
<field name="name">Builds</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.build</field>
|
||||
<field name="view_type">form</field>
|
||||
</record>
|
||||
<menuitem id="menu_build" action="action_build" parent="menu_runbot"/>
|
||||
|
||||
<!-- Events -->
|
||||
<record id="logging_action" model="ir.actions.act_window">
|
||||
<field name="name">Events</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">ir.logging</field>
|
||||
<field name="view_type">form</field>
|
||||
</record>
|
||||
<menuitem id="logging_menu" action="logging_action" parent="menu_runbot"/>
|
||||
|
||||
|
||||
<!-- Website menu -->
|
||||
<record id="website_menu" model="website.menu">
|
||||
<field name="name">Runbot</field>
|
||||
<field name="url">/runbot</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">1</field>
|
||||
</record>
|
||||
|
||||
<!-- Templates -->
|
||||
<template id="runbot.build_name">
|
||||
<t t-if="bu.state=='pending'"><i class="text-default fa fa-pause"/> pending</t>
|
||||
<t t-if="bu.state=='testing'"><i class="text-info fa fa-spinner"/> testing <t t-esc="bu.job[4:]"/> <small><t t-esc="bu.job_time"/>s</small></t>
|
||||
<t t-if="bu.result=='ok'"><i class="text-success fa fa-thumbs-up"/><small> age <t t-esc="s2h(bu.job_age)"/> time <t t-esc="bu.job_time"/>s</small></t>
|
||||
<t t-if="bu.result=='ko'"><i class="text-danger fa fa-thumbs-down"/><small> age <t t-esc="s2h(bu.job_age)"/> time <t t-esc="bu.job_time"/>s</small></t>
|
||||
</template>
|
||||
|
||||
<template id="runbot.build_button">
|
||||
<div t-attf-class="btn-group {{klass}} pull-right">
|
||||
<div t-attf-class="btn-group {{klass}}">
|
||||
<a t-if="bu.state=='running'" t-attf-href="http://{{bu.domain}}/?db={{bu.dest}}-all" class="btn btn-primary"><i class="fa fa-sign-in"/></a>
|
||||
<a t-attf-href="https://#{bu.repo_id.base}/commit/#{bu.name}" class="btn btn-default"><i class="fa fa-github"/></a>
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown"><i class="fa fa-cog"/><span class="caret"></span></button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li t-if="bu.state=='pending'">
|
||||
<a t-attf-href="/runbot/build/{{bu.id}}/force">Build now <i class="fa fa-level-up"> </i></a>
|
||||
</li>
|
||||
<t t-if="bu.state=='running'">
|
||||
<li><a t-attf-href="http://{{bu.domain}}/?db={{bu.dest}}-all">Connect all <i class="fa fa-sign-in"></i></a></li>
|
||||
<li><a t-attf-href="http://{{bu.domain}}/?db={{bu.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.state in ['done','running']">
|
||||
<a t-attf-href="/runbot/build/#{bu.id}/force">Rebuild <i class="fa fa-refresh"/></a>
|
||||
</li>
|
||||
<li t-if="bu.state!='testing'" class="divider"></li>
|
||||
<li><a t-attf-href="/runbot/build/{{bu.name}}">Logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<li><a t-attf-href="/runbot/static/build/#{bu.dest}/logs/job_10_test_base.txt">Full base logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<li><a t-attf-href="/runbot/static/build/#{bu.dest}/logs/job_20_test_all.txt">Full all logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a t-attf-href="{{bu.branch_id.branch_url}}">Branch or pull <i class="fa fa-github"/></a></li>
|
||||
<li><a t-attf-href="https://{{bu.repo_id.base}}/commit/{{bu.name}}">Commit <i class="fa fa-github"/></a></li>
|
||||
<li><a t-attf-href="https://{{bu.repo_id.base}}/compare/{{bu.branch_id.branch_name}}">Compare <i class="fa fa-github"/></a></li>
|
||||
<!-- TODO branch.pull from -->
|
||||
<li class="divider"></li>
|
||||
<li class="disabled"><a href="#">Runtime: <t t-esc="bu.job_time"/>s</a></li>
|
||||
<li class="disabled"><a href="#">Port: <t t-esc="bu.port"/></a></li>
|
||||
<li class="disabled"><a href="#">Age: <t t-esc="s2h(bu.job_age)"/></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="runbot.repo">
|
||||
<t t-call='website.layout'>
|
||||
<t t-set="head">
|
||||
<t t-if="refresh">
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
</t>
|
||||
<div class="container" style="width: 100%;">
|
||||
<div class="row">
|
||||
<div class='col-md-12'>
|
||||
<nav class="navbar navbar-default" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" t-attf-href="/runbot/{{'repo/' + slug(repo) if repo else ''}}"><b t-if="repo"><t t-esc="repo.base"/></b></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||
<form class="navbar-form navbar-left form-inline">
|
||||
<div class="btn-group">
|
||||
<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>
|
||||
<li class="divider"></li>
|
||||
<li t-att-class="'active' if limit=='100' else ''"><a t-att-href="qu(limit=100)">Show last 100</a></li>
|
||||
<li t-att-class="'active' if limit=='1000' else ''"><a t-att-href="qu(limit=1000)">Show last 1000</a></li>
|
||||
<li t-att-class="'active' if limit=='10000' else ''"><a t-att-href="qu(limit=10000)">Show last 10000</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Switch repository <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<t t-foreach='repos' t-as='repo'>
|
||||
<li><a t-attf-href="/runbot/repo/{{slug(repo)}}"><t t-esc="repo.base"/></a></li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="navbar-form navbar-right" role="search" t-att-action="qu(search='')" method="get">
|
||||
<div class="form-group">
|
||||
<input type="search" name="search" class="form-control" placeholder="Search" t-att-value="search"/>
|
||||
<button type="submit" class="btn btn-default">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div t-if="not repo" class="mb32">
|
||||
<h1>No Repository yet.</h1>
|
||||
</div>
|
||||
|
||||
<table t-if="repo" class="table table-condensed table-bordered table-striped">
|
||||
<tr>
|
||||
<th>Branch</th>
|
||||
<td colspan="4" class="text-right">
|
||||
<t t-esc="testing"/>/<t t-esc="repo.testing"/> testing,
|
||||
<t t-esc="running"/>/<t t-esc="repo.running"/> running,
|
||||
<t t-esc="pending"/> pending.
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr t-foreach="branches" t-as="br">
|
||||
<td>
|
||||
<i t-if="br.sticky" class="fa fa-star"/>
|
||||
<b t-esc="br.branch_name"/>
|
||||
<small><t t-esc="s2h(br.builds[0].job_age)"/></small><br/>
|
||||
<div class="btn-group btn-group-xs">
|
||||
<a t-attf-href="{{br.branch_url}}" class="btn btn-default btn-xs">Branch or pull <i class="fa fa-github"/></a>
|
||||
</div>
|
||||
</td>
|
||||
<t t-foreach="br.builds" t-as="bu">
|
||||
<t t-if="bu.state=='pending'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu.state=='testing'"><t t-set="klass">info</t></t>
|
||||
<t t-if="bu.state in ['running','done'] and bu.result == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="bu.state in ['running','done'] and bu.result == 'ok'"><t t-set="klass">success</t></t>
|
||||
<td t-attf-class="{{klass}}">
|
||||
<t t-call="runbot.build_button"><t t-set="klass">btn-group-sm</t></t>
|
||||
<t t-esc="bu.dest"/><br/>
|
||||
<t t-call="runbot.build_name"/>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="runbot.build">
|
||||
<t t-call='website.layout'>
|
||||
<div class="container" style="width: 100%;">
|
||||
<div class="row" >
|
||||
<div class='col-md-12'>
|
||||
<nav class="navbar navbar-default" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" t-attf-href="/runbot/repo/#{ slug(build.repo_id) }"><b><t t-esc="build.repo_id.base"/></b></a>
|
||||
<a class="navbar-brand" t-attf-href="/runbot/build/{{build.name}}">
|
||||
<t t-esc="build.dest"/>
|
||||
<t t-call="runbot.build_name">
|
||||
<t t-set="bu" t-value="build"/>
|
||||
<t t-set="klass" t-value="''"/>
|
||||
</t>
|
||||
</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||
<form class="navbar-form navbar-left form-inline">
|
||||
<div class="btn-group">
|
||||
<t t-call="runbot.build_button">
|
||||
<t t-set="bu" t-value="build"/>
|
||||
<t t-set="klass" t-value="''"/>
|
||||
</t>
|
||||
</div>
|
||||
</form>
|
||||
<p class="navbar-text">
|
||||
</p>
|
||||
<form class="navbar-form navbar-left form-inline">
|
||||
<div class="btn-group">
|
||||
<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 role="presentation" class="dropdown-header">Level</li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='debug')"><i t-if="level == 'debug'" class="fa fa-check"/> Debug</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='info')"><i t-if="level == 'info'" class="fa fa-check"/> Info</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='warning')"><i t-if="level == 'warning'" class="fa fa-check"/> Warning</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='error')"><i t-if="level == 'error'" class="fa fa-check"/> Error</a></li>
|
||||
<li role="presentation" class="dropdown-header">Type</li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'level',type='runbot')"><i t-if="type == 'runbot'" class="fa fa-check"/> Runbot</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'level',type='server')"><i t-if="type == 'server'" class="fa fa-check"/> Server</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'level',type='client')"><i t-if="type == 'client'" class="fa fa-check"/> Client</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default">Expand</button>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Other builds <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<t t-foreach='other_builds' t-as='other_build'>
|
||||
<li><a t-attf-href="/runbot/build/{{other_build.name}}"><t t-esc='other_build.dest'/></a></li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="navbar-form navbar-right" role="search" t-attf-action="/runbot/build/{{build.name}}" method="get">
|
||||
<div class="form-group">
|
||||
<input type="search" name="search" class="form-control" placeholder="Search" t-att-value="search or ''"/>
|
||||
<button type="submit" class="btn btn-default">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Level</th>
|
||||
<th>Type</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<t t-foreach="logs" t-as="l">
|
||||
<tr>
|
||||
<td style="white-space: nowrap;"><t t-esc="l.create_date"/></td>
|
||||
<td><b t-esc="l.level"/></td>
|
||||
<td><t t-esc="l.type"/></td>
|
||||
<td>
|
||||
<t t-esc="l.name"/>:<t t-esc="l.line"/> <t t-esc="l.func"/> <a t-attf-href="#">File <i class="fa fa-external-link"/></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="4"><pre style="margin:0;padding:0; border: none;"><t t-esc="l.message"/></pre></td></tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="runbot.webclient_config">
|
||||
[global]
|
||||
server.environment = "development"
|
||||
server.socket_host = "0.0.0.0"
|
||||
server.socket_port = %d
|
||||
server.thread_pool = 10
|
||||
tools.sessions.on = True
|
||||
log.access_level = "INFO"
|
||||
log.error_level = "INFO"
|
||||
tools.csrf.on = False
|
||||
tools.log_tracebacks.on = False
|
||||
tools.cgitb.on = True
|
||||
openerp.server.host = 'localhost'
|
||||
openerp.server.port = '%d'
|
||||
openerp.server.protocol = 'socket'
|
||||
openerp.server.timeout = 450
|
||||
[openerp-web]
|
||||
dblist.filter = 'BOTH'
|
||||
dbbutton.visible = True
|
||||
company.url = ''
|
||||
openerp.server.host = 'localhost'
|
||||
openerp.server.port = '%d'
|
||||
openerp.server.protocol = 'socket'
|
||||
openerp.server.timeout = 450
|
||||
</template>
|
||||
|
||||
<template id="runbot.nginx_config">
|
||||
pid <t t-esc="nginx_dir"/>/nginx.pid;
|
||||
error_log <t t-esc="nginx_dir"/>/error.log;
|
||||
worker_processes 1;
|
||||
events { worker_connections 1024; }
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
server_names_hash_max_size 512;
|
||||
server_names_hash_bucket_size 256;
|
||||
index index.html;
|
||||
|
||||
access_log <t t-esc="nginx_dir"/>/access.log;
|
||||
client_body_temp_path <t t-esc="nginx_dir"/>;
|
||||
fastcgi_temp_path <t t-esc="nginx_dir"/>;
|
||||
|
||||
autoindex on;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/css text/plain application/xml application/json application/javascript;
|
||||
|
||||
proxy_temp_path <t t-esc="nginx_dir"/>;
|
||||
proxy_read_timeout 600;
|
||||
proxy_connect_timeout 600;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
server {
|
||||
listen 8080 default;
|
||||
location / { proxy_pass http://127.0.0.1:<t t-esc="port"/>; }
|
||||
}
|
||||
<t t-foreach="builds" t-as="build">
|
||||
server {
|
||||
listen 8080;
|
||||
server_name ~^<t t-esc="build.dest"/>[-.].*$;
|
||||
location / { proxy_pass http://127.0.0.1:<t t-esc="build.port"/>; }
|
||||
location /longpolling { proxy_pass http://127.0.0.1:<t t-esc="build.port + 1"/>; }
|
||||
}
|
||||
</t>
|
||||
}
|
||||
</template>
|
||||
|
||||
<!-- Cron -->
|
||||
<record model="ir.cron" id="repo_cron">
|
||||
<field name='name'>Runbot Cron</field>
|
||||
<field name='interval_number'>1</field>
|
||||
<field name='interval_type'>minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model">runbot.repo</field>
|
||||
<field name="function">cron</field>
|
||||
<field name="args">()</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
4
runbot/security/ir.model.access.csv
Normal file
4
runbot/security/ir.model.access.csv
Normal file
@ -0,0 +1,4 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_runbot_repo,runbot_repo,runbot.model_runbot_repo,,1,0,0,0
|
||||
access_runbot_branch,runbot_branch,runbot.model_runbot_branch,,1,0,0,0
|
||||
access_runbot_build,runbot_build,runbot.model_runbot_build,,1,0,0,0
|
|
Loading…
Reference in New Issue
Block a user