#!/usr/bin/perl use strict; use warnings; use Linux::Inotify2; use YAML::Tiny; use Getopt::Long; use File::stat; use File::Find; use File::Basename; use File::Path qw(make_path); use File::Copy qw(move); use AnyEvent; use RPM2; use Time::HiRes 'time'; use Email::MIME; use Email::Sender::Simple qw(sendmail); use Email::Sender::Transport::Sendmail; use Net::LDAP; # Init an empty conf my $conf = {}; # Disable output buffering $| = 1; # Defaults for command line flags my $opt = { config => '../etc/config.yml', verbose => 0, quiet => 0 }; # Read some options from the command line GetOptions ( 'config=s' => \$opt->{config}, 'quiet' => \$opt->{quiet}, 'verbose' => \$opt->{verbose} ); # Check if the config file exists, and if so, parse it # and load it in $conf if ( -e $opt->{config} ) { log_verbose( "Reading config file " . $opt->{config} ); my $yaml = YAML::Tiny->read( $opt->{config} ); if ( not $yaml or not $yaml->[0] ) { die "Config file " . $opt->{config} . " is invalid\n"; } $conf = $yaml->[0]; } else { # If the config file doesn't exist, just die die "Config file " . $opt->{config} . " doesn't exist\n"; } my $ldap_msg; my $inotify = new Linux::Inotify2 or die "Unable to create new inotify object: $!"; log_verbose("Searching for folders in $conf->{paths}->{uploads}"); find({ wanted => sub { -d and create_watcher($inotify, $File::Find::name); } }, $conf->{paths}->{uploads}); my $cv = AnyEvent->condvar; my $poller = AnyEvent->io( fh => $inotify->fileno, poll => 'r', cb => sub { $inotify->poll } ); # Receive event signals (inotify signals) $cv->recv; # Print messages only if the verbose flag was given sub log_verbose { my $msg = shift; print $msg . "\n" if ( $opt->{verbose} ); } # Print normal messages sub log_info { my $msg = shift; print $msg . "\n" if ( not $opt->{quiet} ); } # Print error messages sub log_error { my $msg = shift; print $msg . "\n"; } # Create a watcher for a specific directory sub create_watcher { my ($inotify, $dir) = @_; log_verbose("Start watching folder $dir"); $inotify->watch ($dir, IN_CLOSE_WRITE | IN_MOVED_TO, sub { my $event = shift; my $candidate = $event->fullname; handle_submit($candidate); }); } # takes the path of an SRPM to rebuild, # build it with mock, sign the result, update the repo # and sync to remote mirrors if defined sub handle_submit { my $srpm = shift; if (not -f $srpm){ log_verbose("$srpm isn't a file, ignoring"); return; } if ($srpm !~ m/src\.rpm$/i){ log_verbose("New file $srpm isn't an src.rpm file, ignoring"); return; } log_info("New file to process $srpm"); my $submiter = getpwuid(stat($srpm)->uid); my $email; log_info("File submited by $submiter"); my $ldap = ldap_connect(); if (defined $ldap){ $email = user2email($ldap, $submiter); if (not defined $email){ log_verbose("LDAP returned no result"); } } if (defined $email){ log_verbose("Notifications will be sent to $email"); } else { log_verbose("No email address for $submiter, no notification will be sent"); } $ldap->done; $ldap->disconnect; # Do not check the signature here # We could try to submit a signed src.rpm for which we do not have the key system-wide my $src_pkg = RPM2->open_package($srpm, RPM2->_rpmvsf_nosignatures); if (not $src_pkg->is_source_package){ log_verbose("Couldn't parse $srpm as a valid srpm"); return; } my $target = basename(dirname($srpm)); if (not defined $conf->{targets}->{$target}){ log_info("$srpm submited for target $target, but it's not defined in the configuration"); } foreach my $arch (@{$conf->{targets}->{$target}}){ my $job_id = $src_pkg->as_nvre() . '-' . time(); my $result = $conf->{paths}->{builds} . '/' . $submiter . '/' . $target . '-' . $arch . '/' . $job_id; log_info("Rebuilding $srpm for $target/$arch in $result (job ID $job_id)"); make_path($result); my $mock_msg; foreach my $out (qx(mock -r $target-$arch --resultdir=$result $srpm 2>&1)){ $mock_msg .= $out; chomp $out; log_info("[$job_id] $out"); } if ($? != 0) { log_info("[$job_id] Build submited by $submiter failed"); handle_error($job_id, 'Mock build', $mock_msg, $email); return; } my $repo_dir = $conf->{paths}->{repo}; my $repo_cache_dir = $conf->{paths}->{repo_cache}; if ($src_pkg->release =~ m/\.(beta|git\.)/){ $repo_dir .= '/testing'; $repo_cache_dir .= '/testing'; } $repo_dir .= '/' . $target; $repo_cache_dir .= '/' . $target; find({ wanted => sub { return if (not -f); return if (not $_ =~ m/\.rpm$/); my $built_pkg = $_; log_info("[$job_id] Signing package $built_pkg"); # Note : the optional passphrase for the gpg key is in rpmmacros qx(rpm --addsign $built_pkg); if ($? != 0) { log_info("[$job_id] Signing failed"); handle_error($job_id, 'Package signature error', "Command rpm --addsign $built_pkg failed", $email); return; } # Open the package without checking the signature, as the key might not be present in the # rpm trusted store my $pkg = RPM2->open_package($built_pkg, RPM2->_rpmvsf_nosignatures); my $dest = $repo_dir; if ($pkg->is_source_package){ $dest .= '/SRPMS'; } else { # the resulting RPM can be noarch, so use this instead of $arch $dest .= '/' . $pkg->arch; } log_info("[$job_id] Moving $built_pkg to the repo $dest"); make_path($dest); make_path($repo_cache_dir); move $built_pkg, $dest . '/' . basename($built_pkg); } }, $result); log_info("[$job_id] Removing old packages"); qx(rm -f \$(repomanage --keep=2 --old $repo_dir)); log_info("[$job_id] Updating repo metadata for $target"); qx(createrepo --checksum sha -x "*debuginfo*" --update -c $repo_cache_dir $repo_dir); if ($? != 0) { log_info("[$job_id] Createrepo failed"); handle_error( $job_id, 'Createrepo error', "Command createrepo --checksum sha -x \"*debuginfo*\" --update -c $repo_cache_dir $repo_dir", $email ); return; } log_info("[$job_id] Building package finished"); # Now push to mirrors if defined if (defined $conf->{mirror} and defined $conf->{mirror}->{push}){ foreach my $mirror (@{$conf->{mirror}->{push}}){ log_info("[$job_id] syncing repo to $mirror->{dest}"); my $rsync_cmd = 'rsync '; if (defined $mirror->{rsync_opts}){ $rsync_cmd .= join(' ', @{$mirror->{rsync_opts}}); } else { $rsync_cmd .= join(' ', @{$conf->{mirror}->{rsync_opts}}); } $rsync_cmd .= ' ' . $conf->{paths}->{repo} . '/ ' . $mirror->{dest} . '/'; log_verbose("[$job_id] Running command $rsync_cmd"); foreach my $out (qx($rsync_cmd 2>&1)){ chomp $out; log_verbose("[$job_id] $out"); } if ($? != 0) { log_info("[$job_id] Syncing to $mirror->{dest} failed"); handle_error($job_id, 'Mirror update error', "Command $rsync_cmd failed", $email); return; } } } if (defined $email){ my $body = "Resulting RPM are available in $conf->{paths}->{repo}/$target"; if (defined $conf->{mirror} and defined $conf->{mirror}->{push}){ $body .= "\nand have been synced to the following mirror:\n"; foreach my $mirror (@{$conf->{mirror}->{push}}){ $body .= "$mirror->{dest}\n"; } } send_notification( $email, "Rebuilding " . $src_pkg->as_nvre() . " for $target/$arch succeded", $body ); } } if (defined $ldap){ $ldap->done; $ldap->disconnect; } return; } # Handle errors. Log it, and notify the admin sub handle_error { my $job_id = shift; my $step = shift; my $err = shift; my $dest = shift; log_error( $err ); if ( defined $dest ) { send_notification( $dest, "Error while building $job_id", "Building $job_id failed at step '$step'. The error was\n$err\n" ); } } # Send an email message sub send_notification { my $to = shift; my $subject = shift; my $body = shift; my $mail = Email::MIME->create( header_str => [ From => $conf->{notify}->{from}, To => $to, Subject => $subject ], attributes => { charset => 'utf-8', encoding => 'base64' }, body_str => $body ); my $transport = Email::Sender::Transport::Sendmail->new(); sendmail( $mail, { transport => $transport } ); } # Lookup in LDAP if we can get the email address of a user sub user2email { my $ldap = shift; my $user = shift; if (not defined $ldap or not defined $conf->{ldap}->{search_base} or not defined $conf->{ldap}->{search_filter}){ log_verbose("LDAP not connected or not configured, skiping lookup"); return; } my $filter = $conf->{ldap}->{search_filter}; $filter =~ s/\{user\}/$user/g; log_verbose("Searching in $conf->{ldap}->{search_base} with filter $filter"); my $results = $ldap->search( base => $conf->{ldap}->{search_base}, filter => $filter, attrs => [ $conf->{ldap}->{email_attr} ] ); if ($results->code){ log_verbose("Error occured while searching in LDAP : " . $results->error); return; } if ($results->count != 1){ log_verbose("Searching returned " . $results->count . "result(s), while it should have returned 1"); return; } return $results->entry(0)->get_value( $conf->{ldap}->{email_attr} ); } # Connect to LDAP # which will be used to lookup the email address of the submiter sub ldap_connect { my $ldaph; if (defined $conf->{ldap} and defined $conf->{ldap}->{servers}){ log_verbose("Connecting to " . join(', ', @{$conf->{ldap}->{servers}})); $ldaph = new Net::LDAP($conf->{ldap}->{servers}, timeout => 10, ); if (not defined $ldaph){ log_info("Couldn't connect to any LDAP servers (" . join(',', @{$conf->{ldap}->{servers}}) . ")"); } else { if (defined $conf->{ldap}->{start_tls} and $conf->{ldap}->{start_tls}){ log_verbose("Upgrade LDAP connection using StartTLS"); $ldap_msg = $ldaph->start_tls( verify => 'require' ); if ($ldap_msg->code){ log_verbose("StartTLS failed : " . $ldap_msg->error); log_verbose("LDAP support will be disabled"); $ldaph = undef; } } if (defined $conf->{ldap}->{bind_dn} and defined $conf->{ldap}->{bind_pass}){ log_verbose("Binding as $conf->{ldap}->{bind_dn}"); $ldap_msg = $ldaph->bind( $conf->{ldap}->{bind_dn}, password => $conf->{ldap}->{bind_pass} ); if ($ldap_msg->code){ log_verbose("LDAP bind failed : " . $ldap_msg->error); log_verbose("LDAP support will be disabled"); $ldaph = undef; } } else { log_verbose("Using anonymous bind"); $ldap_msg = $ldaph->bind; } } } else { log_verbose("No LDAP servers configured"); } return $ldaph; }