3
import cgitb,optparse,os,re,subprocess,sys,time
5
import launchpadlib.launchpad,mako.template
7
#----------------------------------------------------------
8
# OpenERP rdtools utils
9
#----------------------------------------------------------
12
out=[time.strftime("%Y-%m-%d %H:%M:%S")]
14
if not isinstance(i,basestring):
17
out+=["%s=%r"%(k,v) for k,v in kw.items()]
21
fd=os.open(name,os.O_CREAT|os.O_RDWR,0600)
22
fcntl.lockf(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)
25
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
29
if isinstance(l,list):
30
rc=os.spawnvp(os.P_WAIT, l[0], l)
31
elif isinstance(l,str):
33
rc=os.spawnvp(os.P_WAIT, tmp[0], tmp)
43
return n.replace("~","").replace(":","_").replace("/","_")
45
#----------------------------------------------------------
47
#----------------------------------------------------------
49
class RunBotBranch(object):
50
def __init__(self,runbot,branch):
53
self.running_port=None
54
self.running_server_pid=None
55
self.running_web_pid=None
57
self.date_last_modified=0
62
self.unique_name=branch.unique_name
63
self.project_name=re.search("/openobject-(addons|server|client-web)/",self.unique_name).group(1)
64
self.uname=underscorize(self.unique_name)
66
self.repo_path=os.path.join(self.runbot.wd,'repo',self.uname)
67
self.subdomain="%s-%s"%(self.project_name,self.name.replace('_','-').replace('.','-'))
68
self.running_path=os.path.join(self.runbot.wd,'running',self.subdomain)
70
self.server_path=os.path.join(self.running_path,"server")
71
self.server_bin_path=os.path.join(self.server_path,"openerp-server.py")
72
if not os.path.exists(self.server_bin_path): # for 6.0 branches
73
self.server_bin_path=os.path.join(self.server_path,"bin","openerp-server.py")
75
self.web_path=os.path.join(self.running_path,"web")
76
self.web_bin_path=os.path.join(self.web_path,"openerp-web.py")
78
self.log_path=os.path.join(self.runbot.wd,'static', self.subdomain, 'logs')
79
self.log_server_path=os.path.join(self.log_path,'server.txt')
80
self.log_web_path=os.path.join(self.log_path,'client-web.txt')
81
self.coverage_base_path=os.path.join(self.log_path,'coverage-base')
82
self.coverage_full_path=os.path.join(self.log_path,'coverage-full')
84
def update(self,branch):
85
log("branch-update",branch=self.unique_name)
86
if self.revision_count != branch.revision_count:
87
if os.path.exists(self.repo_path):
88
run(["bzr","pull","-d",self.repo_path,"--overwrite"])
90
run(["bzr","branch","lp:%s"%self.unique_name,self.repo_path])
91
self.date_last_modified=branch.date_last_modified
92
self.revision_count=branch.revision_count
93
self.merge_count=len(list(branch.getMergeProposals()))
96
def start_rsync(self,port):
97
log("branch-start-rsync",branch=self.unique_name,port=port)
98
server_src = os.path.join(self.runbot.wd,'repo','openerp_openobject-server_trunk')
99
addons_src = os.path.join(self.runbot.wd,'repo','openerp_openobject-addons_trunk')
100
web_src = os.path.join(self.runbot.wd,'repo','openerp_openobject-client-web_trunk')
101
if self.project_name == "server":
102
server_src = self.repo_path
103
if self.project_name == "addons":
104
addons_src = self.repo_path
105
if self.project_name == "client-web":
106
web_src = self.repo_path
107
for i in [self.running_path,self.log_path]:
108
if not os.path.exists(i):
110
run(["rsync","-a","--exclude",".bzr","--delete","%s/"%server_src,self.server_path])
111
if not os.path.exists(os.path.join(self.server_path,"openerp/addons")): # for 6.0 branches
112
# TODO copy the 6.0 addons, not the trunk ones.
113
run(["rsync","-a","--exclude",".bzr","%s/"%addons_src,os.path.join(self.server_path,"bin/addons")])
115
run(["rsync","-a","--exclude",".bzr","%s/"%addons_src,os.path.join(self.server_path,"openerp/addons")])
116
run(["rsync","-a","--exclude",".bzr","--delete","%s/"%web_src,self.web_path])
118
def start_createdb(self,port):
119
run(["dropdb",self.subdomain])
120
run(["createdb",self.subdomain])
122
def start_run_server(self,port):
123
log("branch-start-run-server",branch=self.unique_name,port=port)
124
out=open(self.log_server_path,"w")
125
cmd=[self.server_bin_path,"-d",self.subdomain,"-i","base","--no-xmlrpc","--no-xmlrpcs","--netrpc-port=%d"%(self.runbot.server_port+port)]
126
log("run",*cmd,log=self.log_server_path)
127
p=subprocess.Popen(cmd, stdout=out, stderr=out, close_fds=True)
128
self.running_server_pid=p.pid
130
def start_run_web(self,port):
133
server.environment = "development"
134
server.socket_host = "0.0.0.0"
135
server.socket_port = %d
136
server.thread_pool = 10
137
tools.sessions.on = True
138
log.access_level = "INFO"
139
log.error_level = "INFO"
140
tools.csrf.on = False
141
tools.log_tracebacks.on = False
142
tools.cgitb.on = True
143
openerp.server.host = 'localhost'
144
openerp.server.port = '%d'
145
openerp.server.protocol = 'socket'
146
openerp.server.timeout = 450
148
dblist.filter = 'BOTH'
149
dbbutton.visible = True
151
"""%(self.runbot.web_port+port,self.runbot.server_port+port)
152
config=config.replace("\n ","\n")
153
open(os.path.join(self.web_path,"doc","openerp-web.cfg"),"w").write(config)
155
out=open(self.log_web_path,"w")
156
cmd=[self.web_bin_path]
157
log("run",*cmd,log=self.log_web_path)
158
p=subprocess.Popen(cmd, stdout=out, stderr=out, close_fds=True)
159
self.running_web_pid=p.pid
161
def start_test(self,port):
162
# here we should fork
163
log("branch-start-test-coverage",branch=self.unique_name,port=port)
164
dbname="%s_full"%self.subdomain
166
# TODO if we fork and a new commit happen before the end of test this will fail:
167
# maybe use a dbname like <subdomain>_full_<revno> ?
168
# but if we do we should try to delete <subdomain>_full_* from the psql -l output
169
run(["dropdb",dbname])
170
run(["createdb",dbname])
172
# TODO use COVERAGE_FILE environ variable to save the data in the log directory
173
# Isnt it python-coverage ?
174
run(["coverage","run","--branch",self.server_bin_path,"-d",dbname,"-i","base","--stop-after-init","--no-xmlrpc","--no-xmlrpcs","--no-netrpc"])
175
run(["coverage","html","-d",self.coverage_base_path,"--ignore-errors","--include=*.py"])
177
# TODO get the list of all module, os.listdir(addons) should work but with a small blacklist ?
179
mods = ",".join(mods)
180
#run(["dropdb",dbname])
181
#run(["createdb",dbname])
182
#run(["coverage","run","--branch",self.server_bin_path,"-d",dbname,"-i",mods,"--stop-after-init","--no-xmlrpc","--no-xmlrpcs","--no-netrpc"])
183
#run(["coverage","html","-d",self.coverage_full_path,"--ignore-errors","--include=*.py"])
187
def start(self,port):
188
log("branch-start",branch=self.unique_name,port=port)
189
self.start_rsync(port)
190
self.start_createdb(port)
191
self.start_run_server(port)
192
self.start_run_web(port)
193
self.start_test(port)
195
self.runbot.running.insert(0,self)
196
self.runbot.running.sort(key=lambda x:x.date_last_modified,reverse=1)
198
self.running_t0=time.time()
199
self.running_port=port
202
log("branch-stop",branch=self.unique_name,port=self.running_port)
203
kill(self.running_server_pid)
204
kill(self.running_web_pid)
205
self.runbot.running.remove(self)
207
self.running_port=None
209
class RunBot(object):
210
def __init__(self,wd,team,poll,server_port,web_port,number,nginx_port,domain):
214
self.server_port=int(server_port)
215
self.web_port=int(web_port)
216
self.number=int(number)
217
self.nginx_port=int(nginx_port)
222
def nginx_reload(self):
223
nginx_pid_path = os.path.join(self.wd,'nginx','nginx.pid')
224
if os.path.isfile(nginx_pid_path):
225
pid=int(open(nginx_pid_path).read())
228
run(["nginx","-p",self.wd,"-c",os.path.join(self.wd,"nginx/nginx.conf")])
230
def nginx_config(self):
233
error_log nginx/error.log;
235
events { worker_connections 1024; }
237
include /etc/nginx/mime.types;
238
server_names_hash_bucket_size 128;
240
client_body_temp_path nginx; proxy_temp_path nginx; fastcgi_temp_path nginx; access_log nginx/access.log; index index.html;
241
server { listen ${r.nginx_port} default; server_name _; root ./static; }
242
% for i in r.running:
244
listen ${r.nginx_port};
245
server_name ${i.subdomain}.${r.domain};
246
location / { proxy_pass http://127.0.0.1:${r.web_port+i.running_port}; proxy_set_header X-Forwarded-Host $host; }
251
return mako.template.Template(template).render(r=self)
253
def nginx_index_time(self,t):
254
for m,u in [(86400,'d'),(3600,'h'),(60,'m')]:
256
return str(int(t/m))+u
257
return str(int(t))+"s"
259
def nginx_index(self):
260
template = """<!DOCTYPE html>
263
<title>OpenERP runbot (openerp-dev)</title>
264
<link rel="shortcut icon" href="/favicon.ico" />
265
<link rel="stylesheet" href="style.css" type="text/css">
267
<body id="indexfile">
269
<div class="content"><h1>OpenERP runbot (openerp-dev)</h1></div>
272
<table class="index">
274
<tr class="tablehead">
275
<th class="name left">Branch</th>
280
<th class="right">LP merge</th>
285
<td class="name left">x branches</td>
290
<td class="right"></td>
294
% for i in r.running:
296
<td class="name left">
297
<a href="http://${i.subdomain}.${r.domain}/">${i.subdomain}</a> <small>(netrpc: ${r.server_port+i.running_port})</small>
299
bzr branch <a href="https://code.launchpad.net/${i.unique_name}">lp:${i.unique_name}</a>
302
${i.date_last_modified.strftime("%Y-%m-%d %H:%M:%S")}<br>
304
% if t-i.running_t0 < 120:
305
<span style="color:red;">${r.nginx_index_time(t-i.running_t0)}</span>
307
<span style="color:green;">${r.nginx_index_time(t-i.running_t0)}</span>
311
<a href="http://${r.domain}/${i.subdomain}/logs/server.txt">server</a>
312
<a href="http://${r.domain}/${i.subdomain}/logs/client-web.txt">web</a>
313
<a href="http://${r.domain}/${i.subdomain}/logs/coverage-base/">coverage</a>
315
<td> <a href="http://bazaar.launchpad.net/${i.unique_name}/revision/${i.revision_count}">${i.revision_count}</a> </td>
317
<% bug=re.search('bug-([0-9]+)-',i.subdomain) %>
319
<a href="https://bugs.launchpad.net/bugs/${bug.group(1)}">Bug ${bug.group(1)}</a>
326
<a href="https://code.launchpad.net/${i.unique_name}/+activereviews">${i.merge_count} pending</a>
337
<div class="content">
338
<p>Last modification: x.</p>
343
return mako.template.Template(template).render(r=self,t=time.time(),re=re)
345
def nginx_udpate(self):
346
log("runbot-nginx-update")
347
f=open(os.path.join(self.wd,'static','index.html'),"w")
348
f.write(self.nginx_index())
350
f=open(os.path.join(self.wd,'nginx','nginx.conf'),"w")
351
f.write(self.nginx_config())
355
def allocate_port_and_run(self,rbb):
356
if len(self.running) >= self.number:
357
victim = self.running[-1]
359
running_ports=[i.running_port for i in self.running]
360
for p in range(self.number):
361
if p not in running_ports:
367
log("runbot-process")
368
launchpad=launchpadlib.launchpad.Launchpad.login_anonymously('openerp-runbot', 'edge', 'lpcache')
370
launchpad.branches.getByUniqueName(unique_name="~openerp/openobject-server/trunk"),
371
launchpad.branches.getByUniqueName(unique_name="~openerp/openobject-addons/trunk"),
372
launchpad.branches.getByUniqueName(unique_name="~openerp/openobject-client-web/trunk"),
373
launchpad.branches.getByUniqueName(unique_name="~openerp/openobject-server/6.0"),
374
launchpad.branches.getByUniqueName(unique_name="~openerp/openobject-addons/6.0"),
375
launchpad.branches.getByUniqueName(unique_name="~openerp/openobject-client-web/6.0"),
377
for b in trunk_branches:
378
RunBotBranch(self,b).update(b)
379
team=launchpad.people[self.team]
380
team_branches=team.getBranches()
381
branches_sorted=[(b.date_last_modified,b) for b in team_branches if re.search("/openobject-(addons|server|client-web)/",b.unique_name)]
382
branches_sorted.sort(reverse=1)
383
branches=trunk_branches+[b[1] for b in branches_sorted]
384
for b in branches[:self.number]:
385
rbb=self.branches.setdefault(b.unique_name,RunBotBranch(self,b))
386
updated=rbb.update(b)
390
self.allocate_port_and_run(rbb)
397
log("runbot-sleep",self.sleeptime)
398
time.sleep(self.sleeptime)
400
log("runbot-exception")
401
print cgitb.text(sys.exc_info())
402
except KeyboardInterrupt,e:
403
log("sigint recevied exiting...")
404
for i in self.running:
409
dest = os.path.join(wd,'repo')
410
if not os.path.exists(dest):
411
run(["bzr","init-repo",dest])
412
for i in ['nginx','static','running','lpcache']:
413
dest = os.path.join(wd,i)
414
if not os.path.exists(dest):
416
run('sudo su - postgres -c "createuser -s $USER"')
420
os.chdir(os.path.normpath(os.path.dirname(__file__)))
421
parser = optparse.OptionParser(usage="%prog [--runbot-init|--runbot-run] [options] ",version="1.0")
422
parser.add_option("--runbot-init", action="store_true", help="initialize the runbot environment")
423
parser.add_option("--runbot-run", action="store_true", help="run the runbot")
424
parser.add_option("--runbot-dir", metavar="DIR", default=".", help="runbot working dir (%default)")
425
parser.add_option("--runbot-team", metavar="TEAM", default="openerp-dev", help="launchpad team to monitor (%default)")
426
parser.add_option("--runbot-server-port", metavar="PORT", default=9100, help="starting port for servers (%default)")
427
parser.add_option("--runbot-web-port", metavar="PORT", default=9200, help="starting port for client-web (%default)")
428
parser.add_option("--runbot-nginx-port", metavar="PORT", default=9000, help="starting port for nginx server (%default)")
429
parser.add_option("--runbot-nginx-domain", metavar="DOMAIN", default="runbot.openerp.com", help="virtual host domain (%default)")
430
parser.add_option("--runbot-number", metavar="NUMBER", default=5, help="max concurrent instance to run (%default)")
431
parser.add_option("--runbot-poll", metavar="SECONDS", default=300, help="launchpad polling interval (%default)")
432
o, a = parser.parse_args(sys.argv)
434
runbot_init(o.runbot_dir)
436
r = RunBot(o.runbot_dir,o.runbot_team,o.runbot_poll,o.runbot_server_port,o.runbot_web_port,o.runbot_number,o.runbot_nginx_port,o.runbot_nginx_domain)
441
if __name__ == '__main__':
442
print "kill ` ps faux | grep ./running | awk '{print $2}' `"