Subversion Repositories DevTools

Rev

Rev 1042 | Rev 1046 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

#! /usr/bin/perl
########################################################################
# Copyright (C) 2011 Vix-ERG Limited, All rights reserved
#
# Module name   : blatDaemon.pl
# Module type   :
# Compiler(s)   : Perl
# Environment(s):
#
# Description   :
#
# Usage         :   ARGV[0] - Path to config file for this instance
#
#......................................................................#

require 5.008_002;
use strict;
use warnings;
use Getopt::Long;
use File::Basename;
use Data::Dumper;
use File::Spec::Functions;
use POSIX ":sys_wait_h";
use File::Temp qw/tempfile/;

use FindBin;                                    # Determine the current directory
use lib "$FindBin::Bin/lib";                    # Allow local libraries

use Utils;
use StdLogger;                                  # Log to sdtout
use Logger;                                     # Log to file

#
#   Database interface
#   Pinched from jats and modified so that this software is not dependent on JATS
#
use IO::Handle;
use JatsRmApi;
use DBI;

#
#   Globals
#
my $logger = StdLogger->new();                  # Stdout logger. Only during config
my $name = basename( $ARGV[0]);
   $name =~ s~.conf$~~;
my $now = 0;
my $tagDirTime = 0;
my $lastDirScan = 0;
my $lastReleaseScan = 0;
my $releaseScanMode = 0;
my $lastTagListScan = 0;
my $transferred;
my $mtimeConfig = 0;
my $conf;
my $extraPkgs;
my $excludePkgs;
my %releaseData;

#
#   Describe config uration parameters
#
my %cdata = (
    '.ignore'         => {'pkg\.(.+)' => 'pkgs' },
    'piddir'          => {'mandatory' => 1    , 'fmt' => 'dir'},
    'sleep'           => {'default' => 5      , 'fmt' => 'period'},
    'dpkg_archive'    => {'mandatory' => 1    , 'fmt' => 'dir'},
    'logfile'         => {'mandatory' => 1    , 'fmt' => 'vfile'},
    'logfile.size'    => {'default' => '1M'   , 'fmt' => 'size'},
    'logfile.count'   => {'default' => 9      , 'fmt' => 'int'},
    'verbose'         => {'default' => 0      , 'fmt' => 'int'},
    'user'            => {'mandatory' => 1    , 'fmt' => 'text'},
    'hostname'        => {'mandatory' => 1    , 'fmt' => 'text'},
    'identity'        => {'mandatory' => 1    , 'fmt' => 'file'},
    'bindir'          => {'mandatory' => 1    , 'fmt' => 'text'},
    'tagdir'          => {'mandatory' => 1    , 'fmt' => 'dir'},
    'forcedirscan'    => {'default' => 100    , 'fmt' => 'period'},
    'tagage'          => {'default' => '10m'  , 'fmt' => 'period'},
    'tagListUpdate'   => {'default' => '1h'  , 'fmt' => 'period'},
    'synctime'        => {'default' => '2h'   , 'fmt' => 'period'},
    'syncretry'       => {'default' => '5m'   , 'fmt' => 'period'},
    'project'         => {'mandatory' => 0    , 'fmt' => 'int_list'},
    'release'         => {'mandatory' => 0    , 'fmt' => 'int_list'},
    'writewindow'     => {'default' => '3h'   , 'fmt' => 'period'},
    'maxpackages'     => {'default' => 5      , 'fmt' => 'int'},
    'deletePackages'  => {'default' => 0      , 'fmt' => 'bool'},
    'deleteImmediate' => {'default' => 0      , 'fmt' => 'bool'},
    'deleteAge'       => {'default' => 0      , 'fmt' => 'period'},
);


#
#   Read in the configuration
#       Set up a logger
#       Write a pidfile - thats not used
readConfig();
Utils::writepid($conf);
$logger->logmsg("Starting...");
sighandlers($conf);

#
#   Main processing loop
#   Will exit when terminated by parent
#
while ( 1 )
{
    $logger->verbose3("Processing");
    $now = time();

    $transferred = {};
    readConfig();
    processReleaseList();
    processTags();
    maintainTagList();
    %releaseData = ();

    sleep( $conf->{'sleep'} );
    waitpid(-1, WNOHANG);                           # Reap dead children
}
$logger->logmsg("Child End");
exit 0;

#-------------------------------------------------------------------------------
# Function        : readConfig
#
# Description     : Re read the config file if it modification time has changed
#
# Inputs          : Nothing
#
# Returns         : Nothing
#
sub readConfig
{
    my ($mtime) = Utils::mtime($ARGV[0]);
    if ( $mtimeConfig != $mtime )
    {
        $logger->logmsg("Reading config file: $ARGV[0]");
        $mtimeConfig = $mtime;
        my $errors;
        ($conf, $errors) = Utils::readconf ( $ARGV[0], \%cdata );
        if ( scalar @{$errors} > 0 )
        {
            warn "$_\n" foreach (@{$errors});
            die ("Config contained errors\n");
        }

        #
        #   Reset some information
        #   Create a new logger
        #
        $logger = Logger->new($conf);
        $conf->{logger} = $logger;
        $conf->{'pidfile'} = $conf->{'piddir'} . '/' . $name . '.pid';
        $logger->verbose("Log Levl: $conf->{verbose}");

        #
        #   Extract extra package config
        #
        $extraPkgs = {};
        $excludePkgs = {};
        while (my($key, $data) = each ( %{$conf->{pkgs}} ))
        {
            if ( $data eq 'EXCLUDE' )
            {
                $excludePkgs->{$key} = 1;
                $logger->verbose("Exclude Pkg: $key");
            }
            else
            {
                $extraPkgs->{$key}{$data} = 1;
                $logger->verbose("Extra Pkg: $key -> $data");
            }
        }
    }
}


#-------------------------------------------------------------------------------
# Function        : processReleaseList
#
# Description     : Process the release list
#                       Determine if its time to process release list
#                       Determine release list
#                       Determine release content
#                       Determine new items
#
# Inputs          : None
#
# Returns         : Nothing
#
sub processReleaseList
{
    #
    #   Is Release List Processing active
    #   Can configure blat to disable release sync
    #   This will then allow 'new' packages to be sent
    #
    if ( $conf->{maxpackages} == 0 || $conf->{'synctime'} <= 0)
    {
        $logger->verbose2("processReleaseList disabled");
        return;
    }

    #
    #   Time to perform the scan
    #   Will do at startup and every time period there after
    #
    my $wtime = $releaseScanMode ? $conf->{'syncretry'} : $conf->{'synctime'};
    return unless ( $now > ($lastReleaseScan + $wtime ));
    $logger->verbose("processReleaseList");
    $lastReleaseScan = $now;
    $releaseScanMode = 1;                                   # Assume error

    #
    #   Get list of packages from Remote site
    #   Invoke a program on the remote site and parse the results
    #
    #     ssh  -i ./ssh/id_rsa_pkg_admin  pkg_admin@10.247.28.57 "./get_plist.pl"
    #
    #   Returned data looks like:
    #   1141792602 GMT(Wed Mar  8 04:36:42 2006) [DL] ishieldmodules/11.5.0.cots
    #   
    #
    my $remotePkgList;
    my $ph;
    my $tgt_cmd = "$conf->{'bindir'}/get_plist.pl";
    my $ssh_cmd = "ssh -n -o \"BatchMode yes\" -i $conf->{'identity'} $conf->{'user'}\@$conf->{'hostname'} \"$tgt_cmd\" 2>&1";

    $logger->verbose2("processReleaseList:ssh_cmd:$ssh_cmd");
    open ($ph, "$ssh_cmd |");
    while ( <$ph> )
    {
        chomp;
        if ( parsePkgList($_, \%{$remotePkgList} ) )
        {
            $logger->verbose2("processReleaseList:Data: $_");
        }
        else
        {
            $logger->warn("processReleaseList:Bad Data: $_");
        }
    }
    close ($ph);
    $logger->verbose("processReleaseList:End: $?");
    if ( $? != 0 )
    {
        $logger->warn("Cannot retrieve package list: $?");
        return;
    }
#Utils::DebugDumpData ("remotePkgList", $remotePkgList);
    

    #
    #   Determine the set of packages in the releases to be transferred
    #   Examine
    #
    my @rlist = getReleaseList();
    unless ( @rlist )
    {
        $logger->verbose2("No Releases to Process");
        return;
    }
    my $pkgList = getPkgList(@rlist);

    #
    #   Append extra packages
    #   These are packages that are specifically named by the user
    #
    #   Note: If there are symbolic links, then the target of the
    #         link is treated used as the package name
    #
    #         Symlink MUST be within the same directory
    #
    #         Used to transfer jats2_current
    #
    my $pkgLink;
    while ( (my ($pname, $pvers)) = each %{$extraPkgs} ) {
        while ( (my ($pver, $pdata) ) = each %{$pvers} ) {

            my $epath = catfile( $conf->{'dpkg_archive'} , $pname, $pver );
            if ( -l $epath )
            {
                my $lver = readlink( $epath );
                if ( ! defined $lver )
                {
                    $logger->warn("Cant resolve symlink: $pname, $pver");
                    next;
                }

                if ( $lver =~ m ~/~ )
                {
                    $logger->warn("Won't resolve symlink: $pname, $pver, $lver");
                    next;
                }
                $pkgLink->{$pname}{$pver} = $lver;
                $pdata = $pver;
                $pver = $lver;
            }

            $logger->verbose2("Add extra package: $pname, $pver, $pdata");
            $pkgList->{$pname}{$pver} = $pdata;
        }
    }

    #
    #   If there are no packages to process, then assume that this is an error
    #   condition. Retry the operation soon.
    #
    unless ( keys %{$pkgList} )
    {

        $logger->verbose2("No packages to process");
        return;
    }

#    while ( (my ($pname, $pvers)) = each %{$pkgList} )
#    {
#        while ( (my ($pver, $ptime) ) = each %{$pvers} )
#        {
#            print "L-- $pname, $pver, $ptime \n";
#
#        }
#    }

    #
    #   Delete Excess Packages
    #       Packages not required on the target
    #           KLUDGE: Don't delete links to packages
    #           Don't delete packages marked for deletion
    #
    my $excessPkgList;
    if ( $conf->{deletePackages} )
    {
        while ( (my ($pname, $pvers)) = each %{$remotePkgList} )
        {
            while ( (my ($pver, $pdata) ) = each %{$pvers} )
            {
                if ( !exists $pkgList->{$pname}{$pver} )
                {
                    if ( exists $pkgLink->{$pname}{$pver} )
                    {
                        $logger->verbose2("Keep Excess package-link: ${pname}/${pver}");
                        next;
                    }

                    if ( exists $excludePkgs->{$pname} )
                    {
                        $logger->verbose2("Keep Excluded package: ${pname}");
                        next;
                    }

                    if ( exists $pdata->{deleted} )
                    {
                        if ( $conf->{deleteAge} )
                        {
                            if ( $pdata->{deleted} <= $conf->{deleteAge} )
                            {
                                $logger->verbose2("Already marked for future age deletion: ${pname}/${pver}, $pdata->{deleted}");
                                next;
                            }
                            $pdata->{FORCEDELETE} = 1;
                        }

                        if ( !$conf->{deleteImmediate} )
                        {
                            $logger->verbose2("Already marked for deletion: ${pname}/${pver}");
                            next;
                        }
                    }

                    #
                    #   Force deletion
                    #       deleteImmediate mode
                    #       target is a broken link
                    #
                    $pdata->{FORCEDELETE} = 1
                        if ($conf->{deleteImmediate} || $pdata->{broken});

                    $excessPkgList->{$pname}{$pver} = $pdata;
                    $logger->verbose("Excess package: ${pname}/${pver}");
                }
            }
        }
    }

    #
    #   Process the remote list and the local list
    #   The remote time-stamp is the modification time of the packages descpkg file
    #
    #   Mark for transfer packages that
    #       Are in the local set but not the remote set
    #       Have a different time stamp
    #
    #   Ignore packages not in the local archive
    #   Ignore packages that don't have a descpkg
    #   Ignore packages that are writable - still being formed
    #
    my $needPkgList;
    while ( (my ($pname, $pvers)) = each %{$pkgList} )
    {
        #
        #   Ignore excluded packages
        #
        next if ( exists $excludePkgs->{$pname} );

        while ( (my ($pver, $pdata) ) = each %{$pvers} )
        {
            my $tmtime = $remotePkgList->{$pname}{$pver}{time} || 0;

            # Package is present in both list
            my ($mtime, $mode) = Utils::mtime( catfile( $conf->{'dpkg_archive'} , $pname, $pver, 'descpkg' ));
            if ( $mtime == 0 )
            {
                # PackageVersion not in local archive (at least the descpkg file is not)
                # Skip now - will pick it up later
                $logger->verbose("Package not in dpkg_archive: $pname, $pver");
                next;
            }

            if ( $mode & 0222 )
            {
                # Descpkg file is writable
                # Package may be in the process of being created
                # If the package has been wriatble for a long time, then
                # consider for transfer
                my $age = $now - $mtime;
                if ( $age < ($conf->{'writewindow '} || 600) )
                {
                    $logger->verbose("Package is writable: $pname, $pver, ", $now - $mtime);
                    next;
                }
            }

            if ( $mtime != $tmtime )
            {
                # Package not present on target, or timestamps differ
                $logger->verbose("Package Needs to be transferred: $pname, $pver, $mtime, $tmtime");
                $needPkgList->{$pname}{$pver} = $pdata;
                next;
            }
        }
    }

    #
    #   Debug output only
    #   Display what we need to transfer
    #
    if ( $conf->{verbose} > 2 )
    {
        while ( (my ($pname, $pvers)) = each %{$needPkgList} )
        {
            while ( (my ($pver, $pdata) ) = each %{$pvers} )
            {
                $logger->verbose("Need to transfer: $pname, $pver, $pdata");
            }
        }
    }

    #
    #   Time to do the real work
    #   Transfer packages and delete excess packages
    #   Note: Perform the transfers first
    #         Limit the number of packages processed in one pass
    #
    my $txcount = $conf->{maxpackages};

    #
    #   Transfer packages that we have identified
    #
    send_pkgs:
    while ( (my ($pname, $pvers)) = each %{$needPkgList} )
    {
        while ( (my ($pver, $pdata) ) = each %{$pvers} )
        {
            if ( --$txcount <= 0 )
            {
                $logger->warn("Max transfer count exceeded");
                $lastReleaseScan = 0;
                last send_pkgs;
            }
            transferPackage ($pname, $pver, $pdata);
        }
    }

    #
    #   Delete packages that have been identified as excess
    #
    delete_pkgs:
    while ( (my ($pname, $pvers)) = each %{$excessPkgList} )
    {
        while ( (my ($pver, $pdata) ) = each %{$pvers} )
        {
            if ( --$txcount <= 0 )
            {
                $logger->warn("Max transfer count exceeded");
                $lastReleaseScan = 0;
                last delete_pkgs;
            }
            deletePackage ($pname, $pver, $pdata);
        }
    }

    #
    #   Send package list to the target
    #
    sendPackageList ($pkgList);

    #
    #   On a successful transfer
    #       Force tag processing
    #       Set scan Mode to normal
    #
    $tagDirTime = 0;
    $releaseScanMode = 0;
}

#-------------------------------------------------------------------------------
# Function        : sendPackageList
#
# Description     : Transfer package list to the target
#
# Inputs          : $pkgList            - Ref to hash of package names and versions
#
# Returns         : Nothing
#                   Don't really care about any errors from this process
#                   Its not essential
#
sub sendPackageList
{
    my ($pkgList) = @_;
    my ($fh, $filename) = tempfile( "/tmp/blat.$$.XXXX", SUFFIX => '.txt');
    $logger->verbose("sendPackageList:TmpFile: $filename");
    
    #
    #   Create a temp file with data
    #
    foreach my $pname ( sort keys %{$pkgList} )
    {
        foreach my $pver ( sort keys %{$pkgList->{$pname}} )
        {
            print $fh "$pname/$pver\n";
        }
    }
    close $fh;

    #
    #   Transfer to target
    #   Create the process pipe to transfer the file
    #   gzip the file and pipe the result through a ssh session to the target machine
    #   gzip -c filename |  ssh  -i $IDENTITY  pkg_admin@${TARGET_HOST} "./receive_file filename"
    #
    my $ph;
    my $gzip_cmd = "gzip --no-name -c \"$filename\"";
    my $tgt_cmd = "$conf->{'bindir'}/receive_file \"ArchiveList\"";
    my $ssh_cmd = "ssh -o \"BatchMode yes\" -i $conf->{'identity'} $conf->{'user'}\@$conf->{'hostname'} \"$tgt_cmd\" 2>&1";

    $logger->verbose2("sendPackageList:gzip_cmd:$gzip_cmd");
    $logger->verbose2("sendPackageList:tgt_cmd:$tgt_cmd");
    $logger->verbose2("sendPackageList:ssh_cmd:$ssh_cmd");

    open ($ph, "$gzip_cmd | $ssh_cmd |");
    while ( <$ph> )
    {
        chomp;
        $logger->verbose2("sendPackageList:Data: $_");
    }
    close ($ph);
    $logger->verbose("sendPackageList:End: $?");
    unlink $filename;
}


#-------------------------------------------------------------------------------
# Function        : getPkgList
#
# Description     : Determine a set of package versions within the list
#                   of provided releases
#
# Inputs          : @rlist              - A list of releases to examine
#
# Returns         : Ref to a hask of package versions
#
sub getPkgList
{
    my %pdata;
    my $RM_DB;
    connectRM(\$RM_DB);
    $logger->verbose("getPkgList");

    #
    #   Determine the releases that are in this project
    #   Build up an sql query
    #
    my @m_rlist;
    push @m_rlist,"rc.RTAG_ID=$_" foreach ( @_ );
    my $m_rlist = join ' OR ', @m_rlist;
    my $m_sqlstr = "SELECT DISTINCT pv.PV_ID, pkg.PKG_NAME, pv.PKG_VERSION" .
                    " FROM RELEASE_MANAGER.RELEASE_CONTENT rc, RELEASE_MANAGER.PACKAGE_VERSIONS pv, RELEASE_MANAGER.PACKAGES pkg" .
                    " WHERE ( $m_rlist ) AND rc.PV_ID = pv.PV_ID AND pv.PKG_ID = pkg.PKG_ID" .
                    " ORDER by PKG_NAME DESC";
    $logger->verbose3("getPkgList:Sql:$m_sqlstr");
                    
    my $sth = $RM_DB->prepare($m_sqlstr);
    if ( defined($sth) )
    {
        if ( $sth->execute( ) )
        {
            if ( $sth->rows )
            {
                while (my @row = $sth->fetchrow_array )
                {
                    $logger->verbose2("getPkgList:Data:@row");
                    $pdata{$row[1]}{$row[2]} = 1;
                }
            }
            $sth->finish();
        }
    }
    else
    {
        $logger->warn("getPkgList: SQL Prepare failure");
    }
   return \%pdata;
}


#-------------------------------------------------------------------------------
# Function        : getReleaseList
#
# Description     : Determine the list of releases to be proccessed
#                   From:
#                       Convert projects to a list of releases
#                       Configured list of releases
#
# Inputs          : None
#
# Returns         : A list of releases to be processed
#
sub getReleaseList
{
    my $RM_DB;
    my %rlist;
    $logger->verbose("getReleaseList");

    #
    #   Cache data
    #   Only for one cycle of the main loop
    #
    if ( exists $releaseData{getReleaseList} )
    {
        $logger->verbose3("getReleaseList:Cache hit");
        return @{$releaseData{getReleaseList}};
    }

    #
    #   Convert list of projects into a list of releases
    #
    my @plist = split /[,\s]+/, $conf->{'project'} || '';
    if ( @plist )
    {
        #
        #   Determine the releases that are in this project
        #   Build up an sql query
        #
        connectRM(\$RM_DB);
        my @m_plist;
        push @m_plist,"PROJ_ID=$_" foreach ( @plist );
        my $m_plist = join ' OR ', @m_plist;
        my $m_sqlstr = "SELECT rt.RTAG_ID" .
                    " FROM RELEASE_MANAGER.RELEASE_TAGS rt" .
                    " WHERE ( $m_plist ) AND rt.OFFICIAL != 'A' AND rt.OFFICIAL != 'Y'";

        $logger->verbose3("getReleaseList:Sql:$m_sqlstr");
        my $sth = $RM_DB->prepare($m_sqlstr);
        if ( defined($sth) )
        {
            if ( $sth->execute( ) )
            {
                if ( $sth->rows )
                {
                    while (my @row = $sth->fetchrow_array )
                    {
                        $logger->verbose2("getReleaseList:Data:@row");
                        $rlist{$row[0]} = 1;
                    }
                }
                $sth->finish();
            }
        }
        else
        {
            $logger->warn("getReleaseList: SQL Prepare failure");
        }
    }

    #
    #   Add in the user specified list of releases
    #
    my @rlist = split /[,\s]+/, $conf->{'release'} || '';
    $rlist{$_} = 1 foreach(@rlist);

    #
    #   Sort for pretty display only
    #
    @{$releaseData{getReleaseList}} = sort {$a <=> $b} keys %rlist;

    return @{$releaseData{getReleaseList}};
}

#-------------------------------------------------------------------------------
# Function        : maintainTagList
#
# Description     : Maintain a data structure for the maintenance of the
#                   tags directory
#
# Inputs          : None
#
# Returns         : Nothing
#
sub maintainTagList
{
    #
    #   Time to perform the scan
    #   Will do at startup and every time period there after
    #
    return unless ( $now > ($lastTagListScan + $conf->{tagListUpdate} ));
    $logger->verbose("maintainTagList");
    $lastTagListScan = $now;

    #
    #   Get list of things
    #
    my %config;
    %{$config{projects}} = map { $_ => 1 } split (/[,\s]+/, $conf->{'project'} || '');
    %{$config{releases}} = map { $_ => 1 } getReleaseList();

    #
    #   Save data
    #
    my $dump =  Data::Dumper->new([\%config], [qw(*config)]);
#print $dump->Dump;
#$dump->Reset;

    #
    #   Save config data
    #
    my $conf_file = catfile( $conf->{'tagdir'},'.config' );
    $logger->verbose3("maintainTagList: Writting $conf_file");

    my $fh;
    open ( $fh, '>', $conf_file ) or $logger->err("Can't create $conf_file: $!");
    print $fh $dump->Dump;
    close $fh;
}


#-------------------------------------------------------------------------------
# Function        : processTags
#
# Description     : Process tags and send marked package versions to the target
#                       Determine if new tags are present
#                       Process each tag
#
# Inputs          : None
#
# Returns         : Nothing
#
sub processTags
{
    #
    #   Determine if new tags are present by examining the time
    #   that the directory was last modified.
    #
    #   Allow for a forced scan to catch packages that did not transfer
    #   on the first attempt
    #
    my ($mtime) = Utils::mtime($conf->{'tagdir'} );
    if ( ($mtime > $tagDirTime) || ($now > ($lastDirScan + $conf->{'forcedirscan'})) )
    {
        $logger->verbose2("processTags: ",$conf->{'tagdir'}, $mtime - $tagDirTime, $now - $lastDirScan);
        $tagDirTime = $mtime;
        $lastDirScan = $now;

        my $dh;
        unless (opendir($dh, $conf->{'tagdir'}))
        {
            $logger->warn ("can't opendir $conf->{'tagdir'}: $!");
            return;
        }

        #
        #   Process each entry
        #   Ignore those that start with a .
        #
        while (my $tag = readdir($dh) )
        {
            next if ( $tag =~ m~^\.~ );
            my $file = "$conf->{'tagdir'}/$tag";
            $logger->verbose3("processTags: $file");

            next unless ( -f $file );
            next if ( $tag  eq 'ReleaseList' );

            if ( $tag =~ m~(.+)::(.+)~  )
            {
                my $package = $1;
                my $version = $2;
                if ( transferPackage( $package, $version ))
                {
                    unlink $file;
                }
                else
                {
                    my ($mtime) = Utils::mtime( $file );
                    if ( $now - $mtime > $conf->{'tagage'} )
                    {
                        $logger->warn ("Delete unsatisfied tag: $tag");
                        unlink $file;
                    }
                }
            }
        }
        closedir $dh;
    }
}

#-------------------------------------------------------------------------------
# Function        : transferPackage
#
# Description     : Transfer specified package to target system
#
# Inputs          : $pname          - Name of the package
#                   $pver           - Package version
#                   $plink          - (optional) Symlink in same package
#
# Returns         : true    - Package transferred
#                   false   - Package not transferred
#
sub transferPackage
{
    my ($pname, $pver, $plink ) = @_;
    my $rv = 0;
    $logger->logmsg("transferPackage: @_");

    #
    #   Do not transfer excluded files
    #
    if ( exists $excludePkgs->{$pname} )
    {
        $logger->verbose("transferPackage: Excluded package not transferred");
        return 1;
    }
    
    #
    #   plink of 1 is not a symlink
    #
    $plink = undef if ( defined($plink) && $plink eq '1' );

    #
    #   If its been transferred in the current scan, then
    #   indicate that all is well
    #
    if ( $transferred->{$pname}{$pver}  )
    {
        $logger->verbose("transferPackage: Already transferred");
        return 1;
    }

    my $sfile = catfile( $conf->{'dpkg_archive'} , $pname, $pver );
    unless ( -d $sfile )
    {
        $logger->warn("transferPackage:Package not found: $pname, $pver");
        return $rv;
    }

    #
    #   Create the process piple to transfer the package
    #   Tar the directory and pipe the result through a ssh session to
    #   the target machine
    #   gtar -czf - -C "$dpkg/${pname}/${pver}" . |  ssh  -i $IDENTITY  pkg_admin@${TARGET_HOST} "./receive_package ${rx_opts} \"$pname\" \"$pver\""
    #
    my $ph;
    my $tar_cmd = "gtar -czf - -C \"$sfile\" .";
    my $tgt_opts = defined($plink) ? "\"-L$plink\"" : '';
    my $tgt_cmd = "$conf->{'bindir'}/receive_package $tgt_opts \"$pname\" \"$pver\"";
    my $ssh_cmd = "ssh -o \"BatchMode yes\" -i $conf->{'identity'} $conf->{'user'}\@$conf->{'hostname'} \"$tgt_cmd\" 2>&1";

    $logger->verbose2("transferPackage:tar_cmd:$tar_cmd");
    $logger->verbose2("transferPackage:tgt_cmd:$tgt_cmd");
    $logger->verbose2("transferPackage:ssh_cmd:$ssh_cmd");

    open ($ph, "$tar_cmd | $ssh_cmd |");
    while ( <$ph> )
    {
        chomp;
        $logger->verbose2("transferPackage:Data: $_");
    }
    close ($ph);
    $logger->verbose("transferPackage:End: $?");
    if ( $? == 0 )
    {
        #
        #   Mark has having been transferred in the current cycle
        #
        $transferred->{$pname}{$pver} = 1;
        $rv = 1;
    }
    else
    {
        $logger->warn("transferPackage:Transfer Error: $pname, $pver, $?");
    }
    return $rv;
}

#-------------------------------------------------------------------------------
# Function        : deletePackage
#
# Description     : Delete specified package to target system
#
# Inputs          : $pname          - Name of the package
#                   $pver           - Package version
#                   $pdata          - Hash of extra data
#
# Returns         : true    - Package transferred
#                   false   - Package not transferred
#
sub deletePackage
{
    my ($pname, $pver, $pdata ) = @_;
    my $rv = 0;
    $logger->logmsg("deletePackage: $pname, $pver");

    #
    #   Create the process pipe to delete the package
    #   Tar the directory and pipe the result through a ssh session to
    #   the target machine
    #   gtar -czf - -C "$dpkg/${pname}/${pver}" . |  ssh  -i $IDENTITY  pkg_admin@${TARGET_HOST} "./receive_package ${rx_opts} \"$pname\" \"$pver\""
    #
    my $ph;
    my $flags = $pdata->{FORCEDELETE}  ? '' : ' -T';
    my $tgt_cmd = "$conf->{'bindir'}/delete_package $flags \"$pname\" \"$pver\"";
    my $ssh_cmd = "ssh -o \"BatchMode yes\" -i $conf->{'identity'} $conf->{'user'}\@$conf->{'hostname'} \"$tgt_cmd\" 2>&1";

    $logger->verbose2("deletePackage:tgt_cmd:$tgt_cmd");
    $logger->verbose2("deletePackage:ssh_cmd:$ssh_cmd");

    open ($ph, "$ssh_cmd |");
    while ( <$ph> )
    {
        chomp;
        $logger->verbose2("deletePackage:Data: $_");
    }
    close ($ph);
    $logger->verbose("deletePackage:End: $?");
    if ( $? == 0 )
    {
        $rv = 1;
    }
    else
    {
        $logger->warn("deletePackage:Error: $pname, $pver, $?");
    }
    return $rv;
}


#-------------------------------------------------------------------------------
# Function        : parsePkgList
#
# Description     : Parse one line from a pkgList
#                   Lines are multiple item="data" items
#
# Inputs          : $line                   - Line of data
#                   $hashp                  - Ref to hash to populate
#
# Returns         : A hash of data items
#
sub parsePkgList
{
    my ($line, $hashp) = @_;
    my $rv;

    while ( $line =~ m~\s*(.+?)="(.+?)"~ )
    {
        $rv->{$1} = $2;
        $line = $';
    }
#Utils::DebugDumpData ("parsePkgList", $rv);

    my $pname = $rv->{pname};
    my $pver =  $rv->{pver};
    return undef unless ( $pname && $pver );

    delete $rv->{pname};
    delete $rv->{pver};
    delete $rv->{GMT};

    $hashp->{$pname}{$pver} = $rv;
    return $hashp;
}


#-------------------------------------------------------------------------------
# Function        : sighandlers
#
# Description     : Install signal handlers
#
# Inputs          : $conf           - System config
#
# Returns         : Nothing
#
sub sighandlers
{
        my $conf = shift;
        my $logger = $conf->{logger};

        $SIG{TERM} = sub {
                # On shutdown
                $logger->logmsg('Received SIGTERM. Shutting down....' );
                unlink $conf->{'pidfile'} if (-f $conf->{'pidfile'});
                exit 0;
        };

        $SIG{HUP} = sub {
                # On logrotate
                $logger->logmsg('Received SIGHUP.');
                $logger->rotatelog();
        };

        $SIG{USR1} = sub {
                # On Force Archive Sync
                $logger->logmsg('Received SIGUSR1.');
        $lastReleaseScan = 0
        };

    $SIG{__WARN__} = sub { $logger->warn("@_") };
    $SIG{__DIE__} = sub { $logger->err("@_") };
    

}

#-------------------------------------------------------------------------------
# Function        : Error, Verbose, Warning
#
# Description     : Support for JatsRmApi
#
# Inputs          : Message
#
# Returns         : Nothing
#
sub Error
{
    $logger->err("@_");
}

sub Verbose
{
    $logger->verbose2("@_");
}

sub Warning
{
    $logger->warn("@_");
}