#!/usr/bin/env perl
# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html

package xCAT::DSHCore;

use locale;
use strict;
use Socket;

use xCAT::MsgUtils;
use xCAT::Utils;

#---------------------------------------------------------------------------

=head3
        fork_no_output

        Forks a process for the given command array and returns the process
        ID for the forked process.  Since no I/O is needed for the pipes, no
        STDOUT/STDERR pipes are returned to the caller.

        Arguments:
        	$fork_id - unique identifer to use for tracking the forked process
        	@command - command and parameter array to execute in the forkec process

        Returns:
        	$pid - process identifer for the forked process
                
        Globals:
        	None
    
        Error:
        	None
    
        Example:
        	$pid = xCAT::DSHCore->fork_no_output('hostname1PID', @command_array);

        Comments:

=cut

#---------------------------------------------------------------------------

sub fork_no_output
{
    my ($class, $fork_id, @command) = @_;

    my $pid;

    if ($pid = xCAT::Utils->xfork)
    {

    }
    elsif (defined $pid)
    {
        open(STDOUT, ">/dev/null");
        open(STDERR, ">/dev/null");

        select(STDOUT);
        $| = 1;
        select(STDERR);
        $| = 1;

        if (!(exec {$command[0]} @command))
        {
            return (-4, undef);
        }

    }
    else
    {
        return (-3, undef);
    }

    return ($pid, undef, undef, undef, undef);
}

#---------------------------------------------------------------------------

=head3
        fork_output

        Forks a process for the given command array and returns the process
        ID for the forked process and references to all I/O pipes for STDOUT
        and STDERR.

        Arguments:
        	$fork_id - unique identifer to use for tracking the forked process
        	@command - command and parameter array to execute in the forkec process

        Returns:
        	$pid - process identifer for the forked process
                
        Globals:
        	None
    
        Error:
        	None
    
        Example:
        	$pid = xCAT::DSHCore->fork_no_output('hostname1PID', @command_array);

        Comments:

=cut

#---------------------------------------------------------------------------

sub fork_output
{
    my ($class, $fork_id, @command) = @_;
no strict;
    my $pid;
    my %pipes = ();

    my $rout_fh = "rout_$fork_id";
    my $rerr_fh = "rerr_$fork_id";
    my $wout_fh = "wout_$fork_id";
    my $werr_fh = "werr_$fork_id";

    (pipe($rout_fh, $wout_fh) == -1) && return (-1, undef);
    (pipe($rerr_fh, $werr_fh) == -1) && return (-2, undef);

    if ($pid = fork)
    {
        close($wout_fh);
        close($werr_fh);
    }

    elsif (defined $pid)
    {
        close($rout_fh);
        close($rerr_fh);

        !(open(STDOUT, ">&$wout_fh")) && return (-5, undef);
        !(open(STDERR, ">&$werr_fh")) && return (-6, undef);

        select(STDOUT);
        $| = 1;
        select(STDERR);
        $| = 1;

        if (!(exec {$command[0]} @command))
        {
            return (-4, undef);
        }

    }
    else
    {
        return (-3, undef);
    }

    return ($pid, *$rout_fh, *$rerr_fh, *$wout_fh, *$werr_fh);
use strict;
}

#---------------------------------------------------------------------------

=head3
        ifconfig_inet

        Builds a list of all IP Addresses bound to the local host and
        stores them in a global list

        Arguments:
        	None

        Returns:
        	None
                
        Globals:
        	@local_inet
    
        Error:
        	None
    
        Example:
        	xCAT::DSHCore->ifconfig_inet;

        Comments:
        	Internal routine only

=cut

#---------------------------------------------------------------------------

sub ifconfig_inet
{
    my @local_inet = ();

    if ($^O eq 'aix')
    {
        my @ip_address = ();
        my @output     = `/usr/sbin/ifconfig -a`;

        foreach my $line (@output)
        {
            ($line =~ /inet ((\d{1,3}?\.){3}(\d){1,3})\s/o)
              && (push @local_inet, $1);
        }
    }

    elsif ($^O eq 'linux')
    {
        my @ip_address = ();
        my @output     = `/sbin/ifconfig -a`;

        foreach my $line (@output)
        {
            ($line =~ /inet addr:((\d{1,3}?\.){3}(\d){1,3})\s/o)
              && (push @local_inet, $1);
        }
    }
}

#---------------------------------------------------------------------------

=head3
        pipe_handler

        Handles and processes dsh output from a given read pipe handle.  The output
        is immediately written to each output file handle as it is available.

        Arguments:
	        $options - options hash table describing dsh configuration options
	        $target_properties - property information of the target related to the pipe handle
	        $read_fh - reference to the read pipe handle
	        $buffer_size - local buffer size to read data from the handle
	        $label - prefix label to use for dsh output
	        $write_buffer - buffer of data that is yet to be written (must wait until \n is read)
	        @write_fhs - array of output file handles where output will be written

        Returns:
        	1 if the EOF reached on $read_fh
        	undef otherwise
                
        Globals:
        	None
    
        Error:
        	None
    
        Example:

        Comments:

=cut

#---------------------------------------------------------------------------
# NOTE: global environment $::__DSH_LAST_LINE only can be used in DSHCore::pipe_handler and DSHCore::pipe_handler_buffer
$::__DSH_LAST_LINE  = undef;
sub pipe_handler
{
    my ($class, $options, $target_properties, $read_fh, $buffer_size, $label,
        $write_buffer, @write_fhs)
      = @_;

    my $line;
    my $target_hostname;
    my $eof_reached = undef;
    my $cust_rc_deal =0;

    if ($::USER_POST_CMD)
    {
        # If user provide post-command to display return code,
        # the keyword 'DSH_RC' will be searched,
        # the return code is gotten in another way as shown like below:
        # ...
        # <output>
        # <return_code>
        # DSH_RC
        #
        # The last two lines are needed to be moved out from output
        $cust_rc_deal = 1;
    }

    while (sysread($read_fh, $line, $buffer_size) != 0
           || ($eof_reached = 1))
    {
        last if ($eof_reached);

        if ($line =~ /^\n$/)
        {

            # need to preserve blank lines in the output.
            $line = $label . $line;
        }

        my @lines = split "\n", $line;

        if (@$write_buffer)
        {
            my $buffered_line = shift @$write_buffer;
            my $next_line     = shift @lines;
            $next_line = $buffered_line . $next_line;
            unshift @lines, $next_line;
        }

        if ($line !~ /\n$/)
        {
            push @$write_buffer, (pop @lines);
        }

        if (@lines)
        {
            if ($cust_rc_deal)
            {
                # Dump the last line at the beginning of current buffer
                if ($::__DSH_LAST_LINE)
                {
                    unshift @lines, "$::__DSH_LAST_LINE" ;
                }
                # Pop current buffer to $::__DSH_LAST_LINE
                $::__DSH_LAST_LINE = $lines[scalar @lines - 1];
                pop @lines;
                # Skip this loop if array @lines is empty.
                if (scalar @lines == 0)
                {
                    next;
                }
            }

            $line = join "\n", @lines;
            $line .= "\n";

            if ($line =~ /:DSH_TARGET_RC=/)
            {
                my $start_offset = index($line, ':DSH_TARGET_RC');
                my $end_offset = index($line, ':', $start_offset + 1);
                my $target_rc_string =
                  substr($line, $start_offset, $end_offset - $start_offset);
                my ($discard, $target_rc) = split '=', $target_rc_string;
                $line =~ s/:DSH_TARGET_RC=$target_rc:\n//g;
                $$target_properties{'target-rc'} = $target_rc;
            }
            if ( $::__DSH_LAST_LINE =~ /DSH_RC/ && $cust_rc_deal) {
                my $target_rc = undef;
                # Get the number in the last line
                $line =~ /[\D]*([0-9]+)\s*$/ ;
                $target_rc = $1;
                $$target_properties{'target-rc'} = $target_rc;
                # Remove the last line
                $line =~ s/$target_rc\s*\n$//g;
                #$line = $line . "## ret=$target_rc";
                # Clean up $::__DSH_LAST_LINE
                undef $::__DSH_LAST_LINE ;
                # when '-z' is specified, display return code
                $::DSH_EXIT_STATUS &&
                    ($line .="Remote_command_rc = $target_rc");
            }

            if ($line ne '')
            {
                if ($line !~ /^$label\n$/)
                {
                    $line = $label . $line;
                }
                $line =~ s/$/\n/ if $line !~ /\n$/;
            }

            $line =~ s/\n/\n$label/g;
            ($line =~ /\n$label$/) && ($line =~ s/\n$label$/\n/);
            chomp $line;

            my @output_files    = ();
            my @output_file_nos = ();

            foreach my $write_fh (@write_fhs)
            {
                my $file_no = fileno($write_fh);
                if (grep /$file_no/, @output_file_nos)
                {
                    $line =~ s/$label//g;
                }

                my $rsp={};
                $rsp->{data}->[0] = $line;
                xCAT::MsgUtils->message("I", $rsp, $::CALLBACK);
                #print $write_fh $line;
            }

            if (@output_files)
            {
                foreach my $output_file (@output_files)
                {
                    pop @write_fhs;
                    close $output_file
                      || print STDOUT
                      "dsh>  Error_file_closed $$target_properties{hostname} $output_file\n";
                    my $rsp={};
                    $rsp->{data}->[0] =
                      "Error_file_closed $$target_properties{hostname $output_file}.\n";
                    xCAT::MsgUtils->message("E", $rsp, $::CALLBACK);
                    ($output_file == $$target_properties{'output-fh'})
                      && delete $$target_properties{'output-fh'};
                    ($output_file == $$target_properties{'output-fh'})
                      && delete $$target_properties{'error-fh'};
                }
            }

            my $rin = '';
            vec($rin, fileno($read_fh), 1) = 1;
            my $fh_count = select($rin, undef, undef, 0);
            last if ($fh_count == 0);
        }
    }

    return $eof_reached;
}

#---------------------------------------------------------------------------

=head3
        pipe_handler_buffer

        Handles and processes dsh output from a given read pipe handle.  The output
        is stored in a buffer supplied by the caller.

        Arguments:
	        $target_properties - property information of the target related to the pipe handle
	        $read_fh - reference to the read pipe handle
	        $buffer_size - local buffer size to read data from the handle
	        $label - prefix label to use for dsh output
	        $write_buffer - buffer of data that is yet to be written (must wait until \n is read)
	        $output_buffer - buffer where output will be written

        Returns:
        	1 if the EOF reached on $read_fh
        	undef otherwise
                
        Globals:
        	None
    
        Error:
        	None
    
        Example:

        Comments:

=cut

#---------------------------------------------------------------------------
# NOTE: global environment $::__DSH_LAST_LINE only can be used in DSHCore::pipe_handler and DSHCore::pipe_handler_buffer

sub pipe_handler_buffer
{
    my ($class, $target_properties, $read_fh, $buffer_size, $label,
        $write_buffer, $output_buffer)
      = @_;

    my $line;
    my $eof_reached = undef;

    my $cust_rc_deal =0;
    if ($::USER_POST_CMD)
    {
        # If user provide post-command to display return code,
        # the keyword 'DSH_RC' will be searched,
        # the return code is gotten in another way as shown like below:
        # ...
        # <output>
        # <return_code>
        # DSH_RC
        #
        # The last two lines are needed to be moved out from output
        $cust_rc_deal = 1;
    }

    while (   (sysread($read_fh, $line, $buffer_size) != 0)
           || ($eof_reached = 1))
    {
        last if ($eof_reached);
        if ($line =~ /^\n$/)
        {

            # need to preserve blank lines in the output.
            $line = $label . $line;
        }

        my @lines = split "\n", $line;

        if (@$write_buffer)
        {
            my $buffered_line = shift @$write_buffer;
            my $next_line     = shift @lines;
            $next_line = $buffered_line . $next_line;
            unshift @lines, $next_line;
        }

        if ($line !~ /\n$/)
        {
            push @$write_buffer, (pop @lines);
        }

        if (@lines)
        {
            if ($cust_rc_deal)
            {
                # Dump the last line at the beginning of current buffer
                if ($::__DSH_LAST_LINE)
                {
                    unshift @lines, "$::__DSH_LAST_LINE" ;
                }
                # Pop current buffer to $::__DSH_LAST_LINE
                $::__DSH_LAST_LINE = $lines[scalar @lines - 1];
                pop @lines;
                # Skip this loop if array @lines is empty.
                if (scalar @lines == 0)
                {
                    next;
                }
            }

            $line = join "\n", @lines;
            $line .= "\n";

            if ($line =~ /:DSH_TARGET_RC=/)
            {
                my $start_offset = index($line, ':DSH_TARGET_RC');
                my $end_offset = index($line, ':', $start_offset + 1);
                my $target_rc_string =
                  substr($line, $start_offset, $end_offset - $start_offset);
                my ($discard, $target_rc) = split '=', $target_rc_string;
                $line =~ s/:DSH_TARGET_RC=$target_rc:\n//g;
                $$target_properties{'target-rc'} = $target_rc;
            }
            if ( $::__DSH_LAST_LINE =~ /DSH_RC/ && $cust_rc_deal) {
                my $target_rc = undef;
                # Get the number in the last line
                $line =~ /[\D]*([0-9]+)\s*$/ ;
                $target_rc = $1;
                $$target_properties{'target-rc'} = $target_rc;
                # Remove the last line
                $line =~ s/$target_rc\s*\n$//g;
                #$line = $line . "## ret=$target_rc";
                # Clean up $::__DSH_LAST_LINE
                undef $::__DSH_LAST_LINE ;
                # when '-z' is specified, display return code
                $::DSH_EXIT_STATUS &&
                    ($line .="Remote_command_rc = $target_rc");
            }

            if ($line ne '')
            {
                if ($line !~ /^$label\n$/)
                {
                    $line = $label . $line;
                }
                $line =~ s/$/\n/ if $line !~ /\n$/;
            }

            $line =~ s/\n/\n$label/g;
            ($line =~ /\n$label$/) && ($line =~ s/\n$label$/\n/);

            push @$output_buffer, $line;

            my $rin = '';
            vec($rin, fileno($read_fh), 1) = 1;
            my $fh_count = select($rin, undef, undef, 0);
            last if ($fh_count == 0);
        }
    }
    return $eof_reached;
}

#---------------------------------------------------------------------------

=head3
        fping_hostnames

        Executes fping on a given list of hostnames and returns a list of those
        hostnames that did not respond

        Arguments:
        	@hostnames - list of hostnames to execute for fping

        Returns:
        	@no_response - list of hostnames that did not respond
        	undef if fping is not installed
                
        Globals:
        	None
    
        Error:
        	None
    
        Example:
        	@bad_hosts = xCAT::DSHCore->fping_hostnames(@host_list);

        Comments:

=cut

#---------------------------------------------------------------------------

sub fping_hostnames
{
    my ($class, @hostnames) = @_;

    my $fping = (-x '/usr/sbin/fping') || undef;
    !$fping && return undef;

    my @output = `/usr/sbin/fping -B 1.0 -r 1 -t 50 -i 10 -p 50 @hostnames`;

    my @no_response = ();
    foreach my $line (@output)
    {
        my ($hostname, $token, $status) = split ' ', $line;
        !(($token eq 'is') && ($status eq 'alive'))
          && (push @no_response, $hostname);
    }

    return @no_response;
}

#---------------------------------------------------------------------------

=head3
        ping_hostnames

        Executes ping on a given list of hostnames and returns a list of those
        hostnames that did not respond

        Arguments:
        	@hostnames - list of hostnames to execute for fping

        Returns:
        	@no_response - list of hostnames that did not respond
        	undef if fping is not installed
                
        Globals:
        	None
    
        Error:
        	None
    
        Example:
        	@bad_hosts = xCAT::DSHCore->ping_hostnames(@host_list);

        Comments:

=cut

#---------------------------------------------------------------------------

sub ping_hostnames
{
    my ($class, @hostnames) = @_;

    my $ping = (($^O eq 'aix') && '/usr/sbin/ping')
      || (($^O eq 'linux') && '/bin/ping')
      || undef;
    !$ping && return undef;

    my @no_response = ();
    foreach my $hostname (@hostnames)
    {
        (system("$ping -c 1 -w 1 $hostname > /dev/null 2>&1") != 0)
          && (push @no_response, $hostname);
    }

    return @no_response;
}

#---------------------------------------------------------------------------

=head3
        resolve_hostnames

        Resolve all related information for a given target, including context
        IP address information and fully qualified hostname.  If the target is
        unresolvable include in a list of unresolvable targets, otherwise store
        all resolved properties for the target.

        Arguments:
        	$options - options hash table describing dsh configuration options
        	$resolved_targets - hash table of resolved properties, keyed by target name
        	$unresolved_targets - hash table of unresolved targets and relevant property information
        	$context_targets - hash table of targets grouped by context name
        	@target_list - input list of target names to resolve

        Returns:
        	None
                
        Globals:
        	None
    
        Error:
        	None
    
        Example:

        Comments:

=cut

#---------------------------------------------------------------------------

sub resolve_hostnames
{
    my ($class, $options, $resolved_targets, $unresolved_targets,
        $context_targets, @target_list)
      = @_;
    my @local_inet;
    scalar(@local_inet) || xCAT::DSHCore->ifconfig_inet;

    foreach my $context_user_target (@target_list)
    {
        my ($context, $user_target) = split ':', $context_user_target;
        if (($context eq 'XCAT') && ($$options{'context'} eq 'DSH'))
        {

            # The XCAT context may not be specified for this node since DSH is the only
            # available context.
            my $rsp={};
            $rsp->{data}->[0] = "DSH is the only available context.\n";
            xCAT::MsgUtils->message("I", $rsp, $::CALLBACK);
            next;
        }
        !$user_target
          && (   ($user_target = $context)
              && ($context = $$options{'context'}));

        my ($user, $target) = split '@', $user_target;
        !$target && (($target = $user) && ($user = undef));

        $$options{'context-all'} && ($context = $$options{'context-all'});

        if (my ($hostname, $aliases, $addrtype, $length, @addrs) =
            gethostbyname($target))
        {
            my $ip_address = inet_ntoa($addrs[0]);
            my $localhost  = (grep { /^$ip_address$/ } @local_inet) || undef;

            if ($hostname eq $ip_address)
            {
                my $packed_ip = inet_aton($ip_address);
                my $hostbyaddr = gethostbyaddr($packed_ip, AF_INET);
                $hostbyaddr && ($hostname = $hostbyaddr);
            }

            my %properties = (
                              'hostname'   => $hostname,
                              'ip-address' => $ip_address,
                              'localhost'  => $localhost,
                              'user'       => $user,
                              'context'    => $context,
                              'unresolved' => $target
                              );

            $user && ($user .= '@');
            $$resolved_targets{"$user$hostname"} = \%properties;

            if ($context_targets)
            {
                if (!$$context_targets{$context})
                {
                    my %context_target_list = ();
                    $$context_targets{$context} = \%context_target_list;
                }

                $$context_targets{$context}{"$user$hostname"}++;
            }
        }

        else
        {
            my %properties = (
                              'hostname'   => $target,
                              'user'       => $user,
                              'context'    => $context,
                              'unresolved' => $target
                              );
            $$unresolved_targets{"$user_target"} = \%properties;
        }
    }

    xCAT::DSHCore->removeExclude($resolved_targets, $unresolved_targets,
                           $context_targets);
}

#---------------------------------------------------------------------------

=head3
        pping_hostnames

        Executes pping on a given list of hostnames and returns a list of those
        hostnames that did not respond

        Arguments:
                @hostnames - list of hostnames to execute for fping

	        Returns:
                @no_response - list of hostnames that did not respond

        Globals:
                None

        Error:
                None

        Example:
                @bad_hosts = xCAT::DSHCore->pping_hostnames(@host_list);

        Comments:

=cut

#---------------------------------------------------------------------------

sub pping_hostnames
{
    my ($class, @hostnames) = @_;

    my $hostname_list = join ",", @hostnames;
    my @output =
      xCAT::Utils->runcmd("pping $hostname_list", -1);
     $::RUNCMD_RC =0; # reset
    my @no_response = ();
    foreach my $line (@output)
    {
        my ($hostname, $result) = split ':', $line;
        my ($token,    $status) = split ' ', $result;
        chomp($token);
       if ($token ne 'ping') {
          push @no_response, $hostname;
       }
    }

    return @no_response;
}

#---------------------------------------------------------------------------

sub removeExclude
{
    shift;
    my ($resolved_targets, $unresolved_targets, $context_targets) = @_;
    return if (!$resolved_targets || !$unresolved_targets);

    my @invalid_resolved_targets;
    my @invalid_unresolved_targets;

    %::__EXCLUDED_TARGETS;

    for my $unrsvl_tg (keys %$unresolved_targets)
    {
        if ($unresolved_targets->{$unrsvl_tg}->{'hostname'} =~ /^-/)
        {
            $::__EXCLUDED_TARGETS{$unrsvl_tg} =
              $unresolved_targets->{$unrsvl_tg};
            delete $unresolved_targets->{$unrsvl_tg};
        }
    }

    for my $excluded_tg (keys %::__EXCLUDED_TARGETS)
    {
        for my $rslv_tg (keys %$resolved_targets)
        {
            if (  $::__EXCLUDED_TARGETS{$excluded_tg}->{'hostname'} eq '-'
                . $resolved_targets->{$rslv_tg}->{'hostname'}
                || $::__EXCLUDED_TARGETS{$excluded_tg}->{'hostname'} eq '-'
                . $resolved_targets->{$rslv_tg}->{'ip-address'}
                || $::__EXCLUDED_TARGETS{$excluded_tg}->{'hostname'} eq '-'
                . $resolved_targets->{$rslv_tg}->{'unresolved'}
                || $::__EXCLUDED_TARGETS{$excluded_tg}->{'unresolved'} eq '-'
                . $resolved_targets->{$rslv_tg}->{'hostname'}
                || $::__EXCLUDED_TARGETS{$excluded_tg}->{'unresolved'} eq '-'
                . $resolved_targets->{$rslv_tg}->{'ip-address'}
                || $::__EXCLUDED_TARGETS{$excluded_tg}->{'unresolved'} eq '-'
                . $resolved_targets->{$rslv_tg}->{'unresolved'})
            {
                push @invalid_resolved_targets, $rslv_tg;
            }
        }
    }

    for my $invalid_res (@invalid_resolved_targets)
    {
        my $context = $resolved_targets->{$invalid_res}->{'context'};
        delete $context_targets->{$context}->{$invalid_res};
        if (!scalar(keys %{$context_targets->{$context}}))
        {
            delete $context_targets->{$context};
        }
        delete $resolved_targets->{$invalid_res};
    }

}
1;