# IBM(c) 2008 EPL license http://www.eclipse.org/legal/epl-v10.html
# Ver. 2.1 (4) - sf@mauricebrinkmann.de
# Ver. 3.0     - Herbert Mehlhose, IBM - add support of VirtualBox V4 				
#
#-------------------------------------------------------

=head1
  xCAT plugin package to handle VirtualBox machines

   Supported command:
		 rpower

=cut

#-------------------------------------------------------
package xCAT_plugin::vbox;
require Sys::Hostname;
require xCAT::Table;

require xCAT::Utils;
require xCAT::TableUtils;
require xCAT::ServiceNodeUtils;
require xCAT::MsgUtils;
use Getopt::Long;
1;

use strict;
use warnings;

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

=head3	handled_commands

Return list of commands handled by this plugin

=cut

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

sub handled_commands
{
	return {rpower => 'nodehm:power,mgt'};
}

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

=head3	preprocess_request

  Check and setup for hierarchy 

=cut

#-------------------------------------------------------
sub preprocess_request
{
	my $req = shift;
	my $cb	= shift;
	if ($req->{_xcatpreprocessed}->[0] == 1) { return [$req]; }
	#exit if preprocessed
	my $nodes	 = $req->{node};
	my $service  = "xcat";

	# find service nodes for requested nodes
	# build an individual request for each service node
	my $sn = xCAT::ServiceNodeUtils->get_ServiceNode($nodes, $service, "MN");

	my @requests = ();
	
	# build each request for each service node
	foreach my $snkey (keys %$sn) {
	my $n=$sn->{$snkey};
	print "snkey=$snkey, nodes=@$n\n";
			my $reqcopy = {%$req};
			$reqcopy->{node} = $sn->{$snkey};
			$reqcopy->{'_xcatdest'} = $snkey;
			$reqcopy->{_xcatpreprocessed}->[0] = 1;
			push @requests, $reqcopy;

	}
	return \@requests;
}


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

=head3	process_request

  Process the command

=cut

#-------------------------------------------------------
sub process_request
{

	my $request  = shift;
	our $callback = shift;
	my $nodes	 = $request->{node};
	my $command  = $request->{command}->[0];
	my $args	 = $request->{arg};
	my $envs	 = $request->{env};
	our $rsp;
	my $node;

	my $soapsupport = eval { require SOAP::Lite; };
	unless ($soapsupport) { #Still no SOAP::Lite module
	  $callback->({error=>"SOAP::Lite perl module missing, unable to fulfill Virtual Box plugin requirements",errorcode=>[42]});
	  return [];
	}
require xCAT::vboxService;
   
	my @nodes=@$nodes;

	PROCESSNODES: foreach $node (@nodes) ######################################
	{
		# Load node information from xCAT tables
		my ($error, $url, $username, $password, $vmname, $vncport) = getInfo($node);
		next PROCESSNODES if ($error == 1);
		last PROCESSNODES if ($error == 2);
		
		# Create connection to VirtualBox web service
		my $api;  # we assume V4 as default API
		my $vbox = getWebService($url, $username, $password, $node);
		next PROCESSNODES if (!$vbox);
		
		# get version
		my $vboxvers = xCAT::vboxService->IVirtualBox_getVersion($url, $vbox);
		#$callback->({info=>"$node: Vbox version: '$vboxvers', requested vncport via xCAT 'vm' table: '$vncport'"});
		if ($vboxvers =~ "^3.0" and $vncport =~ ",")
		{
			addError("$node: VirtualBox version 3.0 does only support a single RDP port, please correct entry vncport='$vncport' in table 'vm' for this node.");
			next PROCESSNODES;
		}
		if($vboxvers =~ "^3.") {
			$api="3";    # There is no getAPIVersion in Version 3, set to 3
		} elsif($vboxvers =~ "^4.0") {
			$api="4_0";  # There is no getAPIVersion in Version 4.0 set to 4_0
		} else {		
			$api = xCAT::vboxService->IVirtualBox_getAPIVersion($url, $vbox);
		}	
		# Get the machine
		my $machine = getMachine($api, $url, $vbox, $vmname, $node);
		if ($machine) {
						
			# Command differentiation #------------------------------------
			
			if ($command eq "rpower" and @$args > 0) {
				powerCtrl($vboxvers, $api, $url, $vbox, $machine, $vmname, $node, $vncport, $args);
			}
			
			#else # unrecognized command
			#{
			#	 addError("$node: $command unsupported on vm");
			#}
			
			# End Command differentiation #--------------------------------

		} # /existing $machine reference
		
		xCAT::vboxService->IWebsessionManager_logoff($url, $vbox);
		
	} # /for each $node in @nodes
	continue
	{
		# print out status before waiting for others and others and ..
		printMessages();
		
		# yield???
	}
	
	# last chance to print remaining messages
	printMessages();
	
	return;
	
	
	sub addMessage ############################################################
	{
		push @{$rsp->{data}}, shift;
		
	} # /addMessage
	
	sub addError ##############################################################
	{
		push @{$rsp->{error}}, shift;
		
	} # /addError
	
	
	sub printMessages #########################################################
	{
		if (defined($rsp->{data}) or defined($rsp->{error})) {
			if (defined($rsp->{error}))	{
				push @{$rsp->{errorcode}}, 1;
				xCAT::MsgUtils->message("E", $rsp, $callback, 0);
			} else {
				xCAT::MsgUtils->message("I", $rsp, $callback, 0);
			}
		}
		
	} # /printMessages
	
	
	sub powerCtrl #############################################################
	{
		my ($vboxvers, $api, $url, $vbox, $machine, $vmname, $node, $vncport, $args) =
			(shift, shift, shift, shift, shift, shift, shift, shift, shift);
		
		# Get state of the machine first
		my $cmd = "IMachine_getState";
		if($api eq "3") {
			$cmd=$cmd . "_V3";
		}
		my $vmstate = xCAT::vboxService->$cmd($url, $machine);
		
		# Check if there is anything to do
		if (!$vmstate) {
			addError("$node: Can not determine vm's state!");
			return;
		} elsif ($vmstate =~ m/^PoweredOff|^Aborted|^Saved/) { 
			if ($args->[0] eq "off") {
				addMessage("$node: off");
				return;
			} elsif ($args->[0] =~ m/^reset|^boot/) {
				# if machine is off: turn on when reset was choosen
				$args->[0] = "on reset";
			} elsif ($args->[0] =~ m/^stat/) {
				# addMessage("$node: off ($vmstate)");
				addMessage("$node: off");
				return;
			}
		} else {
			if ($args->[0] eq "on") {
				addMessage("$node: on");
				return;
			} elsif ($args->[0] =~ m/^stat/) {
				# addMessage("$node: on ($vmstate)");
				addMessage("$node: on");
				return;
			}
		}
		
		# Determine machine's UUID - Should work since we got the state
		$cmd = "IMachine_getId";
		if($api eq "3") {$cmd=$cmd . "_V3";}
		my $uuid = xCAT::vboxService->$cmd($url, $machine);

		# Open Session
		my $session = getSession($api, $url, $vbox, $node, $vmname);
		if ($session) {
			# Decide what to do
			if ($args->[0] =~ m/^on/) {
				if($api eq "3")	{
					powerOn_V3($vboxvers, $url, $vbox, $machine, $session, $uuid, $node, $vncport, $args);
				} else {
					powerOn($url, $vbox, $machine, $session, $uuid, $node, $vncport, $args);
				}
			} else {
				if($api eq "3") {
					powerOff_V3($url, $vbox, $session, $machine, $uuid, $node, $args);
				} else {
					powerOff($url, $vbox, $session, $machine, $uuid, $node, $args);
				}
			}
			
			xCAT::vboxService->ISession_unlockMachine($url,$session);
		} # /existing $session
		
		return;
		
	
		sub powerOn #**********************************************************
		{
			my	($url, $vbox, $machine, $session, $uuid, $node, $vncport, $args) =
				(shift, shift, shift, shift, shift, shift, shift, shift);
				
			my $locktype="Write";
			xCAT::vboxService->IMachine_lockMachine($url,$machine,$session,$locktype);
					
			my $directmachine = xCAT::vboxService->ISession_getMachine($url, $session);
			if (!$directmachine) {
				addError("$node: Could not access mutable machine");
			} else {
				my $vrde = xCAT::vboxService->IMachine_getVRDEServer($url, $directmachine);

				# 'Oracle VM VirtualBox Extension Pack' needs to be installed on vbox server
				# to be able to use RDP
				my $extpack;
				my @VRDEextpack = xCAT::vboxService->IVRDEServer_getVRDEExtPack($url, $vrde);
				foreach(@VRDEextpack) {
					if($_ eq "Oracle VM VirtualBox Extension Pack") {
						$extpack=1;
					}
				}

				if($extpack) {
					my $hasVRDE = xCAT::vboxService->IVRDEServer_getEnabled($url, $vrde);

					my $prop="TCP/Ports";
					my $portno = xCAT::vboxService->IVRDEServer_getVRDEProperty($url, $vrde, $prop);

					if ($vncport) {	# vncport determines, if rdp will be enabled
						my $change;
						# If port is specified but different: Update machine settings
						if($hasVRDE eq "false") {
							$hasVRDE="true";
							xCAT::vboxService->IVRDEServer_setEnabled($url, $vrde, $hasVRDE);
							$change=1;
						}
						if($vncport ne $portno) {
							xCAT::vboxService->IVRDEServer_setVRDEProperty($url, $vrde, $prop, $vncport);
							$change=1;
						}
						if($change) {
							xCAT::vboxService->IMachine_saveSettings($url, $directmachine);
						}
					} elsif (!$vncport and $hasVRDE eq "true") {
						# If not specified: disable RDP for the guest
						$hasVRDE="false";
						xCAT::vboxService->IVRDEServer_setEnabled($url, $vrde, $hasVRDE);
						xCAT::vboxService->IMachine_saveSettings($url, $directmachine);
					}
					#xCAT::vboxService->IMachine_saveSettings($url, $directmachine);
					xCAT::vboxService->IManagedObjectRef_release($url, $directmachine);
				} else {
					if($vncport) {
						addMessage("Warning: for VirtualBox Version 4 and above, RDP needs the VRDE Extension Pack to be installed on the VirtualBox host system. Either install this package to use RDP with the guest machine or remove the 'vncport' from xCAT vm table for '$node' to avoid this warning message to appear. Starting without RDP for now.");
					}
				}
			}
			
			xCAT::vboxService->ISession_unlockMachine($url,$session);
			
			my $progress =	xCAT::vboxService->IMachine_launchVMProcess($url, $machine, $session, "headless", ""); #gui
							
			if (!$progress) {
				addError("$node: Can not open remote session");
			} else {  # existing $progress
				xCAT::vboxService->IProgress_waitForCompletion($url, $progress, -1);
			
				if (xCAT::vboxService->IProgress_getCompleted($url, $progress)) {
					addMessage("$node: $args->[0]");
				} else { # not successfully completed
					addError("$node: Power on failed");
				}
				
			} # /existing $progress
			
			return;
			
		} # /powerOn
		

		sub powerOn_V3 #**********************************************************
		{
			my ($vboxvers, $url, $vbox, $machine, $session, $uuid, $node, $vncport, $args) =
				(shift, shift, shift, shift, shift, shift, shift, shift, shift);

			xCAT::vboxService->IVirtualBox_openSession_V3($url, $vbox, $session, $uuid);

			my $directmachine = xCAT::vboxService->ISession_getMachine_V3($url, $session);
			my $hasVRDP;
			if (!$directmachine) {
				addError("$node: Could not access mutable machine");
			} else {
				my $vrdp = xCAT::vboxService->IMachine_getVRDPServer_V3($url, $directmachine);
				$hasVRDP = xCAT::vboxService->IVRDPServer_getEnabled_V3($url, $vrdp);
				my $portno;
				my $vncportmismatch;	# current rdp port differs from vm table vncport definition
				if($vboxvers =~ "^3.0") {	# returns single port as unsigned long
					$portno = xCAT::vboxService->IVRDPServer_getPort_V30($url, $vrdp);
					if ($vncport and ($vncport != $portno)) {
						$vncportmismatch=1;
					}
				} else {					# returns one or more ports as string
					$portno = xCAT::vboxService->IVRDPServer_getPorts_V3($url, $vrdp);
					if ($vncport and ($vncport ne $portno)) {
						$vncportmismatch=1;
					}
				}
				my $change;
				if($vncport and ($hasVRDP eq "false")) {
					$hasVRDP="true";
					xCAT::vboxService->IVRDPServer_setEnabled_V3($url, $vrdp, $hasVRDP);
					$change=1;
				}
				if ($vncportmismatch) {
					if($vboxvers =~ "^3.0")	{ # returns single port as unsigned long
						xCAT::vboxService->IVRDPServer_setPort_V30($url, $vrdp, $vncport);
					} else {
						xCAT::vboxService->IVRDPServer_setPorts_V3($url, $vrdp, $vncport);
					}
					$change=1;
				}
				if (!$vncport and $hasVRDP eq "true") {
					$hasVRDP="false";
					xCAT::vboxService->IVRDPServer_setEnabled_V3($url, $vrdp, $hasVRDP);
					$change=1;
				}
				if ($change) {
					xCAT::vboxService->IMachine_saveSettings_V3($url, $directmachine);
				}
				xCAT::vboxService->IManagedObjectRef_release_V3($url, $vrdp);
				xCAT::vboxService->IManagedObjectRef_release_V3($url, $directmachine);
			}

			xCAT::vboxService->ISession_close_V3($url, $session);
			my $v3session;
			if ($hasVRDP eq "true")	{
				$v3session="vrdp"; #vrdp
			} else {
				# found "headles" to be undocumented in SDK ref, but this allows to open a headless session w/o VRDP
				# and thus we can disable rdp without being forced to launch the gui. This allows to follow
				# the logic, that an empty vncport in the vm table will disable rdp, and having a value in
				# table vm will define the port and enable rdp.
				$v3session="headless"; # gui
			}
			#my $progress = xCAT::vboxService->IVirtualBox_openRemoteSession_V3($url, $vbox, $session, $uuid, "vrdp", "");
			my $progress = xCAT::vboxService->IVirtualBox_openRemoteSession_V3($url, $vbox, $session, $uuid, $v3session, "");

			if (!$progress)	{
				addError("$node: Can not open remote session");
			} else { # existing $progress
				xCAT::vboxService->IProgress_waitForCompletion_V3($url, $progress, -1);

				if (xCAT::vboxService->IProgress_getCompleted_V3($url, $progress)) {
					addMessage("$node: $args->[0]");
				} else { # not successfully completed
					addError("$node: Power on failed");
				}

			} # /existing $progress

			return;
		} # /powerOn_V3
		
		sub powerOff #*********************************************************
		{
			my ($url, $vbox, $session, $machine, $uuid, $node, $args) =
				(shift, shift, shift, shift, shift, shift, shift);
			
			# Lock session with machine
			my $vmsessionstate = xCAT::vboxService->IMachine_getSessionState($url, $machine);
			my $vmsessiontype =	xCAT::vboxService->IMachine_getSessionType($url, $machine);
						 
			if ($vmsessionstate eq "Locked") {
				my $locktype="Shared";
				xCAT::vboxService->IMachine_lockMachine($url,$machine,$session,$locktype);
			} elsif ($vmsessionstate eq "Unlocked")	{
				my $locktype="Write";
				xCAT::vboxService->IMachine_lockMachine($url,$machine,$session,$locktype);
			} else { # !$vmsessionstate
				addError("$node: No direct session to machine");
			}

			# just test: check sessiontype - it turns out, that this needs a lock from traces
			#my $teste = xCAT::vboxService->ISession_getType($url, $session);
			#addError("$node: sessiontype =$teste");
			
			# The session is now locked with the machine, now its direct session or console can be optained and manipulated
			# Now get the console for the mutable machine
			my $console = xCAT::vboxService->ISession_getConsole($url, $session);
			if (!$console) {
				addError("$node: No console for mutable machine");
			} else { # existing $console
				if ($args->[0] =~ m/^reset|^boot/) {
					xCAT::vboxService->IConsole_reset($url, $console);
					xCAT::vboxService->IManagedObjectRef_release($url, $console);
					addMessage("$node: on reset");
					#xCAT::vboxService->IManagedObjectRef_release($url, $console);
					return;
				} else {
					xCAT::vboxService->IConsole_powerButton($url, $console);
					unless (xCAT::vboxService->IConsole_getPowerButtonHandled($url, $console) eq "true") {
						# In case of no reaction: force to power down machine
						xCAT::vboxService->IConsole_powerDown($url, $console);
					} addMessage("$node: off");
				}
				
				xCAT::vboxService->IManagedObjectRef_release($url, $console);
				
			} # /existing $console
			
			return;
			
		} # /powerOff
		
		sub powerOff_V3 #*********************************************************
		{
			my ($url, $vbox, $session, $machine, $uuid, $node, $args) =
				(shift, shift, shift, shift, shift, shift, shift);

			# Lock session with machine
			my $vmsessionstate = xCAT::vboxService->IMachine_getSessionState_V3($url, $machine);

			if ($vmsessionstate eq "Open") {
				xCAT::vboxService->IVirtualBox_openExistingSession_V3($url, $vbox, $session, $uuid);
			} elsif ($vmsessionstate eq "Closed") {
				xCAT::vboxService->IVirtualBox_openSession_V3($url, $vbox, $session, $uuid);
			} else {
				addError("$node: No direct session to machine");
			}

			# Now get the console for the mutable machine
			my $console = xCAT::vboxService->ISession_getConsole_V3($url, $session);
			if (!$console) {
				addError("$node: No console for mutable machine");
			} else { # existing $console
				if ($args->[0] =~ m/^reset|^boot/) {
					xCAT::vboxService->IConsole_reset_V3($url, $console);
					xCAT::vboxService->IManagedObjectRef_release_V3($url, $console);
					addMessage("$node: on reset");
					#xCAT::vboxService->IManagedObjectRef_release_V3($url, $console);
					return;
				} else {
					xCAT::vboxService->IConsole_powerButton_V3($url, $console);
					unless (xCAT::vboxService->IConsole_getPowerButtonHandled_V3($url, $console) eq "true") {
						# In case of no reaction: force to power down machine
						xCAT::vboxService->IConsole_powerDown_V3($url, $console);
					} addMessage("$node: off");
				}

				xCAT::vboxService->IManagedObjectRef_release_V3($url, $console);

			} # /existing $console
			
			return;
		} # /powerOff_V3
		
	} # /powerCtrl
	
	
	sub getSession ############################################################
	{
		my $sess;
		my ($api, $url, $vbox, $node, $vmname) = (shift, shift, shift, shift, shift);
		
		my $cmd = "IWebsessionManager_getSessionObject";
		if($api eq "3") {$cmd=$cmd . "_V3";}

		eval {
		$sess = xCAT::vboxService->$cmd($url, $vbox);
		};
		
		return $sess if ($sess and not $@);
		
		addError("$node: No session for $vmname on web service @ $url.");
		return undef;
	} # /getSession
	
	
	sub getMachine ############################################################
	{
		my $machine;
		my ($api, $url, $vbox, $vmname, $node) = (shift, shift, shift, shift, shift);
		
		my $cmd = "IVirtualBox_findMachine";
		if($api eq "3") {$cmd=$cmd . "_V3";}

		eval {
		$machine = xCAT::vboxService->$cmd($url, $vbox, $vmname);
		};
		return $machine if ($machine and not $@);
		
		addError("$node: VM $vmname not known by the web service @ $url.");
		return undef;
	} # /getMachine
	
	
	sub getWebService #########################################################
	{
		my $ws;
		my ($url, $user, $passwd, $node) = (shift, shift, shift, shift);
		
		my $cmd = "IWebsessionManager_logon";
		eval {
		$ws = xCAT::vboxService->$cmd($url, $user, $passwd);
		};
		
		return $ws if ($ws and not $@);
		
		addError("$node: No connection to the web service @ $url.");
		return undef;
	} # /getWebService
	
	
	sub getInfo ###############################################################
	{
		my $node = shift;
		
		# Open tables first, return in case of a critical error = 2
		my $hostsTab = openTable('hosts');
		return (2, undef, undef, undef, undef)	 if (!$hostsTab);
		
		my $vmTab = openTable('vm');
		return (2, undef, undef, undef, undef)	 if (!$vmTab);
		
		my $websrvTab = openTable('websrv');
		return (2, undef, undef, undef, undef)	 if (!$websrvTab);
		
		# Load attributes, return in case of missing information error = 1
		my $values;
		
		my @attributes	= ('host','vncport','comments');
		$values = loadValues($vmTab, $node, \@attributes);
		return (1, undef, undef, undef, undef)	 if (!defined($values));
			
		my $hostname	= $values->{host};
		my $vncport		= $values->{vncport};
		my $comments	= $values->{comments};
		my $vmname		= undef;
		if ($comments	=~ m/vmname:(.+)!/) {
			$vmname		=  $1;
		} else {
			$vmname		=  $node;
		}
		
		@attributes  = ('port','username', 'password');
		$values = loadValues($websrvTab, $hostname, \@attributes);
		return (1, undef, undef, undef, undef)	 if (!defined($values));
			
		my $port	 = $values->{port};
		my $username = $values->{username};
		my $password = $values->{password};
		
		@attributes  = ('ip');
		$values = loadValues($hostsTab, $hostname, \@attributes);
		return (1, undef, undef, undef, undef)	 if (!defined($values));
			
		my $ipaddr	 = $values->{ip};
		my $url		 = "http://$ipaddr:$port/";
		
		return (0, $url, $username, $password, $vmname, $vncport);
		
		
		sub loadValues #*******************************************************
		{
			my ($table, $node, $attributes) = (shift, shift, shift);
			my ($values) = $table->getAttribs({'node'=>$node}, @$attributes);
			$table->close;
			
			addError("$node: Missing information: @$attributes")
				if (!defined($values));
				
			return $values;
			
		} # /loadValues
		
		
		sub openTable #********************************************************
		{
			my $tabname = shift;
			my $table = xCAT::Table->new($tabname);
			
			# try to create vbox tables if they don't exist yet
			if (!$table and $tabname =~ m/websrv/)
			{
				$table = xCAT::Table->new( $tabname, -create=>1, -autocommit=>1 );
				$table->close;
				addMessage("[xCAT] Info: The table $tabname has been created.");
				$table = xCAT::Table->new($tabname);
			}
			
			addError("[xCAT] Can not open table: $tabname. Command aborted.")
				if (!$table);
			
			return $table;
			
		} # /openTable
		
	} # /getInfo
}

# see Lite.pm for other serializer code, e.g. as_string
sub SOAP::Serializer::as_LockType
{
	my ($self, $value, $name, $type, $attr) = @_;
	die "String value expected instead of @{[ref $value]} reference\n"
		if ref $value;
	return [
		$name,
		{'xsi:type' => 'vbox:LockType', %$attr},
		SOAP::Utils::encode_data($value)
	];
}

1;
