File Coverage

blib/lib/PkgForge/Handler/Buildd.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package PkgForge::Handler::Buildd; # -*-perl-*-
2 1     1   2028 use strict;
  1         3  
  1         35  
3 1     1   6 use warnings;
  1         2  
  1         49  
4              
5             # $Id: Buildd.pm.in 17469 2011-06-01 12:36:28Z squinney@INF.ED.AC.UK $
6             # $Source:$
7             # $Revision: 17469 $
8             # $HeadURL: https://svn.lcfg.org/svn/source/tags/PkgForge-Server/PkgForge_Server_1_1_10/lib/PkgForge/Handler/Buildd.pm.in $
9             # $Date: 2011-06-01 13:36:28 +0100 (Wed, 01 Jun 2011) $
10              
11             our $VERSION = '1.1.10';
12              
13 1     1   6 use English qw(-no_match_vars);
  1         2  
  1         9  
14 1     1   478 use File::Copy ();
  1         2  
  1         118  
15 1     1   6 use File::Path ();
  1         1  
  1         15  
16 1     1   4 use File::Spec ();
  1         2  
  1         13  
17 1     1   5 use File::Temp ();
  1         1  
  1         13  
18 1     1   43 use PkgForge::BuildInfo ();
  0            
  0            
19             use PkgForge::BuildLog ();
20             use PkgForge::Job ();
21             use Try::Tiny;
22             use UNIVERSAL::require;
23              
24             use Readonly;
25             Readonly my $BUILD_COMMAND_STUB => 'PkgForge::BuildCommand';
26             Readonly my $TMPDIR_PERMS => oct('0750');
27              
28             use Moose;
29             use MooseX::Types::Moose qw(Int Str);
30             use Moose::Util::TypeConstraints;
31              
32             extends 'PkgForge::Handler';
33              
34             with 'PkgForge::Registry::Role';
35              
36             subtype 'PkgForgeBuildCommandBuilder'
37             => as role_type("$BUILD_COMMAND_STUB\::Builder");
38              
39             subtype 'PkgForgeBuildCommandSubmitter'
40             => as role_type("$BUILD_COMMAND_STUB\::Submitter");
41              
42             subtype 'PkgForgeBuildCommandSigner'
43             => as role_type("$BUILD_COMMAND_STUB\::Signer");
44              
45             subtype 'PkgForgeBuildCommandCheck'
46             => as role_type("$BUILD_COMMAND_STUB\::Check");
47              
48             subtype 'PkgForgeBuildCommandReporter'
49             => as role_type("$BUILD_COMMAND_STUB\::Reporter");
50              
51             has 'builder' => (
52             is => 'ro',
53             isa => 'Str|HashRef[HashRef]',
54             required => 1,
55             );
56              
57             has 'submitters' => (
58             traits => ['Array'],
59             is => 'ro',
60             isa => 'ArrayRef',
61             required => 0,
62             handles => {
63             'has_submitters' => 'count',
64             'submitters_list' => 'elements',
65             },
66             default => sub { [] },
67             );
68              
69             has 'signers' => (
70             traits => ['Array'],
71             is => 'ro',
72             isa => 'ArrayRef',
73             required => 0,
74             handles => {
75             'has_signers' => 'count',
76             'signers_list' => 'elements',
77             },
78             default => sub { [] },
79             );
80              
81             has 'checks' => (
82             traits => ['Array'],
83             is => 'ro',
84             isa => 'ArrayRef',
85             required => 0,
86             handles => {
87             has_checks => 'count',
88             checks_list => 'elements',
89             },
90             default => sub { [] },
91             );
92              
93             has 'reports' => (
94             traits => ['Array'],
95             is => 'ro',
96             isa => 'ArrayRef',
97             required => 0,
98             handles => {
99             has_reports => 'count',
100             reports_list => 'elements',
101             },
102             default => sub { [] },
103             );
104              
105             has 'build_command' => (
106             is => 'ro',
107             isa => 'PkgForgeBuildCommandBuilder',
108             init_arg => undef,
109             required => 1,
110             lazy => 1,
111             builder => 'load_build_command',
112             );
113              
114             has 'submit_commands' => (
115             traits => ['Array'],
116             is => 'ro',
117             isa => 'ArrayRef[PkgForgeBuildCommandSubmitter]',
118             init_arg => undef,
119             required => 1,
120             lazy => 1,
121             handles => {
122             submit_commands_list => 'elements',
123             },
124             builder => 'load_submit_commands',
125             );
126              
127             has 'sign_commands' => (
128             traits => ['Array'],
129             is => 'ro',
130             isa => 'ArrayRef[PkgForgeBuildCommandSigner]',
131             init_arg => undef,
132             required => 1,
133             lazy => 1,
134             handles => {
135             sign_commands_list => 'elements',
136             },
137             builder => 'load_sign_commands',
138             );
139              
140             has 'check_commands' => (
141             traits => ['Array'],
142             is => 'ro',
143             isa => 'ArrayRef[PkgForgeBuildCommandCheck]',
144             init_arg => undef,
145             required => 1,
146             lazy => 1,
147             handles => {
148             check_commands_list => 'elements',
149             },
150             builder => 'load_check_commands',
151             );
152              
153             has 'report_commands' => (
154             traits => ['Array'],
155             is => 'ro',
156             isa => 'ArrayRef[PkgForgeBuildCommandReporter]',
157             init_arg => undef,
158             required => 1,
159             lazy => 1,
160             handles => {
161             report_commands_list => 'elements',
162             },
163             builder => 'load_report_commands',
164             );
165              
166             has 'name' => (
167             is => 'ro',
168             isa => Str,
169             required => 1,
170             documentation => 'Name of the build daemon',
171             );
172              
173             has '+tmpdir' => (
174             lazy => 1,
175             default => sub {
176             my ($self) = @_;
177             return File::Spec->catdir( '/var/tmp/pkgforge/', $self->name );
178             },
179             );
180              
181             has 'timeout' => (
182             is => 'ro',
183             isa => Int,
184             default => 600, # 10 minutes
185             documentation => 'Time after which a build job should be killed',
186             );
187              
188             has '+logconf' => (
189             default => '/etc/pkgforge/log-buildd.cfg',
190             );
191              
192             no Moose;
193             __PACKAGE__->meta->make_immutable;
194              
195             sub load_command_module {
196             my ( $self, $modtype, $value ) = @_;
197              
198             my ( $module, %params );
199              
200             if ( find_type_constraint('Str')->check($value) ) {
201             $module = $value;
202             } elsif ( find_type_constraint('HashRef')->check($value) ) {
203             my %data = %{$value};
204              
205             my @names = keys %data;
206             if ( scalar @names > 1 ) {
207             my $names_list = join q{, }, @names;
208             die "Only one $modtype module may be specified, found a list: $names_list\n";
209             }
210              
211             $module = $names[0];
212             if ( ref $data{$module} eq 'HASH' ) {
213             %params = %{$data{$module}};
214             } elsif ( ref $data{$module} eq 'ARRAY' ) {
215             %params = @{$data{$module}};
216             } else {
217             die "Cannot handle extra $modtype module parameters in $data{$module}\n";
218             }
219              
220             } else {
221             die "Cannot load $modtype module\n";
222             }
223              
224             my $stub = join q{::}, $BUILD_COMMAND_STUB, $modtype;
225             if ( $module !~ m/^\Q$stub\E/ ) {
226             $module = join q{::}, $stub, $module;
227             }
228              
229             $module->require
230             or die "Cannot load $modtype module '$module': $UNIVERSAL::require::ERROR\n";
231              
232             return ( $module, %params );
233             }
234              
235             sub load_commands_list {
236             my ( $self, $modtype, @modlist ) = @_;
237              
238             my @commands;
239             for my $entry (@modlist) {
240             my ( $module, %params ) = $self->load_command_module( $modtype, $entry );
241              
242             my $command = $module->new(%params);
243              
244             push @commands, $command;
245             }
246              
247             return \@commands;
248             }
249              
250             sub load_build_command {
251             my ($self) = @_;
252              
253             my ( $module, %params )
254             = $self->load_command_module( 'Builder', $self->builder );
255              
256             # Load the builder from the database
257              
258             my $builder_in_db = $self->registry->get_builder($self->name);
259             my $platform = $builder_in_db->platform;
260              
261             if ( !$platform->active ) {
262             die "This platform is not currently registered as active\n";
263             }
264              
265             my ( $platform_name, $platform_arch ) = ( $platform->name, $platform->arch );
266              
267             my $obj = $module->new(
268             platform => $platform_name,
269             architecture => $platform_arch,
270             timeout => $self->timeout,
271             tmpdir => $self->tmpdir,
272             debug => $self->debug,
273             %params,
274             );
275              
276             return $obj;
277             }
278              
279             sub load_submit_commands {
280             my ($self) = @_;
281              
282             my $build_cmd = $self->build_command;
283              
284             my $platform_name = $build_cmd->platform;
285             my $platform_arch = $build_cmd->architecture;
286              
287             my @submitters;
288             for my $entry ($self->submitters_list) {
289              
290             my ( $module, %params )
291             = $self->load_command_module( 'Submitter', $entry );
292              
293             my $obj = $module->new(
294             platform => $platform_name,
295             architecture => $platform_arch,
296             %params,
297             );
298              
299             push @submitters, $obj;
300             }
301              
302             return \@submitters;
303             }
304              
305             sub load_check_commands {
306             my ($self) = @_;
307              
308             return $self->load_commands_list( 'Check', $self->checks_list );
309             }
310              
311             sub load_sign_commands {
312             my ($self) = @_;
313              
314             return $self->load_commands_list( 'Signer', $self->signers_list );
315             }
316              
317              
318             sub load_report_commands {
319             my ($self) = @_;
320              
321             return $self->load_commands_list( 'Reporter', $self->reports_list );
322             }
323              
324             sub preflight {
325             my ($self) = @_;
326              
327             my $accept_dir = $self->accepted;
328              
329             if ( !-d $accept_dir ) {
330             $self->logger->log_and_die(
331             level => 'critical',
332             message => "Accepted jobs directory '$accept_dir' does not exist"
333             );
334             }
335              
336             my $results_dir = $self->results;
337              
338             if ( !-d $results_dir ) {
339             $self->logger->log_and_die(
340             level => 'critical',
341             message => "Results directory '$results_dir' does not exist"
342             );
343             }
344              
345             # Test that we have the correct permissions to write into the
346             # results directory.
347              
348             try {
349             my $tmp = File::Temp->new( TEMPLATE => 'pkgforge-XXXX',
350             UNLINK => 1,
351             DIR => $results_dir );
352             $tmp->print("test\n") or die "Failed to print to temp file: $OS_ERROR\n";
353             $tmp->close or die "Could not close temp file: $OS_ERROR\n";
354             } catch {
355             $self->logger->log_and_die(
356             level => 'critical',
357             message => "Results directory '$results_dir' is not writable: $_"
358             );
359             };
360              
361             # Verify the environment for the various commands.
362              
363             for my $cmd ( $self->build_command,
364             $self->check_commands_list,
365             $self->submit_commands_list,
366             $self->report_commands_list ) {
367             try {
368             $cmd->verify_environment;
369             } catch {
370             $self->logger->log_and_die(
371             level => 'critical',
372             message => "The build environment is incomplete: $_\n",
373             );
374             };
375             }
376              
377             # Setup the temporary directory if it does not already exist. This
378             # is different directory for each build daemon.
379              
380             my $tmpdir = $self->tmpdir;
381              
382             if ( !-d $tmpdir ) {
383             my $ok = eval { File::Path::mkpath( $tmpdir, 0, $TMPDIR_PERMS ) };
384             if ( !$ok || $EVAL_ERROR ) {
385             $self->logger->log_and_die(
386             level => 'critical',
387             message => "Could not create temporary directory '$tmpdir': $EVAL_ERROR"
388             );
389             }
390             }
391              
392             chmod $TMPDIR_PERMS, $tmpdir or
393             $self->logger->log_and_die(
394             level => 'critical',
395             message => "Could not set permissions on temporary directory '$tmpdir': $OS_ERROR"
396             );
397              
398             $ENV{TMPDIR} = $tmpdir;
399              
400             return 1;
401             }
402              
403             sub next_task {
404             my ($self) = @_;
405              
406             my $registry = $self->registry;
407             my $next_task = eval { $registry->next_new_task($self->name) };
408              
409             if ($EVAL_ERROR) {
410             $self->log_problem( 'Failed to query registry for a new build job',
411             $EVAL_ERROR );
412             return;
413             }
414              
415             if ( !defined $next_task && $self->debug ) {
416             my $name = $self->name;
417             $self->logger->debug("Nothing waiting in the queue for '$name'");
418             }
419              
420             return $next_task;
421             }
422              
423             sub load_job {
424             my ( $self, $task ) = @_;
425              
426             my $uuid = $task->job->uuid;
427              
428             my $jobs_dir = $self->accepted;
429              
430             my $job_dir = File::Spec->catdir( $jobs_dir, $uuid );
431              
432             my $job = eval { PkgForge::Job->new_from_dir($job_dir) };
433              
434             if ( $EVAL_ERROR || !defined $job ) {
435             $self->log_problem( "Failed to load build job from $job_dir",
436             $EVAL_ERROR );
437             return;
438             }
439              
440             return $job;
441             }
442              
443             sub execute {
444             my ( $self, $task ) = @_;
445              
446             try {
447             $task ||= $self->next_task();
448             if ( !defined $task ) {
449             return;
450             }
451              
452             my $job = $self->load_job($task);
453             if ( !defined $job ) {
454             return;
455             }
456              
457             my $ok = $self->build($job);
458             if ( !$ok ) {
459             return;
460             }
461             } catch {
462             $self->logger->log_and_die(
463             level => 'critical',
464             message => "Something bad happened: $_",
465             );
466             };
467              
468             return;
469             }
470              
471             sub fail_job {
472             my ( $self, $job ) = @_;
473              
474             eval { $self->registry->fail_task( $self->name, $job->id ) };
475              
476             if ($EVAL_ERROR) {
477             $self->logger->log_and_die(
478             level => 'critical',
479             message => "Failed to set failure for build job '$job' in registry: $EVAL_ERROR",
480             );
481             }
482              
483             $self->logger->notice("Failed job $job");
484              
485             return 1;
486             }
487              
488             sub finish_job {
489             my ( $self, $job ) = @_;
490              
491             eval { $self->registry->finalise_task( $self->name, $job->id ) };
492              
493             if ($EVAL_ERROR) {
494             $self->logger->log_and_die(
495             level => 'critical',
496             message => "Failed to finalise build job '$job' in registry: $EVAL_ERROR",
497             );
498             }
499              
500             $self->logger->notice("Successfully completed job $job");
501              
502             return 1;
503             }
504              
505             sub reset_unfinished_tasks {
506             my ($self) = @_;
507              
508             eval { $self->registry->reset_unfinished_tasks($self->name) };
509             if ( $EVAL_ERROR ) {
510             $self->log_problem( 'Failed to reset unfinished tasks', $EVAL_ERROR );
511             }
512              
513             return;
514             }
515              
516             sub store_products {
517             my ( $self, $buildinfo, $resultsdir ) = @_;
518              
519             if ( !-d $resultsdir ) {
520             try {
521             File::Path::mkpath($resultsdir);
522             } catch {
523             $self->logger->log_and_die(
524             level => 'critical',
525             message => "Failed to create directory '$resultsdir': $_",
526             );
527             };
528             }
529              
530             my %sources_list = map { $_ => 1 } $buildinfo->source_files;
531              
532             for my $file ( $buildinfo->products_list ) {
533             if ( !-f $file ) {
534             $self->logger->error("Failed to transfer file '$file': it does not exist");
535             next;
536             }
537              
538             # Do not transfer source files that we already have stored elsewhere
539              
540             my $basename = (File::Spec->splitpath($file))[2];
541             if ( exists $sources_list{$basename} ) {
542             next;
543             }
544              
545             my $ok = File::Copy::copy( $file, $resultsdir );
546             if ( !$ok ) {
547             $self->logger->error("Failed to transfer file '$file': $OS_ERROR");
548             }
549              
550             }
551              
552             return;
553             }
554              
555             sub store_logs {
556             my ( $self, $buildinfo, $resultsdir ) = @_;
557              
558             if ( !-d $resultsdir ) {
559             try {
560             File::Path::mkpath($resultsdir);
561             } catch {
562             $self->logger->log_and_die(
563             level => 'critical',
564             message => "Failed to create directory '$resultsdir': $_",
565             );
566             };
567             }
568              
569             for my $file ($buildinfo->logs_list) {
570             if ( !-f $file ) {
571             $self->logger->error("Failed to transfer logfile '$file': it does not exist");
572             next;
573             }
574              
575             my $ok = File::Copy::copy( $file, $resultsdir );
576             if ( !$ok ) {
577             $self->logger->error("Failed to transfer logfile '$file': $OS_ERROR");
578             }
579              
580             }
581              
582             return;
583             }
584              
585              
586             sub build {
587             my ( $self, $job ) = @_;
588              
589             # Local log file directory for this particular job.
590              
591             my $job_logdir = File::Spec->catdir( $self->logdir, $self->name, $job->id );
592              
593             if ( !-d $job_logdir ) {
594             $self->logger->debug("Will create job log directory '$job_logdir'");
595              
596             try {
597             File::Path::mkpath($job_logdir);
598             } catch {
599             $self->logger->log_and_die(
600             level => 'critical',
601             message => "Failed to create directory '$job_logdir': $_",
602             );
603             };
604             }
605              
606             # Final results directory for this particular job.
607              
608             my $builder = $self->build_command;
609             my $subdir = $builder->platform;
610             if ( $builder->has_architecture ) {
611             $subdir = join q{-}, $subdir, $builder->architecture;
612             }
613              
614             my $job_resultsdir
615             = File::Spec->catdir( $self->results, $job->id, $subdir );
616              
617             if ( !-d $job_resultsdir ) {
618              
619             $self->logger->debug("Will create job results directory '$job_resultsdir'");
620              
621             try {
622             File::Path::mkpath($job_resultsdir);
623             } catch {
624             $self->logger->log_and_die(
625             level => 'critical',
626             message => "Failed to create directory '$job_resultsdir': $_",
627             );
628             };
629             }
630              
631             # Check the job actually contains some source packages.
632              
633             my $num = $job->count_packages;
634             if ( $num == 0 ) {
635             $self->logger->notice("Ignoring build job $job as it has zero source packages");
636             $self->finish_job($job);
637             return 1;
638             } else {
639             $self->logger->debug("Job has $num source packages");
640             }
641              
642             $self->logger->notice("Attempting to build job $job");
643              
644             my $buildlog = PkgForge::BuildLog->new( debug => $self->debug,
645             logdir => $job_logdir );
646              
647             my $buildinfo_file = File::Spec->catfile( $job_resultsdir,
648             'buildinfo.yml' );
649              
650             my $buildinfo = PkgForge::BuildInfo->new(
651             builder => $self->name,
652             jobid => $job->id,
653             logs => [$buildlog->logfile],
654             yamlfile => $buildinfo_file,
655             );
656              
657             my $success = 0;
658             try {
659              
660             $buildinfo->phase_reached('build');
661             my $build_ok = $self->build_command->run( $job, $buildinfo, $buildlog );
662             if ( !$build_ok ) {
663             die "Failed to build $job\n";
664             }
665              
666             $buildinfo->phase_reached('check');
667             for my $command ($self->check_commands_list) {
668             $buildinfo->phase_reached("check-$command");
669             my $pass = $command->run( $job, $buildinfo, $buildlog );
670              
671             if (!$pass) {
672             die "Failed check: '$command'\n";
673             }
674             }
675              
676             $buildinfo->phase_reached('sign');
677             for my $command ($self->sign_commands_list) {
678             $buildinfo->phase_reached("sign-$command");
679             my $pass = $command->run( $job, $buildinfo, $buildlog );
680              
681             if (!$pass) {
682             die "Failed signing: '$command'\n";
683             }
684             }
685              
686             $buildinfo->phase_reached('submit');
687             for my $command ($self->submit_commands_list) {
688             $buildinfo->phase_reached("submit-$command");
689             my $pass = $command->run( $job, $buildinfo, $buildlog );
690              
691             if (!$pass) {
692             die "Failed submission: '$command'\n";
693             }
694             }
695              
696              
697             $buildinfo->phase_reached('end');
698             $buildinfo->completed(1);
699             $success = 1;
700              
701             } catch {
702             $buildlog->logger->error("An error occurred during the build process: $_");
703             $buildlog->logger->error("Giving up at phase: '" . $buildinfo->last_phase . "'");
704              
705             $success = 0;
706             };
707              
708             # Mark the status in the registry
709             if ($success) {
710             $self->finish_job($job);
711             } else {
712             $self->fail_job($job);
713             }
714              
715             for my $report ($self->report_commands_list) {
716             try {
717             $report->run( $job, $buildinfo, $buildlog );
718             } catch {
719             $buildlog->logger->error( "Failed to run report: $_" );
720             };
721             }
722              
723             $buildinfo->store_in_yamlfile();
724             $self->store_logs( $buildinfo, $job_resultsdir );
725             $self->store_products( $buildinfo, $job_resultsdir );
726              
727             return $success;
728             }
729              
730             1;
731             __END__
732              
733             =head1 NAME
734              
735             PkgForge::Handler::Buildd - Package Forge Build Daemon
736              
737             =head1 VERSION
738              
739             This documentation refers to PkgForge::Handler::Buildd version 1.1.10
740              
741             =head1 SYNOPSIS
742              
743             use PkgForge::Handler::Buildd;
744              
745             my $handler = PkgForge::Handler::Buildd->new_with_config();
746              
747             $handler->preflight;
748              
749             $handler->execute();
750              
751             =head1 DESCRIPTION
752              
753             This is a Package Forge build handler. It does the work of driving the
754             build process for a particular task which is part of a previously
755             accepted job. Each supported platform/architecture has a separate
756             build handler which selects the appropriate tasks from the queue,
757             attempts the build and then submits the results if successful. This
758             module is intended to be platform and package format agnostic, the
759             relevant Package Forge builders will be selected and used to generate
760             the actual packages.
761              
762             =head1 ATTRIBUTES
763              
764             This class inherits attributes from the L<PkgForge::Handler> class,
765             see that module documentation for full details. The following
766             attributes are added or modified:
767              
768             =over
769              
770             =back
771              
772             =head1 CONFIGURATION AND ENVIRONMENT
773              
774             The value of any attribute can be set via the YAML configuration files
775             C</etc/pkgforge/handlers.yml> and C</etc/pkgforge/buildd.yml>
776              
777             The logging for this build handler is configured using the
778             C</etc/pkgforge/log-buildd.cfg> file. If the file does not exist then
779             the handler will log to stderr.
780              
781             =head1 DEPENDENCIES
782              
783             This module is powered by L<Moose> and uses L<MooseX::Types>.
784              
785             =head1 SEE ALSO
786              
787             L<PkgForge>, L<PkgForge::Handler>
788              
789             =head1 PLATFORMS
790              
791             This is the list of platforms on which we have tested this
792             software. We expect this software to work on any Unix-like platform
793             which is supported by Perl.
794              
795             ScientificLinux5, Fedora13
796              
797             =head1 BUGS AND LIMITATIONS
798              
799             Please report any bugs or problems (or praise!) to bugs@lcfg.org,
800             feedback and patches are also always very welcome.
801              
802             =head1 AUTHOR
803              
804             Stephen Quinney <squinney@inf.ed.ac.uk>
805              
806             =head1 LICENSE AND COPYRIGHT
807              
808             Copyright (C) 2010-2011 University of Edinburgh. All rights reserved.
809              
810             This library is free software; you can redistribute it and/or modify
811             it under the terms of the GPL, version 2 or later.
812              
813             =cut