#!/usr/bin/perl
#
# Copyright (C) 2014 Frank Brendel <frank.brendel@eurolog.com>, EURO-LOG AG 
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

use Getopt::Std;
use IPC::Open3;
use FindBin qw($Script);

END {
  defined fileno STDOUT or return;
  close STDOUT and return;
  warn "$0: failed to close standard output: $!\n";
  $? ||= 1;
}

$VERSION = "1.0.0";

$| = 1;

my $VM; # hash reference with VM properties from PVE
my %Opts;

# define defaults
my $PveNode;
my $Action = "reboot";
my $VmId;
my $PveUser = 'root';
my $PveSh = "/usr/bin/pvesh";
my $Delay = 0;
my $Quiet = 0;

if (@ARGV > 0) # read command line parameter
{
   if(!getopts("p:o:m:u:e:hqVf:", \%Opts))
   {
      exitError(@_);
   }
   if(defined($Opts{h})){ HELP_MESSAGE();}
   if(defined($Opts{V})){ printVersion();}
   
   $PveNode = defined($Opts{p}) ? $Opts{p} : undef;
   $Action  = defined($Opts{o}) ? $Opts{o} : $Action;
   $VmId    = defined($Opts{m}) ? $Opts{m} : undef;
   $PveUser = defined($Opts{u}) ? $Opts{u} : $PveUser;
   $PveSh   = defined($Opts{e}) ? $Opts{e} : $PveSh;
   $Delay   = defined($Opts{f}) ? $Opts{f} : $Delay;
   $Quiet   = defined($Opts{q}) ? $Opts{q} : $Quiet;
     
}
else # read parameter from stdin
{
   while(<>)
   {
      chomp;
      # strip leading and trailing whitespace
      s/^\s*//;
      s/\s*$//;
      # skip comments
      next if /^#/;
      # syntax check
      if(!/^[^=]+=[^=]+$/){ exitError("syntax: name=value", 2);}
      my ($Name, $Value) = split /\s*=\s*/;
      for ($Name)
      {
         /^pvenode$/   && do { $PveNode = $Value; last;};
         /^action$/    && do { $Action = $Value; last;};
         /^port$/      && do { $VmId = $Value; last;};
         /^user$/      && do { $PveUser = $Value; last;};
         /^pvesh$/     && do { $PveSh = $Value; last;};
         /^delay$/     && do { $Delay = $Value; last;};
         /^help$/      && do { HELP_MESSAGE();};
         /^version$/   && do { printVersion();};
         /^quiet$/     && do { $Quiet = $Value; last;};
      }
   }
}

if($Action eq "metadata"){ printMetadata(); exit 0;}
if(!defined($PveNode)){ exitError("pvenode missing");}
if(!defined($VmId) && $Action ne "monitor"){ exitError("vmid missing");}

for ($Action)
{
   /^monitor$/  && do{ pveMonitor(); exit 0;};
   /^status$/   && do{ vmStatus(); exit 0;};
   /^off$/      && do{ vmStop();   exit 0;};
   /^on$/       && do{ vmStart();  exit 0;};
   /^reboot$/   && do{ vmStop(); vmStart(); exit 0;};
   die "unknown action: $Action";
}

# return the Status of a virtual machine in $_.
# Return 0 on success or non-zero on error
sub vmStatus
{
   my $ErrNo;
   my $ErrStr;
   my $Nodes;
   my $Node = shift;
   
   if(!defined($Node)) # search for the VM   
   {
      my $Cmd = "ssh -l $PveUser $PveNode $PveSh get /nodes";
      pveCmd(\$Nodes, \$Cmd, \$ErrNo, \$ErrStr);
      if( $ErrNo != 0 )
      {
         exitError($Cmd . "\n" . $ErrStr, $ErrNo);
      }
      my $_Quiet = $Quiet;
      $Quiet = 1;
      foreach (@$Nodes)
      {
         $Node = $_->{node};
         
         if(vmStatus($Node) == 0){ last;}
      }
      $Quiet = $_Quiet;
   }
   else # don't search for the VM; we know the PVE node
   {
      my $Cmd = "ssh -l $PveUser $PveNode $PveSh get /nodes/$Node/qemu/$VmId/status/current";
      $ErrNo = undef;
      $ErrStr = undef;
      pveCmd(\$VM, \$Cmd, \$ErrNo, \$ErrStr);
      if( $ErrNo != 0 )
      {
         if( $ErrStr !~ /no such VM \('$VmId'\)/ )
         {
            exitError($Cmd . "\n" . $ErrStr, $ErrNo);
         }
      }
   }
   
   if(defined($VM->{status}))
   {
      $_ = $VM->{status};
      # memorize the PVE node
      $VM->{node} = $Node;
   }
   else
   {
      $_ = "not found";
   }
   if(!$Quiet){ print "vm $VmId is $_" . (defined($VM) && $VM->{node} ? " on pvenode $VM->{node}" : "") . "\n";}
   return $ErrNo;
}

# start a virtual machine
# Return 0  if the status is "running" or non-zero on error.
sub vmStart
{
   my $_Quiet = $Quiet;
   $Quiet = 1;
   # check if it is already running
   vmStatus();
   if($_ eq "running")
   {
      $Quiet = $_Quiet;
      if(!$Quiet){ print "warning: vm $VmId is already $_ on $VM->{node}\n"};
      return 0;
   }
   if($_ eq "not found")
   {
      exitError("vm $VmId not found");
   }
   
   my $Cmd = "ssh -l $PveUser $PveNode $PveSh create /nodes/$VM->{node}/qemu/$VmId/status/start --skiplock=1";
   my $ErrNo;
   my $ErrStr;
   pveCmd(undef, \$Cmd, \$ErrNo, \$ErrStr);
   if( $ErrNo != 0 )
   {
      $_ = $ErrStr;
      return $ErrNo;
   }
   
   # check if it is running
   for (my $i=0; $i<30; $i++)
   {
      vmStatus($VM->{node});
      $ErrStr = $_;
      if($ErrStr eq "running")
      {
         $Quiet = $_Quiet;
         if(!$Quiet){ print "success: $VmId has been turned on\n"};
         $_ = $ErrStr;
         return 0;
      }
      sleep 1;
   }
   exitError($ErrStr);
}

# stop a virtual machine
# Return 0  if the status is "stopped" or non-zero on error.
sub vmStop
{
   my $_Quiet = $Quiet;
   $Quiet = 1;
   # check if it is already stopped
   vmStatus();
   if($_ eq "stopped")
   {
      $Quiet = $_Quiet;
      if(!$Quiet){ print "warning: vm $VmId is already $_ on $VM->{node}\n"};
      return 0;
   }
   if($_ eq "not found")
   {
      exitError("vm $VmId not found");
   }
   
   my $Cmd = "ssh -l $PveUser $PveNode $PveSh create /nodes/$VM->{node}/qemu/$VmId/status/stop --skiplock=1";
   my $ErrNo;
   my $ErrStr;
   pveCmd(undef, \$Cmd, \$ErrNo, \$ErrStr);
   if( $ErrNo != 0 )
   {
      $_ = $ErrStr;
      return $ErrNo;
   }
   
   # check if it is stopped
   for (my $i=0; $i<30; $i++)
   {
      vmStatus($VM->{node});
      $ErrStr = $_;
      if( $ErrStr eq "stopped")
      {
         $Quiet = $_Quiet;
         if(!$Quiet){ print "success: $VmId has been stopped\n"};
         $_ = $ErrStr;
         return 0;
      }
      sleep 1;
   }
   exitError($ErrStr);
}

# check if the PVE node is accessible
sub pveMonitor
{
   my $Cmd = "ssh -l $PveUser $PveNode $PveSh get /version";
   my $PveVersion;
   my $ErrNo;
   my $ErrStr;
   pveCmd(\$PveVersion, \$Cmd, \$ErrNo, \$ErrStr);
   if( $ErrNo != 0 )
   {
      exitError($ErrStr, $ErrNo);
   }
   if(!$Quiet){ print "Proxmox version: $PveVersion->{version}-$PveVersion->{release}/$PveVersion->{repoid}\n";}
   return 0;
}

# send command to pve; write pvesh output into $Var
sub pveCmd
{
   my $Var = shift;
   my $Cmd = shift;
   my $ErrNo = shift;
   my $ErrStr = shift;
      
   my $Pid = open3 (\*WTR, \*RDR,\*ERR, $$Cmd)
   or die "error open3(): $!";
   
   my $_Var;
   if(ref $Var)
   {
      local $/ = undef;
      $_Var = <RDR>;
   }
   my $_ErrStr;
   {
      local $/ = undef;
      $_ErrStr = <ERR>;
   }
   close WTR;
   close RDR;
   close ERR;
   waitpid($Pid, 0);
   $$ErrNo = $? >> 8;
   $$ErrStr = $_ErrStr;
   # simple conversation from JSON to Perl
   # replacing the ':' with '=>' to get a Perl hash ref should be sufficient
   if(defined($_Var))
   {
      $_Var =~ s/\s:\s/ => /g;
      $_Var = '$$Var = ' . $_Var . ";";
      eval($_Var);
   }
}

sub HELP_MESSAGE 
{
   print "Usage:\n";
   print "\n";
   print "$Script [options]\n";
   print "\n";
   print "Options:\n";
   print "  -p <string>      PVE node\n";
   print "  -o <string>      Action: reboot (default), off, on or status\n";
   print "  -m <string>      virtual machine id\n";
   print "  -u <string>      username (default=root)\n";
   print "  -e <string>      path to pvesh (default=/usr/bin/pvesh)\n";
   print "  -f <seconds>     Wait X seconds before fencing is started\n";
   print "  -h               help\n";
   print "  -q               quiet mode\n";
   print "  -V               version\n";
   
   exit 0;
}

sub printVersion
{
   print "$Script version $VERSION\n";
   exit 0;
}

sub printMetadata
{
print '<?xml version="1.0" ?>
<resource-agent name="fence_pve" shortdesc="Fencing agent for the Proxmox Virtual Environment" >
   <longdesc>
      This agent can be used to fence virtual machines acting as nodes in a virtualized cluster.
      It does this by connecting to the Proxmox node per ssh and executes pvesh.
      Set &lt;pcmk_host_map&gt; to map the nodename to the corresponding virtual machine id.
      fence_pve requires passwordless login by using authentication keys.
      Refer to ssh(1), ssh-keygen(1) and ssh-copy-id(1) for more information on setting up ssh keys.
   </longdesc>
   <vendor-url>http://www.clusterlabs.org</vendor-url>
   <parameters>
      <parameter name="action" unique="0" required="1">
         <getopt mixed="-o &lt;action&gt;" />
         <content type="string" default="reboot" />
         <shortdesc lang="en">Fencing Action</shortdesc>
      </parameter>
      <parameter name="pvenode" unique="0" required="1">
         <getopt mixed="-p &lt;pvenode&gt;" />
         <content type="string"  />
         <shortdesc lang="en">The PVE node to ssh to.</shortdesc>
      </parameter>
      <parameter name="port" unique="0" required="1">
         <getopt mixed="-m &lt;vmid&gt;" />
         <content type="string"  />
         <shortdesc lang="en">The virtual machine id to operate on.</shortdesc>
      </parameter>
      <parameter name="user" unique="0" required="0">
         <getopt mixed="-u &lt;name&gt;" />
         <content type="string" default="root" />
         <shortdesc lang="en">The ssh login name.</shortdesc>
      </parameter>
      <parameter name="pvesh" unique="0" required="0">
         <getopt mixed="-e &lt;pvesh&gt;" />
         <content type="string" default="/usr/bin/pvesh"/>
         <shortdesc lang="en">Path to pvesh.</shortdesc>
      </parameter>
      <parameter name="delay" unique="0" required="0">
         <getopt mixed="-f &lt;seconds&gt;" />
         <content type="string" default="0"/>
         <shortdesc lang="en">Wait X seconds before fencing is started</shortdesc>
      </parameter>
      <parameter name="help" unique="0" required="0">
         <getopt mixed="-h" />           
         <content type="string"  />
         <shortdesc lang="en">Display help and exit</shortdesc>                    
      </parameter>
      <parameter name="version" unique="0" required="0">
         <getopt mixed="-V" />           
         <content type="string"  />
         <shortdesc lang="en">Print version and exit</shortdesc>                    
      </parameter>
      <parameter name="quiet" unique="0" required="0">
         <getopt mixed="-q" />           
         <content type="string" default="0"/>
         <shortdesc lang="en">Quiet mode</shortdesc>                    
      </parameter>
   </parameters>
   <actions>
      <action name="on" />
      <action name="off" />
      <action name="reboot" />
      <action name="status" />
      <action name="metadata" />
   </actions>
</resource-agent>
';
}

sub exitError
{
   my $ErrStr = shift;
   my $ErrNo = shift || 1;
   print STDERR (defined($ErrStr) ? "ERROR: " . $ErrStr . "\n" : "");
   print STDERR "Please use '-h' for usage.\n";
   exit $ErrNo;
}
