#!/usr/bin/perl

###############################################################################
###############################################################################
##
##  Copyright (C) Sistina Software, Inc.  1997-2003  All rights reserved.
##  Copyright (C) 2004 Red Hat, Inc.  All rights reserved.
##  
##  This copyrighted material is made available to anyone wishing to use,
##  modify, copy, or redistribute it subject to the terms and conditions
##  of the GNU General Public License v.2.
##
###############################################################################
###############################################################################

use Getopt::Std;
use Net::Telnet ();

# WARNING!! Do not add code bewteen "#BEGIN_VERSION_GENERATION" and 
# "#END_VERSION_GENERATION"  It is generated by the Makefile

#BEGIN_VERSION_GENERATION
$FENCE_RELEASE_NAME="DEVEL.1092766954";
$REDHAT_COPYRIGHT=("Copyright (C) Red Hat, Inc.  2004  All rights reserved.");
$BUILD_DATE="(built Tue Aug 17 18:22:34 UTC 2004)";
#END_VERSION_GENERATION

# Get the program name from $0 and strip directory names
$_=$0;
s/.*\///;
my $pname = $_;

# Change these if the text returned by your equipment is different.
# Test by running script with options -t -v and checking /tmp/apclog

my $immediate = 'immediate'; # Or 'delayed' - action string prefix on menu
my $masterswitch = 'masterswitch plus '; # 'Device Manager' option to choose
my $login_prompt = '/: /';
my $cmd_prompt = '/> /';

my $max_open_tries = 3;      # How many telnet attempts to make.  Because the 
                             # APC can fail repeated login attempts, this number
                             # should be more than 1
my $open_wait = 5;           # Seconds to wait between each telnet attempt
my $telnet_timeout = 20;     # Seconds to wait for matching telent response

$opt_o = 'reboot';        # Default fence action.  
$switchnum = 1;           # Default switch

sub usage 
{
    print "Usage:\n";
    print "\n";
    print "$pname [options]\n";
    print "\n";
    print "Options:\n";
    print "  -a <ip>          IP address or hostname of MasterSwitch\n";
    print "  -h               usage\n";
    print "  -l <name>        Login name\n";
    print "  -n <num>         Outlet number to change: [<switch>:]<outlet> \n";
    print "  -o <string>      Action: Reboot (default), Off or On\n";
    print "  -p <string>      Login password\n";
    print "  -q               quiet mode\n";
    print "  -T               Test mode (cancels action)\n";
    print "  -V               version\n";
    print "  -v               Log to file /tmp/apclog\n";

    exit 0;
}

sub fail
{
  ($msg)=@_;
  print $msg."\n" unless defined $opt_q;
  $t->close if defined $t;
  exit 1;
}

sub fail_usage
{
  ($msg)=@_;
  print STDERR $msg."\n" if $msg;
  print STDERR "Please use '-h' for usage.\n";
  exit 1;
}

sub version
{
  print "$pname $FENCE_RELEASE_NAME $BUILD_DATE\n";
  print "$REDHAT_COPYRIGHT\n" if ( $REDHAT_COPYRIGHT );

  exit 0;
}

if (@ARGV > 0) {
   getopts("a:hl:n:o:p:qTvV") || fail_usage ;

   usage if defined $opt_h;
   version if defined $opt_V;

   fail_usage "Unkown parameter." if (@ARGV > 0);

   fail_usage "No '-a' flag specified." unless defined $opt_a;
   fail_usage "No '-n' flag specified." unless defined $opt_n;
   fail_usage "No '-l' flag specified." unless defined $opt_l;
   fail_usage "No '-p' flag specified." unless defined $opt_p;
   fail_usage "Unrecognised action '$opt_o' for '-o' flag"
      unless $opt_o =~ /^(Off|On|Reboot)$/i;

   if ( $opt_n =~ /(\d+):(\d+)/ ) {
      $switchnum=($1);
      $opt_n = ($2);
   }

} else {
   get_options_stdin();

   fail "failed: no IP address" unless defined $opt_a;
   fail "failed: no plug number" unless defined $opt_n;
   fail "failed: no login name" unless defined $opt_l;
   fail "failed: no password" unless defined $opt_p;
   fail "failed: unrecognised action: $opt_o"
      unless $opt_o =~ /^(Off|On|Reboot)$/i;

} 

$masterswitch = $masterswitch . $switchnum;

my $t = new Net::Telnet;
$t->timeout($telnet_timeout);

$t->input_log('/tmp/apclog')
  if $opt_v;

&login;
&navigate;
&action;

exit 0;


sub login
{
  # Opt to return on failure and we'll retry
  $t->errmode('return');  

  for (my $i=0; $i<$max_open_tries; $i++)
  {
    $t->open($opt_a);
    ($_) = $t->waitfor($login_prompt);
  
    # Expect 'User Name : ' 
    if (! /name/i) {
      $t->close;
      sleep($open_wait);
      next;        
    }

    $t->print($opt_l);
    ($_) = $t->waitfor($login_prompt);
  
    # Expect 'Password  : ' 
    if (! /password/i ) {
      $t->close;
      sleep($open_wait);
      next;         
    }
  
    # Send password
    $t->print($opt_p);  
    # No point retrying if we get this far e.g. if password is rejected
    return;
  }
  fail "failed: telnet failed: ". $t->errmsg."\n" 
}


# Navigate through menus to the appropriate outlet control menu.
# Uses multi-line (mostly) case-insensitive matches to recognise menus
# and works out what option number to select from each menu.

sub navigate
{
  # Abort on failure beyond here
  $t->errmode(\&telnet_error);  

  while(1)
  {
    # Get the new text from the menu
    ($_) = $t->waitfor($cmd_prompt);

    # Identify next option 
    if ( 
          # "Control Console", "1- Device Manager"
          /--\s*control console.*(\d+)\s*-\s*device manager/is  ||

          # "Device Manager", "1- MasterSwitch plus 1"
          /--\s*device manager.*(\d+)\s*-\s*$masterswitch/is ||

          # "Device Manager", "1- Cluster Node 0   ON"
          /--\s*(?:device manager|$masterswitch).*($opt_n)\s*-[^\n]*\s(?-i:ON|OFF)\*?\s/ism ||

          # "MasterSwitch plus 1", "1- Outlet 1:1  Outlet #1  ON"
          /--\s*$masterswitch.*(\d+)\s*-\s*Outlet\s+$opt_n\s[^\n]*\s(?-i:ON|OFF)\*?\s/ism ||

          # Outlet Control
          /(\d+)\s*-\s*((Control|Outlet)\s?){2}\s*(\d+:)?$opt_n\s/i 
       ) {
      $t->print($1);
      next;
    }

    # "Outlet Control X:N", "4- Immediate Reboot"
    if ( /(\d+)\s*-\s*$immediate $opt_o/i ) {
      $t->print($1);
      last;
    }

    #> $t->close;
    fail "failed: unrecognised menu response\n";
  }
}


sub logout 
{
    while(1)
    {
	# Get the new text from the menu
	($_) = $t->waitfor($cmd_prompt);

	if ( 
	     # "Control Console", "4- Logout"	 
	     /--\s*control console.*(\d+)\s*-\s*Logout/is
	     ) {
	    $t->print($1);
	    last;
	}
	else {
	    # The APC menu uses "<esc>" to go 'up' menues.  We must set
            # the output_record_separator to "" so that "\n" is not printed
            # after the "<esc>" character
	    
	    # save ors
	    $ors=$t->output_record_separator;
	    
	    # unset ors
	    $t->output_record_separator("");
	    
	    # send escape
	    $t->print("\x1b");
	    
	    # restore ors
	    $t->output_record_separator("$ors");
	    
	    next;
	}
    }
}


sub action
{
  # "Enter 'YES' to continue or <ENTER> to cancel : "
  ($_) = $t->waitfor('/: /');
  if (! /$immediate $opt_o.*outlet $opt_n\s.*YES.*to continue/si ) {
    fail "failed: unrecognised $opt_o response\n";
    #> $t->close;
  }

  # Test mode?
  $t->print($opt_T?'NO':'YES');

  # "Success", "Press <ENTER> to continue..." 
  ($_) = $t->waitfor('/continue/');
  $t->print('');

  if (defined $opt_T) {
    logout(); 
    print "success: test outlet $opt_n $opt_o\n" unless defined $opt_q; 
    $t->close;

    # Allow the APC some time to clean connection
    # before next login.
    sleep 1;

    exit 0;
  } elsif ( /Success/i ) {
    logout();
    print "success: outlet $opt_n $opt_o\n" unless defined $opt_q; 
    $t->close;

    # Allow the APC some time to clean connection
    # before next login.
    sleep 1;

    exit 0;
  } 

  fail "failed: unrecognised action response\n";
}


sub get_options_stdin
{
    my $opt;
    my $line = 0;
    while( defined($in = <>) )
    {
	$_ = $in;
        chomp;

	# strip leading and trailing whitespace
        s/^\s*//;
        s/\s*$//;

	# skip comments
	next if /^#/;

        $line+=1;
        $opt=$_;
	next unless $opt;

	($name,$val)=split /\s*=\s*/, $opt;

	if ( $name eq "" )
	{
	   print STDERR "parse error: illegal name in option $line\n";
	   exit 2;
	} 

        # DO NOTHING -- this field is used by fenced 
	elsif ($name eq "agent" ) { } 

	# FIXME -- depricated.  use "port" and "switch" instead.
	elsif ($name eq "fm" ) 
	{
	    my ($dummy1,$dummy2) = split /\s+/,$val;
            print STDERR "Depricated \"fm\" entry.  Refer to the man page for fence_apc.\n";
            if ( $dummy2 =~ /(\d+):(\d+)/ ) {
                $switchnum = $1;
                $opt_n = $2;
            }
	} 

	elsif ($name eq "ipaddr" ) 
	{
	    $opt_a = $val;
	} 
	elsif ($name eq "login" ) 
	{
	    $opt_l = $val;
        } 

	# FIXME -- depreicated residue of old fencing system
	elsif ($name eq "name" ) { } 

	elsif ($name eq "option" ) 
	{
	    $opt_o = $val;
	} 
	elsif ($name eq "passwd" ) 
	{
	    $opt_p = $val;
	} 
	elsif ($name eq "port" ) 
	{
	    $opt_n = $val;
	} 
	elsif ($name eq "switch" ) 
	{
	    $switchnum = $val;
	} 
	elsif ($name eq "test" ) 
	{
	    $opt_T = $val;
	} 
	elsif ($name eq "verbose" ) 
	{
	    $opt_v = $val;
	} 

	# FIXME should we do more error checking?  
	# Excess name/vals will be eaten for now
	else 
	{
	   fail "parse error: unknown option \"$opt\"";
	}
    }
}


sub telnet_error
{
  fail "failed: telnet returned: ".$t->errmsg."\n";
}


