File Coverage

blib/lib/Tapper/PRC/Testcontrol.pm
Criterion Covered Total %
statement 38 40 95.0
branch n/a
condition n/a
subroutine 14 14 100.0
pod n/a
total 52 54 96.3


line stmt bran cond sub pod time code
1             package Tapper::PRC::Testcontrol;
2             BEGIN {
3 7     7   2544440 $Tapper::PRC::Testcontrol::AUTHORITY = 'cpan:TAPPER';
4             }
5             {
6             $Tapper::PRC::Testcontrol::VERSION = '4.1.2';
7             }
8              
9 7     7   218 use 5.010;
  7         27  
  7         282  
10 7     7   41 use warnings;
  7         27  
  7         953  
11 7     7   42 use strict;
  7         24  
  7         282  
12              
13 7     7   23547 use IPC::Open3;
  7         48861  
  7         507  
14 7     7   9733 use File::Copy;
  7         39481  
  7         804  
15 7     7   19137 use File::Temp qw/tempdir/;
  7         12406  
  7         462  
16 7     7   6843 use Moose;
  7         5071895  
  7         71  
17 7     7   93660 use YAML 'LoadFile';
  7         123317  
  7         7114  
18 7     7   92 use File::Basename 'dirname';
  7         17  
  7         1053  
19 7     7   9193 use English '-no_match_vars';
  7         48304  
  7         67  
20 7     7   4054 use IO::Handle;
  7         11  
  7         350  
21 7     7   43 use File::Basename 'basename';
  7         14  
  7         3543  
22              
23 7     7   4454 use Tapper::Remote::Config;
  0            
  0            
24             # ABSTRACT: Control running test programs
25              
26             extends 'Tapper::PRC';
27              
28             our $MAXREAD = 1024; # read that much in one read
29              
30              
31              
32             sub capture_handler_tap
33             {
34             my ($self, $filename) = @_;
35             my $content;
36             open my $fh, '<', $filename or die "Can not open $filename to send captured report";
37             { local $/; $content = <$fh> }
38             close $fh;
39             return $content;
40             }
41              
42              
43              
44             sub send_output
45             {
46             my ($self, $captured_output, $testprogram) = @_;
47              
48             # add missing minimum Tapper meta information
49             my $headerlines = "";
50             $headerlines .= "# Tapper-suite-name: ".basename($testprogram->{program})."\n" unless $captured_output =~ /\# Tapper-suite-name:/;
51             $headerlines .= "# Tapper-machine-name: ".$self->cfg->{hostname}."\n" unless $captured_output =~ /\# Tapper-machine-name:/;
52             $headerlines .= "# Tapper-reportgroup-testrun: ".$self->cfg->{test_run}."\n" unless $captured_output =~ /\# Tapper-reportgroup-testrun:/;
53              
54             $captured_output =~ s/^(1\.\.\d+\n)/$1$headerlines/m;
55              
56             my ($error, $message) = $self->tap_report_away($captured_output);
57             return $message if $error;
58             return 0;
59              
60             }
61              
62              
63              
64             sub testprogram_execute
65             {
66             my ($self, $test_program) = @_;
67              
68             my $program = $test_program->{program};
69             my $chdir = $test_program->{chdir};
70             my $progpath = $self->cfg->{paths}{testprog_path};
71             my $output = $program;
72             $output =~ s|[^A-Za-z0-9_-]|_|g;
73             $output = $test_program->{out_dir}.$output;
74              
75              
76             # make relative paths absolute
77             $program=$progpath.$program if $program !~ m(^/);
78              
79             # try to catch non executables early
80             return("tried to execute $program which does not exist") unless -e $program;
81              
82              
83             if (not -x $program) {
84             system ("chmod", "ugo+x", $program);
85             return("tried to execute $program which is not an execuable and can not set exec flag") if not -x $program;
86             }
87              
88             return("tried to execute $program which is a directory") if -d $program;
89             return("tried to execute $program which is a special file (FIFO, socket, device, ..)") unless -f $program or -l $program;
90              
91             foreach my $file (@{$test_program->{upload_before} || [] }) {
92             my $target_name =~ s|[^A-Za-z0-9_-]|_|g;
93             $target_name = $test_program->{out_dir}.'/before/'.$target_name;
94             File::Copy::copy($file, $target_name);
95             }
96              
97             $self->log->info("Try to execute test suite $program");
98              
99             pipe (my $read, my $write);
100             return ("Can't open pipe:$!") if not (defined $read and defined $write);
101              
102             my $pid=fork();
103             return( "fork failed: $!" ) if not defined($pid);
104              
105             if ($pid == 0) { # hello child
106             close $read;
107             %ENV = (%ENV, %{$test_program->{environment} || {} });
108             open (STDOUT, ">", "$output.stdout") or syswrite($write, "Can't open output file $output.stdout: $!"),exit 1;
109             open (STDERR, ">", "$output.stderr") or syswrite($write, "Can't open output file $output.stderr: $!"),exit 1;
110             if ($chdir) {
111             if (-d $chdir) {
112             chdir $chdir;
113             } elsif ($chdir == "AUTO" and $program =~ m,^/, ) {
114             chdir dirname($program);
115             }
116             }
117             exec ($program, @{$test_program->{argv} || []}) or syswrite($write,"$!\n");
118             close $write;
119             exit -1;
120             } else {
121             # hello parent
122             close $write;
123             my $killed;
124             # (XXX) better create a process group an kill this
125             local $SIG{ALRM}=sub {
126             $killed = 1;
127             kill (15, $pid);
128              
129             # allow testprogram to react on SIGTERM
130             my $grace_period = $ENV{HARNESS_ACTIVE} ? 1 : 60; # wait less during test
131             while ($grace_period and (kill 0, $pid)) {
132             waitpid($pid,0);
133             sleep 1;
134             $grace_period--;
135             }
136             kill (9, $pid);
137             };
138             alarm ($test_program->{timeout} || 0);
139             waitpid($pid,0);
140             my $retval = $?;
141             alarm(0);
142              
143             foreach my $file (@{$test_program->{upload_after} || [] }) {
144             my $target_name =~ s|[^A-Za-z0-9_-]|_|g;
145             $target_name = $test_program->{out_dir}.'/after/'.$target_name;
146             File::Copy::copy($file, $target_name);
147             }
148             if ($test_program->{capture}) {
149             my $captured_output;
150             given($test_program->{capture}) {
151             when ('tap') { eval { $captured_output = $self->capture_handler_tap("$output.stdout")}; return $@ if $@;};
152             default { return "Can not handle captured output, unknown capture type '$test_program->{capture}'. Valid types are (tap)"};
153             }
154             my $error_msg = $self->send_output($captured_output, $test_program);
155             return $error_msg if $error_msg;
156             }
157              
158             return "Killed $program after $test_program->{timeout} seconds" if $killed;
159             if ( $retval ) {
160             my $error;
161             sysread($read,$error, $MAXREAD);
162             return("Executing $program failed:$error");
163             }
164             }
165             return 0;
166             }
167              
168              
169              
170             sub guest_start
171             {
172             my ($self) = @_;
173             my ($error, $retval);
174             GUEST:
175             for (my $i=0; $i<=$#{$self->cfg->{guests}}; $i++) {
176             my $guest = $self->cfg->{guests}->[$i];
177             if ($guest->{exec}){
178             my $startscript = $guest->{exec};
179             $self->log->info("Try to start virtualisation guest with $startscript");
180             if (not -s $startscript) {
181             $self->mcp_send({prc_number => ($i+1), state => 'error-guest',
182             error => qq(Startscript "$startscript" is empty or does not exist at all)});
183             next GUEST;
184             } else {
185             # just try to set it executable always
186             if (not -x $startscript) {
187             unless (system ("chmod", "ugo+x", $startscript) == 0) {
188             $self->mcp_send({prc_number => ($i+1), state => 'error-guest',
189             error =>
190             return qq(Unable to set executable bit on "$startscript": $!)
191             });
192             next GUEST;
193             }
194             }
195             }
196             if (not system($startscript) == 0 ) {
197             $retval = qq(Can't start virtualisation guest using startscript "$startscript");
198             $self->mcp_send({prc_number => ($i+1), state => 'error-guest',
199             error => $retval});
200             next GUEST;
201             }
202             } elsif ($guest->{svm}){
203             my $xm = `which xm`; chomp $xm;
204             $self->log->info("Try load Xen guest described in ",$guest->{svm});
205             ($error, $retval) = $self->log_and_exec($xm, 'create', $guest->{svm});
206             if ($error) {
207             $self->mcp_send({prc_number => ($i+1), state => 'error-guest',
208             error => $retval});
209             next GUEST;
210             }
211             } elsif ($guest->{xen}) {
212             $self->log->info("Try load Xen guest described in ",$guest->{xen});
213              
214             my $guest_file = $guest->{xen};
215             if ($guest_file =~ m/^(.+)\.(?:xl|svm)$/) {
216             $guest_file = $1;
217             }
218              
219             my $xm = `which xm`; chomp $xm;
220             my $xl = `which xl`; chomp $xl;
221              
222             if ( -e $xl ) {
223             ($error, $retval) = $self->log_and_exec($xl, 'create', $guest_file.".xl");
224             if ($error) {
225             $self->mcp_send({prc_number => ($i+1), state => 'error-guest',
226             error => $retval});
227             next GUEST;
228             }
229             } elsif ( -e $xm ) {
230             ($error, $retval) = $self->log_and_exec($xm, 'create', $guest_file.".svm");
231             if ($error) {
232             $self->mcp_send({prc_number => ($i+1), state => 'error-guest',
233             error => $retval});
234             next GUEST;
235             }
236             } else {
237             $retval = "Can not find both xm and xl.";
238             $self->mcp_send({prc_number => ($i+1), state => 'error-guest',
239             error => $retval});
240             next GUEST;
241             }
242             }
243             $self->mcp_send({prc_number => ($i+1), state => 'start-guest'});
244             }
245             return 0;
246             }
247              
248              
249             sub create_log
250             {
251             my ($self) = @_;
252             my $testrun = $self->cfg->{test_run};
253             my $outdir = $self->cfg->{paths}{output_dir}."/$testrun/test/";
254             my ($error, $retval);
255              
256             for (my $i = 0; $i <= $#{$self->cfg->{guests}}; $i++) {
257             # guest count starts with 1, arrays start with 0
258             my $guest_number=$i+1;
259              
260             # every guest gets its own subdirectory
261             my $guestoutdir="$outdir/guest-$guest_number/";
262              
263             $error = $self->makedir($guestoutdir);
264             return $error if $error;
265              
266             $self->log_and_exec("touch $guestoutdir/console");
267             $self->log_and_exec("chmod 666 $guestoutdir/console");
268             ($error, $retval) = $self->log_and_exec("ln -sf $guestoutdir/console /tmp/guest$guest_number.fifo");
269             return "Can't create guest console file $guestoutdir/console: $retval" if $error;
270             }
271             return 0;
272             }
273              
274              
275              
276              
277             sub nfs_mount
278             {
279             my ($self) = @_;
280             my ($error, $retval);
281              
282             $error = $self->makedir($self->cfg->{paths}{prc_nfs_mountdir});
283             return $error if $error;
284              
285             ($error, $retval) = $self->log_and_exec("mount",$self->cfg->{paths}{prc_nfs_mountdir});
286             return 0 if not $error;
287             ($error, $retval) = $self->log_and_exec("mount",$self->cfg->{prc_nfs_server}.":".$self->cfg->{paths}{prc_nfs_mountdir},$self->cfg->{paths}{prc_nfs_mountdir});
288             # report error, but only if not already mounted
289             return "Can't mount ".$self->cfg->{paths}{prc_nfs_mountdir}.":$retval" if ($error and ! -d $self->cfg->{paths}{prc_nfs_mountdir}."/live");
290             return 0;
291             }
292              
293              
294             sub control_testprogram
295             {
296             my ($self) = @_;
297             $ENV{TAPPER_TESTRUN} = $self->cfg->{test_run};
298             $ENV{TAPPER_SERVER} = $self->cfg->{mcp_server};
299             $ENV{TAPPER_REPORT_SERVER} = $self->cfg->{report_server};
300             $ENV{TAPPER_REPORT_API_PORT} = $self->cfg->{report_api_port};
301             $ENV{TAPPER_REPORT_PORT} = $self->cfg->{report_port};
302             $ENV{TAPPER_HOSTNAME} = $self->cfg->{hostname};
303             $ENV{TAPPER_REBOOT_COUNTER} = $self->cfg->{reboot_counter} if $self->cfg->{reboot_counter};
304             $ENV{TAPPER_MAX_REBOOT} = $self->cfg->{max_reboot} if $self->cfg->{max_reboot};
305             $ENV{TAPPER_GUEST_NUMBER} = $self->cfg->{guest_number} || 0;
306             $ENV{TAPPER_SYNC_FILE} = $self->cfg->{syncfile} if $self->cfg->{syncfile};
307             $ENV{CRITICALITY} = $self->cfg->{criticality} // 4; # provide criticality for autoreport test scripts (4 == max)
308             if ($self->{cfg}->{testplan}) {
309             $ENV{TAPPER_TESTPLAN_ID} = $self->cfg->{testplan}{id};
310             $ENV{TAPPER_TESTPLAN_PATH} = $self->cfg->{testplan}{path};
311             }
312              
313              
314              
315             my $test_run = $self->cfg->{test_run};
316             my $out_dir = $self->cfg->{paths}{output_dir}."/$test_run/test/";
317             my @testprogram_list;
318             @testprogram_list = @{$self->cfg->{testprogram_list}} if $self->cfg->{testprogram_list};
319              
320              
321             # prepend outdir with guest number if we are in virtualisation guest
322             $out_dir.="guest-".$self->{cfg}->{guest_number}."/" if $self->{cfg}->{guest_number};
323              
324              
325             my $error = $self->makedir($out_dir);
326              
327             # can't create output directory. Make
328             if ($error) {
329             $self->log->warn($error);
330             $out_dir = tempdir( CLEANUP => 1 );
331             }
332              
333             $ENV{TAPPER_OUTPUT_PATH}=$out_dir;
334              
335             if ($self->cfg->{test_program}) {
336             my $argv;
337             my $environment;
338             my $chdir;
339             $argv = $self->cfg->{parameters} if $self->cfg->{parameters};
340             $environment = $self->cfg->{environment} if $self->cfg->{environment};
341             $chdir = $self->cfg->{chdir} if $self->cfg->{chdir};
342             my $timeout = $self->cfg->{timeout_testprogram} || 0;
343             $timeout = int $timeout;
344             my $runtime = $self->cfg->{runtime};
345             push (@testprogram_list, {program => $self->cfg->{test_program},
346             chdir => $chdir,
347             parameters => $argv,
348             environment => $environment,
349             timeout => $timeout,
350             runtime => $runtime,
351             upload_before => $self->cfg->{upload_before},
352             upload_after => $self->cfg->{upload_after},
353             });
354             }
355              
356              
357             for (my $i=0; $i<=$#testprogram_list; $i++) {
358             my $testprogram = $testprogram_list[$i];
359              
360             $ENV{TAPPER_TS_RUNTIME} = $testprogram->{runtime} || 0;
361              
362             # unify differences in program vs. program_list vs. virt
363             $testprogram->{program} ||= $testprogram->{test_program};
364             $testprogram->{timeout} ||= $testprogram->{timeout_testprogram};
365             $testprogram->{argv} = $testprogram->{parameters} if @{$testprogram->{parameters} || []};
366              
367             # create hash for testprogram_execute
368             $testprogram->{timeout} ||= 0;
369             $testprogram->{out_dir} = $out_dir;
370              
371             my $retval = $self->testprogram_execute($testprogram);
372              
373             if ($retval) {
374             my $error_msg = "Error while executing $testprogram->{program}: $retval";
375             $self->mcp_inform({testprogram => $i, state => 'error-testprogram', error => $error_msg});
376             $self->log->info($error_msg);
377             } else {
378             $self->mcp_inform({testprogram => $i , state => 'end-testprogram'});
379             $self->log->info("Successfully finished test suite $testprogram->{program}");
380             }
381              
382             }
383              
384             return(0);
385             }
386              
387              
388              
389             sub get_peers_from_file
390             {
391             my ($self, $file) = @_;
392             my $peers;
393              
394             $peers = LoadFile($file);
395             return "Syncfile does not contain a list of host names" if not ref($peers) eq 'ARRAY';
396              
397             my $hostname = $self->cfg->{hostname};
398             my %peerhosts;
399             foreach my $host (@$peers) {
400             $peerhosts{$host} = 1;
401             }
402             delete $peerhosts{$hostname};
403              
404             return \%peerhosts;
405             }
406              
407              
408              
409             sub wait_for_sync
410             {
411             my ($self, $syncfile) = @_;
412              
413             my %peerhosts; # easier to delete than from array
414              
415             eval {
416             %peerhosts = %{$self->get_peers_from_file($syncfile)};
417             };
418             return $@ if $@;
419              
420              
421             my $hostname = $self->cfg->{hostname};
422             my $port = $self->cfg->{sync_port};
423             my $sync_srv = IO::Socket::INET->new( LocalPort => $port, Listen => 5, );
424             my $select = IO::Select->new($sync_srv);
425              
426             $self->log->info("Trying to sync with: ". join(", ",sort keys %peerhosts));
427              
428             foreach my $host (keys %peerhosts) {
429             my $remote = IO::Socket::INET->new(PeerPort => $port, PeerAddr => $host,);
430             if ($remote) {
431             $remote->print($hostname);
432             $remote->close();
433             delete($peerhosts{$host});
434             }
435             if ($select->can_read(0)) {
436             my $msg_srv = $sync_srv->accept();
437             my $remotehost;
438             $msg_srv->read($remotehost, 2048); # no hostnames are that long, anything longer is wrong and can be ignored
439             chomp $remotehost;
440             $msg_srv->close();
441             if ($peerhosts{$remotehost}) {
442             delete($peerhosts{$remotehost});
443             } else {
444             $self->log->warn(qq(Received sync request from host "$remotehost" which is not in our peerhost list. Request was sent from ),$msg_srv->peerhost);
445             }
446             }
447             $self->log->debug("In sync with $host.");
448              
449             }
450              
451             while (%peerhosts) {
452             if ($select->can_read()) { # TODO: timeout handling
453             my $msg_srv = $sync_srv->accept();
454             my $remotehost;
455             $msg_srv->read($remotehost, 2048); # no hostnames are that long, anything longer is wrong and can be ignored
456             chomp $remotehost;
457             $msg_srv->close();
458             if ($peerhosts{$remotehost}) {
459             delete($peerhosts{$remotehost});
460             $self->log->debug("In sync with $remotehost.");
461             } else {
462             $self->log->warn(qq(Received sync request from host "$remotehost" which is not in our peerhost list. Request was sent from ),$msg_srv->peerhost);
463             }
464             } else {
465             # handle timeout here when can_read() has a timeout eventually
466             }
467             }
468             return 0;
469             }
470              
471              
472             sub send_keep_alive_loop
473             {
474             my ($self, $sleeptime) = @_;
475             return unless $sleeptime;
476             while (1) {
477             $self->mcp_inform("keep-alive");
478             sleep($sleeptime);
479             }
480             return;
481             }
482              
483              
484              
485             sub run
486             {
487             my ($self) = @_;
488             my $retval;
489             my $producer = Tapper::Remote::Config->new();
490             my $config = $producer->get_local_data("test-prc0");
491             $self->cfg($config);
492             $self->cfg->{reboot_counter} = 0 if not defined($self->cfg->{reboot_counter});
493              
494             if ($self->cfg->{log_to_file}) {
495             $self->log_to_file('testing');
496             }
497              
498             if ($config->{times}{keep_alive_timeout}) {
499             $SIG{CHLD} = 'IGNORE';
500             my $pid = fork();
501             if ($pid == 0) {
502             $self->send_keep_alive_loop($config->{times}{keep_alive_timeout});
503             exit;
504             } else {
505             $config->{keep_alive_child} = $pid;
506             }
507             }
508              
509             # ignore error
510             $self->log_and_exec('ntpdate -s gwo');
511              
512             if ($config->{prc_nfs_server}) {
513             $retval = $self->nfs_mount();
514             $self->log->warn($retval) if $retval;
515             }
516              
517             $self->log->logdie($retval) if $retval = $self->create_log();
518              
519             if ($config->{scenario_id}) {
520             my $syncfile = $config->{paths}{sync_path}."/".$config->{scenario_id}."/syncfile";
521             $self->cfg->{syncfile} = $syncfile;
522              
523             $retval = $self->wait_for_sync($syncfile);
524             $self->log->logdie("Can not sync - $retval") if $retval;
525             }
526              
527             if ($self->{cfg}->{guest_count}) {
528              
529             $retval = $self->guest_start();
530             $self->log->error($retval) if $retval;
531             }
532              
533             $retval = $self->mcp_inform({state => 'start-testing'}) if not $self->cfg->{reboot_counter};
534              
535             $retval = $self->control_testprogram() if $self->cfg->{test_program} or $self->cfg->{testprogram_list};
536              
537             if ($self->cfg->{max_reboot}) {
538             $self->mcp_inform({state => 'reboot', count => $self->cfg->{reboot_counter}, max_reboot => $self->cfg->{max_reboot}});
539             if ($self->cfg->{reboot_counter} < $self->cfg->{max_reboot}) {
540             $self->cfg->{reboot_counter}++;
541             YAML::Syck::DumpFile($config->{filename}, $self->{cfg}) or $self->mcp_error("Can't write config to file: $!");
542             $self->log_and_exec("reboot");
543             return 0;
544             }
545              
546             }
547              
548              
549             # no longer send keepalive
550             if ($config->{keep_alive_child}) {
551             kill 15, $config->{keep_alive_child};
552             sleep 2;
553             kill 9, $config->{keep_alive_child};
554             }
555             sleep 1; # make sure last end-testing can't overtake last end-testprogram (Yes, this did happen)
556             $retval = $self->mcp_inform({state => 'end-testing'});
557              
558              
559             }
560              
561             1;
562              
563             __END__
564             =pod
565              
566             =encoding utf-8
567              
568             =head1 NAME
569              
570             Tapper::PRC::Testcontrol - Control running test programs
571              
572             =head1 FUNCTIONS
573              
574             =head2 capture_handler_tap
575              
576             This function is a handler for the capture function. It handles capture
577             requests of type 'tap'. This means the captured output is supposed to be
578             TAP already and therefore no transformation is needed.
579              
580             @param file handle - opened file handle
581              
582             @return string - output in TAP format
583             @return error - die()
584              
585             =head2 send_output
586              
587             Send the captured TAP output to the report receiver.
588              
589             @param string - TAP text
590              
591             @return success - 0
592             @return error - error string
593              
594             =head2 testprogram_execute
595              
596             Execute one testprogram. Handle all error conditions.
597              
598             @param hash ref - contains all config options for program to execute
599             * program - program name
600             * timeout - timeout in seconds
601             * outdir - output directory
602             * parameters - arrayref of strings - parameters for test program
603             * environment - hashref of strings - environment variables for test program
604             * chdir - string - where to chdir before executing the testprogram
605              
606             @return success - 0
607             @return error - error string
608              
609             =head2 guest_start
610              
611             Start guest images for virtualisation. Only Xen guests can be started at the
612             moment.
613              
614             @return success - 0
615             @return error - error string
616              
617             =head2 create_log
618              
619             Checks whether fifos for guest logging exists and creates them if
620             not. Existing files of wrong type are deleted.
621              
622             @retval success - 0
623             @retval error - error string
624              
625             =head2 nfs_mount
626              
627             Mount the output directory from an NFS server. This method is used since we
628             only want to mount this NFS share in live mode.
629              
630             @return success - 0
631             @return error - error string
632              
633             =head2 control_testprogram
634              
635             Control running of one program including caring for its input, output and
636             the environment variables some testers asked for.
637              
638             @return success - 0
639             @return error - error string
640              
641             =head2 get_peers_from_file
642              
643             Read syncfile and extract list of peer hosts (not including this host).
644              
645             @param string - file name
646              
647             @return success - hash ref
648              
649             @throws plain error message
650              
651             =head2 wait_for_sync
652              
653             Synchronise with other hosts belonging to the same interdependent testrun.
654              
655             @param array ref - list of hostnames of peer machines
656              
657             @return success - 0
658             @return error - error string
659              
660             =head2 send_keep_alive_loop
661              
662             Send keepalive messages to MCP in an endless loop.
663              
664             @param int - sleep time between two keepalives
665              
666             =head2 run
667              
668             Main function of Program Run Control.
669              
670             =head1 AUTHOR
671              
672             AMD OSRC Tapper Team <tapper@amd64.org>
673              
674             =head1 COPYRIGHT AND LICENSE
675              
676             This software is Copyright (c) 2012 by Advanced Micro Devices, Inc..
677              
678             This is free software, licensed under:
679              
680             The (two-clause) FreeBSD License
681              
682             =cut
683