push notifications to IRC #exim-builds
[buildfarm-server.git] / cgi-bin / eximstatus.pl
1 #!/usr/bin/perl
2
3 =comment
4
5 Copyright (c) 2003-2010, Andrew Dunstan
6
7 See accompanying License file for license details
8
9 =cut 
10
11 use strict;
12 use URI::Escape;
13
14 use vars qw($dbhost $dbname $dbuser $dbpass $dbport
15        $all_stat $fail_stat $change_stat $green_stat
16        $server_time
17            $min_script_version $min_web_script_version
18        $default_host $local_git_clone
19 );
20
21 # force this before we do anything - even load modules
22 BEGIN { $server_time = time; }
23
24 use CGI;
25 use Digest::SHA1  qw(sha1_hex);
26 use MIME::Base64;
27 use DBI;
28 use DBD::Pg;
29 use Data::Dumper;
30 use Mail::Send;
31 use Time::ParseDate;
32 use Storable qw(thaw);
33
34 use FindBin qw($RealBin);
35 require "$RealBin/../BuildFarmWeb.pl";
36
37 my $buildlogs = "$RealBin/../buildlogs";
38
39 die "no dbname" unless $dbname;
40 die "no dbuser" unless $dbuser;
41
42 my $dsn="dbi:Pg:dbname=$dbname";
43 $dsn .= ";host=$dbhost" if $dbhost;
44 $dsn .= ";port=$dbport" if $dbport;
45
46 my $query = new CGI;
47
48 my $sig = $query->path_info;
49 $sig =~ s!^/!!;
50
51 my $stage = $query->param('stage');
52 my $ts = $query->param('ts');
53 my $animal = $query->param('animal');
54 my $log = $query->param('log');
55 my $res = $query->param('res');
56 my $conf = $query->param('conf');
57 my $branch = $query->param('branch');
58 my $changed_since_success = $query->param('changed_since_success');
59 my $changed_this_run = $query->param('changed_files');
60 my $log_archive = $query->param('logtar');
61 my $frozen_sconf = $query->param('frozen_sconf') || '';
62
63 my $brhandle;
64 if (open($brhandle,"../htdocs/branches_of_interest.txt"))
65 {
66     my @branches_of_interest = <$brhandle>;
67     close($brhandle);
68     chomp(@branches_of_interest);
69     unless (grep {$_ eq $branch} @branches_of_interest)
70     {
71         print
72             "Status: 492 bad branch parameter $branch\nContent-Type: text/plain\n\n",
73             "bad branch parameter $branch\n";
74         exit;   
75     }
76 }
77
78
79 my $content = 
80         'branch=' . uri_escape($branch) . "&res=$res&stage=$stage&animal=$animal&".
81         "ts=$ts&log=$log&conf=$conf";
82
83 my $extra_content = 
84         "changed_files=$changed_this_run&".
85         "changed_since_success=$changed_since_success&";
86
87 unless ($animal && $ts && $stage && $sig)
88 {
89         print 
90             "Status: 490 bad parameters\nContent-Type: text/plain\n\n",
91             "bad parameters for request\n";
92         exit;
93         
94 }
95
96 # Want to allow all kinds of named branches
97 #unless ($branch =~ /^(HEAD|REL\d+_\d+_STABLE)$/)
98 #{
99 #        print
100 #            "Status: 492 bad branch parameter $branch\nContent-Type: text/plain\n\n",
101 #            "bad branch parameter $branch\n";
102 #        exit;
103 #
104 #}
105
106
107 my $db = DBI->connect($dsn,$dbuser,$dbpass);
108
109 die $DBI::errstr unless $db;
110
111 my $gethost=
112     "select secret from buildsystems where name = ? and status = 'approved'";
113 my $sth = $db->prepare($gethost);
114 $sth->execute($animal);
115 my ($secret)=$sth->fetchrow_array();
116 $sth->finish;
117
118 my $tsdiff = time - $ts;
119
120 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($ts);
121 $year += 1900; $mon +=1;
122 my $date=
123     sprintf("%d-%.2d-%.2d_%.2d:%.2d:%.2d",$year,$mon,$mday,$hour,$min,$sec);
124
125 if ($ENV{BF_DEBUG} || ($ts > time) || ($ts + 86400 < time ) || (! $secret) )
126 {
127     open(TX,">$buildlogs/$animal.$date");
128     print TX "sig=$sig\nlogtar-len=" , length($log_archive),
129         "\nstatus=$res\nstage=$stage\nconf:\n$conf\n",
130         "tsdiff:$tsdiff\n",
131         "changed_this_run:\n$changed_this_run\n",
132         "changed_since_success:\n$changed_since_success\n",
133         "frozen_sconf:$frozen_sconf\n",
134         "log:\n",$log;
135 #    $query->save(\*TX);
136     close(TX);
137 }
138
139 unless ($ts < time + 120)
140 {
141     my $gmt = gmtime($ts);
142     print "Status: 491 bad ts parameter - $ts ($gmt GMT) is in the future.\n",
143     "Content-Type: text/plain\n\n bad ts parameter - $ts ($gmt GMT) is in the future\n";
144         $db->disconnect;
145     exit;
146 }
147
148 unless ($ts + 86400 > time)
149 {
150     my $gmt = gmtime($ts);
151     print "Status: 491 bad ts parameter - $ts ($gmt GMT) is more than 24 hours ago.\n",
152      "Content-Type: text/plain\n\n bad ts parameter - $ts ($gmt GMT) is more than 24 hours ago.\n";
153     $db->disconnect;
154     exit;
155 }
156
157 unless ($secret)
158 {
159         print 
160             "Status: 495 Unknown System\nContent-Type: text/plain\n\n",
161             "System $animal is unknown\n";
162         $db->disconnect;
163         exit;
164         
165 }
166
167
168
169
170 my $calc_sig = sha1_hex($content,$secret);
171 my $calc_sig2 = sha1_hex($extra_content,$content,$secret);
172
173 if ($calc_sig ne $sig && $calc_sig2 ne $sig)
174 {
175
176         print "Status: 450 sig mismatch\nContent-Type: text/plain\n\n";
177         print "$sig mismatches $calc_sig($calc_sig2) on content:\n$content";
178         $db->disconnect;
179         exit;
180 }
181
182 # undo escape-proofing of base64 data and decode it
183 map {tr/$@/+=/; $_ = decode_base64($_); } 
184     ($log, $conf,$changed_this_run,$changed_since_success,$log_archive, $frozen_sconf);
185
186 if ($log =~/Last file mtime in snapshot: (.*)/)
187 {
188     my $snaptime = parsedate($1);
189     my $brch = $branch eq 'HEAD' ? 'master' : $branch;
190     my $last_branch_time = time - (30 * 86400);
191     $last_branch_time = `TZ=UTC GIT_DIR=$local_git_clone git log -1 --pretty=format:\%ct  $brch`;
192     if ($snaptime < ($last_branch_time - 86400))
193     {
194         print "Status: 493 snapshot too old: $1\nContent-Type: text/plain\n\n";
195         print "snapshot too old: $1\n";
196         $db->disconnect;
197         exit;   
198     }
199 }
200
201 ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($ts);
202 $year += 1900; $mon +=1;
203 my $dbdate=
204     sprintf("%d-%.2d-%.2d %.2d:%.2d:%.2d",$year,$mon,$mday,$hour,$min,$sec);
205
206 my $log_file_names;
207 my @log_file_names;
208 my $dirname = "$buildlogs/tmp.$$.unpacklogs";
209
210 my $githeadref;
211
212 if ($log_archive)
213 {
214     my $log_handle;
215     my $archname = "$buildlogs/tmp.$$.tgz";
216     open($log_handle,">$archname");
217     binmode $log_handle;
218     print $log_handle $log_archive;
219     close $log_handle;
220     mkdir $dirname;
221     @log_file_names = `tar -z -C $dirname -xvf $archname 2>/dev/null`;
222     map {s/\s+//g; } @log_file_names;
223     my @qnames = grep { $_ ne 'githead.log' } @log_file_names;
224     map { $_ = qq("$_"); } @qnames;
225     $log_file_names = '{' . join(',',@qnames) . '}';
226     if (-e "$dirname/githead.log" )
227     {
228         open(my $githead,"$dirname/githead.log");
229         $githeadref = <$githead>;
230         chomp $githeadref;
231         close $githead;
232     }
233     unlink $archname;
234 }
235
236 my $config_flags;
237 my $client_conf;
238 if ($frozen_sconf)
239 {
240     $client_conf = thaw $frozen_sconf;
241 }
242
243 if ($min_script_version)
244 {
245         $client_conf->{script_version} ||= '0.0';
246         my $cli_ver = $client_conf->{script_version} ;
247         $cli_ver =~ s/^REL_//;
248         my ($minmajor,$minminor) = split(/\./,$min_script_version);
249         my ($smajor,$sminor) = split(/\./,$cli_ver);
250         if ($minmajor > $smajor || ($minmajor == $smajor && $minminor > $sminor))
251         {
252                 print "Status: 460 script version too low\nContent-Type: text/plain\n\n";
253                 print 
254                         "Script version is below minimum required\n",
255                         "Reported version: $client_conf->{script_version},",
256                         "Minumum version required: $min_script_version\n";
257                 $db->disconnect;
258                 exit;
259         }
260 }
261
262 if ($min_web_script_version)
263 {
264         $client_conf->{web_script_version} ||= '0.0';
265         my $cli_ver = $client_conf->{web_script_version} ;
266         $cli_ver =~ s/^REL_//;
267         my ($minmajor,$minminor) = split(/\./,$min_web_script_version);
268         my ($smajor,$sminor) = split(/\./,$cli_ver);
269         if ($minmajor > $smajor || ($minmajor == $smajor && $minminor > $sminor))
270         {
271                 print "Status: 461 web script version too low\nContent-Type: text/plain\n\n";
272                 print 
273                         "Web Script version is below minimum required\n",
274                         "Reported version: $client_conf->{web_script_version}, ",
275                         "Minumum version required: $min_web_script_version\n"
276                         ;
277                 $db->disconnect;
278                 exit;
279         }
280 }
281
282 my @config_flags;
283 if (not exists $client_conf->{config_opts} )
284 {
285         @config_flags = ();
286 }
287 elsif (ref $client_conf->{config_opts} eq 'HASH')
288 {
289         # leave out keys with false values
290         @config_flags = grep { $client_conf->{config_opts}->{$_} } 
291             keys %{$client_conf->{config_opts}};
292 }
293 elsif (ref $client_conf->{config_opts} eq 'ARRAY' )
294 {
295         @config_flags = @{$client_conf->{config_opts}};
296 }
297
298 if (@config_flags)
299 {
300     @config_flags = grep {! m/=/ } @config_flags;
301     map {s/\s+//g; $_=qq("$_"); } @config_flags;
302     push @config_flags,'git' if $client_conf->{scm} eq 'git';
303     push(@config_flags, 'doc')
304       if (defined $client_conf->{'optional_steps'}->{'make-doc'});
305     push(@config_flags, 'test')
306       if (defined $client_conf->{'optional_steps'}->{'test'});
307     $config_flags = '{' . join(',',@config_flags) . '}' ;
308 }
309
310 my $scm = $client_conf->{scm} || 'cvs';
311 my $scmurl = $client_conf->{scm_url};
312
313 my $logst = <<EOSQL;
314     insert into build_status 
315       (sysname, snapshot, status, stage, log, conf_sum, branch,
316        changed_this_run, changed_since_success, 
317        log_archive_filenames, log_archive, build_flags, scm, scmurl, 
318        git_head_ref, frozen_conf)
319     values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
320 EOSQL
321 ;
322
323
324 # this transaction lets us set log_error_verbosity to terse
325 # just for the duration of the transaction. That turns off logging the
326 # bind params, so all the logs don't get stuffed on the postgres logs
327
328
329 my $sqlres;
330 $db->begin_work;
331 #$db->do("select set_local_error_terse()");
332
333
334 $sth=$db->prepare($logst);
335
336 $sth->bind_param(1,$animal);
337 $sth->bind_param(2,$dbdate);
338 $sth->bind_param(3,$res & 0x8fffffff); # in case we get a 64 bit int status!
339 $sth->bind_param(4,$stage);
340 $sth->bind_param(5,$log);
341 $sth->bind_param(6,$conf);
342 $sth->bind_param(7,$branch);
343 $sth->bind_param(8,$changed_this_run);
344 $sth->bind_param(9,$changed_since_success);
345 $sth->bind_param(10,$log_file_names);
346 #$sth->bind_param(11,$log_archive,{ pg_type => DBD::Pg::PG_BYTEA });
347 $sth->bind_param(11,undef,{ pg_type => DBD::Pg::PG_BYTEA });
348 $sth->bind_param(12,$config_flags);
349 $sth->bind_param(13,$scm);
350 $sth->bind_param(14,$scmurl);
351 $sth->bind_param(15,$githeadref);
352 $sth->bind_param(16,$frozen_sconf,{ pg_type => DBD::Pg::PG_BYTEA });
353
354 $sqlres = $sth->execute;
355
356 if ($sqlres)
357 {
358
359         $sth->finish;
360
361         my $logst2 = q{
362
363           insert into build_status_log 
364                 (sysname, snapshot, branch, log_stage, log_text, stage_duration)
365                 values (?, ?, ?, ?, ?, ?)
366
367     };
368
369         $sth = $db->prepare($logst2);
370
371         $/=undef;
372
373         my $stage_start = $ts;
374
375         foreach my $log_file( @log_file_names )
376         {
377                 next if $log_file =~ /^githead/;
378                 my $handle;
379                 open($handle,"$dirname/$log_file");
380                 my $mtime = (stat $handle)[9];
381                 my $stage_interval = $mtime - $stage_start;
382                 $stage_start = $mtime;
383                 my $ltext = <$handle>;
384                 close($handle);
385                 $ltext =~ s/\x00/\\0/g;
386                 $sqlres = $sth->execute($animal,$dbdate,$branch,$log_file,$ltext, 
387                           "$stage_interval seconds");
388                 last unless $sqlres;
389         }
390
391         $sth->finish unless $sqlres;
392
393 }
394
395 if (! $sqlres)
396 {
397
398         print "Status: 462 database failure\nContent-Type: text/plain\n\n";
399         print "Your report generated a database failure:\n", 
400                $db->errstr, 
401                          "\n";
402         $db->rollback;
403         $db->disconnect;
404         exit;
405 }
406
407
408 $db->commit;
409
410 my $prevst = <<EOSQL;
411
412   select coalesce((select distinct on (snapshot) stage
413                   from build_status
414                   where sysname = ? and branch = ? and snapshot < ?
415                   order by snapshot desc
416                   limit 1), 'NEW') as prev_status
417   
418 EOSQL
419
420 $sth=$db->prepare($prevst);
421 $sth->execute($animal,$branch,$dbdate);
422 my $row=$sth->fetchrow_arrayref;
423 my $prev_stat=$row->[0];
424 $sth->finish;
425
426 my $det_st = <<EOS;
427
428           select operating_system|| ' / ' || os_version as os , 
429                  compiler || ' / ' || compiler_version as compiler, 
430                  architecture as arch
431           from buildsystems 
432           where status = 'approved'
433                 and name = ?
434
435 EOS
436 ;
437 $sth=$db->prepare($det_st);
438 $sth->execute($animal);
439 $row=$sth->fetchrow_arrayref;
440 my ($os, $compiler,$arch) = @$row;
441 $sth->finish;
442
443 $db->begin_work;
444 # prevent occasional duplication by forcing serialization of this operation
445 $db->do("lock table dashboard_mat in share row exclusive mode");
446 $db->do("delete from dashboard_mat");
447 $db->do("insert into dashboard_mat select * from dashboard_mat_data");
448 $db->commit;
449
450
451 #if ($stage ne 'OK') # On Exim build farm nrecent_failures is a view, not table... comment out
452 #{
453 #       $db->begin_work;
454 #       # prevent occasional duplication by forcing serialization of this operation
455 #       $db->do("lock table nrecent_failures in share row exclusive mode");
456 #       $db->do("delete from nrecent_failures");
457 #       $db->do("insert into nrecent_failures select bs.sysname, bs.snapshot, bs.branch from build_status bs where bs.stage <> 'OK' and bs.snapshot > now() - interval '90 days'");
458 #       $db->commit;
459 #}
460
461 $db->disconnect;
462
463 print "Content-Type: text/plain\n\n";
464 print "request was on:\n";
465 print "res=$res&stage=$stage&animal=$animal&ts=$ts";
466
467 my $client_events = $client_conf->{mail_events};
468
469 if ($ENV{BF_DEBUG})
470 {
471         my $client_time = $client_conf->{current_ts};
472     open(TX,">>$buildlogs/$animal.$date");
473     print TX "\n",Dumper(\$client_conf),"\n";
474         print TX "server time: $server_time, client time: $client_time\n" if $client_time;
475     close(TX);
476 }
477
478 my $bcc_stat = [];
479 my $bcc_chg=[];
480 if (ref $client_events)
481 {
482     my $cbcc = $client_events->{all};
483     if (ref $cbcc)
484     {
485         push @$bcc_stat, @$cbcc;
486     }
487     elsif (defined $cbcc)
488     {
489         push @$bcc_stat, $cbcc;
490     }
491     if ($stage ne 'OK')
492     {
493         $cbcc = $client_events->{all};
494         if (ref $cbcc)
495         {
496             push @$bcc_stat, @$cbcc;
497         }
498         elsif (defined $cbcc)
499         {
500             push @$bcc_stat, $cbcc;
501         }
502     }
503     $cbcc = $client_events->{change};
504     if (ref $cbcc)
505     {
506         push @$bcc_chg, @$cbcc;
507     }
508     elsif (defined $cbcc)
509     {
510         push @$bcc_chg, $cbcc;
511     }
512     if ($stage eq 'OK' || $prev_stat eq 'OK')
513     {
514         $cbcc = $client_events->{green};
515         if (ref $cbcc)
516         {
517             push @$bcc_chg, @$cbcc;
518         }
519         elsif (defined $cbcc)
520         {
521             push @$bcc_chg, $cbcc;
522         }
523     }
524 }
525
526
527 my $url = $query->url(-base => 1);
528
529
530 my $stat_type = $stage eq 'OK' ? 'Status' : 'Failed at Stage';
531
532 my $mailto = [@$all_stat];
533 push(@$mailto,@$fail_stat) if $stage ne 'OK';
534
535 my $me = `id -un`; chomp($me);
536
537 my $host = `hostname`; chomp ($host);
538 $host = $default_host unless ($host =~ m/[.]/ || !defined($default_host));
539
540 my $from_addr = "Exim Build Farm <$me\@$host>";
541 $from_addr =~ tr /\r\n//d;
542
543 my $msg = new Mail::Send;
544
545
546 $msg->to(@$mailto);
547 $msg->bcc(@$bcc_stat) if (@$bcc_stat);
548 $msg->subject("Exim BuildFarm member $animal Branch $branch $stat_type $stage");
549 $msg->set('From',$from_addr);
550 my $fh = $msg->open;
551 print $fh <<EOMAIL; 
552
553
554 The Exim BuildFarm member $animal had the following event on branch $branch:
555
556 $stat_type: $stage
557
558 The snapshot timestamp for the build that triggered this notification is: $dbdate
559
560 The specs of this machine are:
561 OS:  $os
562 Arch: $arch
563 Comp: $compiler
564
565 For more information, see $url/cgi-bin/show_history.pl?nm=$animal&br=$branch
566
567 EOMAIL
568
569 $fh->close;
570
571 use HTTP::Tiny;
572 use JSON::PP;
573 HTTP::Tiny->new(timeout => 5)->post(
574     'http://127.0.0.1:2567/api/message', {
575         headers => {'content-type' => 'application/json'},
576         content => encode_json({
577             gateway => 'exim-builds',
578             username => '',
579             text => "$animal [$branch]: @{[lc $stat_type]}: @{[lc $stage]}; commit: https://git.exim.org/@{[substr $githeadref, 0, 10]}",
580         }),
581     }
582 );
583
584 exit if ($stage eq $prev_stat);
585
586 $mailto = [@$change_stat];
587 push(@$mailto,@$green_stat) if ($stage eq 'OK' || $prev_stat eq 'OK');
588
589 $msg = new Mail::Send;
590
591
592 $msg->to(@$mailto);
593 $msg->bcc(@$bcc_chg) if (@$bcc_chg);
594
595 $stat_type = $prev_stat ne 'OK' ? "changed from $prev_stat failure to $stage" :
596     "changed from OK to $stage";
597 $stat_type = "New member: $stage" if $prev_stat eq 'NEW';
598 $stat_type .= " failure" if $stage ne 'OK';
599
600 $msg->subject("Exim BuildFarm member $animal Branch $branch Status $stat_type");
601 $msg->set('From',$from_addr);
602 $fh = $msg->open;
603 print $fh <<EOMAIL;
604
605 The Exim BuildFarm member $animal had the following event on branch $branch:
606
607 Status $stat_type
608
609 The snapshot timestamp for the build that triggered this notification is: $dbdate
610
611 The specs of this machine are:
612 OS:  $os
613 Arch: $arch
614 Comp: $compiler
615
616 For more information, see $url/cgi-bin/show_history.pl?nm=$animal&br=$branch
617
618 EOMAIL
619
620 $fh->close;
621
622 HTTP::Tiny->new(timeout => 5)->post(
623     'http://127.0.0.1:2567/api/message', {
624         headers => {'content-type' => 'application/json'},
625         content => encode_json({
626             gateway => 'exim-builds',
627             username => '',
628             text => "$animal [$branch]: status @{[lc $stat_type]}; $url/cgi-bin/show_history.pl?nm=$animal&br=$branch",
629         }),
630     }
631 );