~openerp-dev/openerp-tools/trunk-bootstrap-fme

« back to all changes in this revision

Viewing changes to openerp-runbot/openerp-runbot.py

  • Committer: Antony Lesuisse
  • Date: 2011-03-09 22:18:53 UTC
  • Revision ID: al@openerp.com-20110309221853-mzy12ng78chdklqb
merge rdtools

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
 
 
3
import cgitb,optparse,os,re,subprocess,sys,time
 
4
 
 
5
import launchpadlib.launchpad,mako.template
 
6
 
 
7
#----------------------------------------------------------
 
8
# OpenERP rdtools utils
 
9
#----------------------------------------------------------
 
10
 
 
11
def log(*l,**kw):
 
12
    out=[time.strftime("%Y-%m-%d %H:%M:%S")]
 
13
    for i in l:
 
14
        if not isinstance(i,basestring):
 
15
            i=repr(i)
 
16
        out.append(i)
 
17
    out+=["%s=%r"%(k,v) for k,v in kw.items()]
 
18
    print " ".join(out)
 
19
 
 
20
def lock(name):
 
21
    fd=os.open(name,os.O_CREAT|os.O_RDWR,0600)
 
22
    fcntl.lockf(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)
 
23
 
 
24
def nowait():
 
25
    signal.signal(signal.SIGCHLD, signal.SIG_IGN)
 
26
 
 
27
def run(l):
 
28
    log("run",*l)
 
29
    if isinstance(l,list):
 
30
        rc=os.spawnvp(os.P_WAIT, l[0], l)
 
31
    elif isinstance(l,str):
 
32
        tmp=['sh','-c',l]
 
33
        rc=os.spawnvp(os.P_WAIT, tmp[0], tmp)
 
34
    return rc
 
35
 
 
36
def kill(pid,sig=9):
 
37
    try:
 
38
        os.kill(pid,sig)
 
39
    except OSError:
 
40
        pass
 
41
 
 
42
def underscorize(n):
 
43
    return n.replace("~","").replace(":","_").replace("/","_")
 
44
 
 
45
#----------------------------------------------------------
 
46
# OpenERP RunBot
 
47
#----------------------------------------------------------
 
48
 
 
49
class RunBotBranch(object):
 
50
    def __init__(self,runbot,branch):
 
51
        self.runbot=runbot
 
52
        self.running=False
 
53
        self.running_port=None
 
54
        self.running_server_pid=None
 
55
        self.running_web_pid=None
 
56
        self.running_t0=None
 
57
        self.date_last_modified=0
 
58
        self.revision_count=0
 
59
        self.merge_count=0
 
60
 
 
61
        self.name=branch.name
 
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)
 
65
 
 
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)
 
69
 
 
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")
 
74
 
 
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")
 
77
 
 
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')
 
83
 
 
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"])
 
89
            else:
 
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()))
 
94
            return True
 
95
 
 
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):
 
109
                os.makedirs(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")])
 
114
        else:
 
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])
 
117
 
 
118
    def start_createdb(self,port):
 
119
        run(["dropdb",self.subdomain])
 
120
        run(["createdb",self.subdomain])
 
121
 
 
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
 
129
 
 
130
    def start_run_web(self,port):
 
131
        config="""
 
132
        [global]
 
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
 
147
        [openerp-web]
 
148
        dblist.filter = 'BOTH'
 
149
        dbbutton.visible = True
 
150
        company.url = ''
 
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)
 
154
 
 
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
 
160
 
 
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
 
165
 
 
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])
 
171
 
 
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"])
 
176
 
 
177
        # TODO get the list of all module, os.listdir(addons) should work but with a small blacklist ?
 
178
        mods = []
 
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"])
 
184
 
 
185
        # TODO analyze logs
 
186
 
 
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)
 
194
 
 
195
        self.runbot.running.insert(0,self)
 
196
        self.runbot.running.sort(key=lambda x:x.date_last_modified,reverse=1)
 
197
        self.running=True
 
198
        self.running_t0=time.time()
 
199
        self.running_port=port
 
200
 
 
201
    def stop(self):
 
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)
 
206
        self.running=False
 
207
        self.running_port=None
 
208
 
 
209
class RunBot(object):
 
210
    def __init__(self,wd,team,poll,server_port,web_port,number,nginx_port,domain):
 
211
        self.wd=wd
 
212
        self.sleeptime=poll
 
213
        self.team=team
 
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)
 
218
        self.domain=domain
 
219
        self.branches={}
 
220
        self.running=[]
 
221
 
 
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())
 
226
            os.kill(pid,1)
 
227
        else:
 
228
            run(["nginx","-p",self.wd,"-c",os.path.join(self.wd,"nginx/nginx.conf")])
 
229
 
 
230
    def nginx_config(self):
 
231
        template="""
 
232
        pid nginx/nginx.pid;
 
233
        error_log nginx/error.log;
 
234
        worker_processes  1;
 
235
        events { worker_connections  1024; }
 
236
        http {
 
237
          include /etc/nginx/mime.types;
 
238
          server_names_hash_bucket_size 128;
 
239
          autoindex on;
 
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:
 
243
             server {
 
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; }
 
247
             }
 
248
          % endfor
 
249
        }
 
250
        """
 
251
        return mako.template.Template(template).render(r=self)
 
252
 
 
253
    def nginx_index_time(self,t):
 
254
        for m,u in [(86400,'d'),(3600,'h'),(60,'m')]:
 
255
            if t>=m:
 
256
                return str(int(t/m))+u
 
257
        return str(int(t))+"s"
 
258
 
 
259
    def nginx_index(self):
 
260
        template = """<!DOCTYPE html>
 
261
        <html>
 
262
        <head>
 
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">
 
266
        </head>
 
267
        <body id="indexfile">
 
268
        <div id="header">
 
269
            <div class="content"><h1>OpenERP runbot (openerp-dev)</h1></div>
 
270
        </div>
 
271
        <div id="index">
 
272
        <table class="index">
 
273
        <thead>
 
274
        <tr class="tablehead">
 
275
            <th class="name left">Branch</th>
 
276
            <th>Date</th>
 
277
            <th>Logs</th>
 
278
            <th>LP revno</th>
 
279
            <th>LP bug</th>
 
280
            <th class="right">LP merge</th>
 
281
        </tr>
 
282
        </thead>
 
283
        <tfoot>
 
284
        <tr class="total">
 
285
            <td class="name left">x branches</td>
 
286
            <td></td>
 
287
            <td></td>
 
288
            <td></td>
 
289
            <td></td>
 
290
            <td class="right"></td>
 
291
        </tr>
 
292
        </tfoot>
 
293
        <tbody>
 
294
        % for i in r.running:
 
295
        <tr class="file">
 
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>
 
298
                <br>
 
299
                bzr branch <a href="https://code.launchpad.net/${i.unique_name}">lp:${i.unique_name}</a>
 
300
            </td>
 
301
            <td class="date">
 
302
                ${i.date_last_modified.strftime("%Y-%m-%d %H:%M:%S")}<br>
 
303
                running time 
 
304
                % if t-i.running_t0 < 120:
 
305
                    <span style="color:red;">${r.nginx_index_time(t-i.running_t0)}</span>
 
306
                % else:
 
307
                    <span style="color:green;">${r.nginx_index_time(t-i.running_t0)}</span>
 
308
                % endif
 
309
            </td>
 
310
            <td>
 
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>
 
314
            </td>
 
315
            <td> <a href="http://bazaar.launchpad.net/${i.unique_name}/revision/${i.revision_count}">${i.revision_count}</a> </td>
 
316
            <td>
 
317
            <% bug=re.search('bug-([0-9]+)-',i.subdomain) %>
 
318
            % if bug:
 
319
                <a href="https://bugs.launchpad.net/bugs/${bug.group(1)}">Bug ${bug.group(1)}</a>
 
320
            % else:
 
321
                /
 
322
            % endif
 
323
            </td>
 
324
            <td class="right">
 
325
            % if i.merge_count:
 
326
                <a href="https://code.launchpad.net/${i.unique_name}/+activereviews">${i.merge_count} pending</a>
 
327
            % else:
 
328
                /
 
329
            % endif
 
330
            </td>
 
331
        </tr>
 
332
        % endfor
 
333
        </tbody>
 
334
        </table>
 
335
        </div>
 
336
        <div id="footer">
 
337
            <div class="content">
 
338
                <p>Last modification: x.</p>
 
339
            </div>
 
340
        </div>
 
341
        </body>
 
342
        """
 
343
        return mako.template.Template(template).render(r=self,t=time.time(),re=re)
 
344
 
 
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())
 
349
        f.close()
 
350
        f=open(os.path.join(self.wd,'nginx','nginx.conf'),"w")
 
351
        f.write(self.nginx_config())
 
352
        f.close()
 
353
        self.nginx_reload()
 
354
 
 
355
    def allocate_port_and_run(self,rbb):
 
356
        if len(self.running) >= self.number:
 
357
            victim = self.running[-1]
 
358
            victim.stop()
 
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:
 
362
                break
 
363
        rbb.start(p)
 
364
        self.nginx_udpate()
 
365
 
 
366
    def process(self):
 
367
        log("runbot-process")
 
368
        launchpad=launchpadlib.launchpad.Launchpad.login_anonymously('openerp-runbot', 'edge', 'lpcache')
 
369
        trunk_branches = [
 
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"),
 
376
        ]
 
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)
 
387
            if updated:
 
388
                if rbb.running:
 
389
                    rbb.stop()
 
390
                self.allocate_port_and_run(rbb)
 
391
        self.nginx_udpate()
 
392
 
 
393
    def loop(self):
 
394
        while 1:
 
395
            try:
 
396
                self.process()
 
397
                log("runbot-sleep",self.sleeptime)
 
398
                time.sleep(self.sleeptime)
 
399
            except Exception,e:
 
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:
 
405
                    i.stop()
 
406
                raise e
 
407
 
 
408
def runbot_init(wd):
 
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):
 
415
            os.mkdir(dest)
 
416
    run('sudo su - postgres -c "createuser -s $USER"')
 
417
 
 
418
def main():
 
419
 
 
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)
 
433
    if o.runbot_init:
 
434
        runbot_init(o.runbot_dir)
 
435
    elif o.runbot_run:
 
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)
 
437
        r.loop()
 
438
    else:
 
439
        parser.print_help()
 
440
 
 
441
if __name__ == '__main__':
 
442
    print "kill ` ps faux | grep ./running  | awk '{print $2}' `"
 
443
    main()
 
444