#!/usr/bin/python

#
# The Following agent has been tested on:
# vmrun 2.0.0 build-116503 (from VMware Server 2.0) against:
#	VMware ESX 4.0.0
#	VMware vCenter 4.0.0
# 	VMware ESX 3.5
# 	VMware Server 2.0.0
#	VMware ESXi 3.5 update 2
# 	VMware Server 1.0.7 (works but list/status show only running VMs)
#
# VI Perl API 1.6 against:
# 	VMware ESX 4.0.0
#	VMware vCenter 4.0.0
# 	VMware ESX 3.5
#	VMware ESXi 3.5 update 2
# 	VMware Virtual Center 2.5
#
# VMware vSphere SDK for Perl 4.0.0 against:
# 	VMware ESX 4.0.0
#	VMware vCenter 4.0.0
#

import sys, re, pexpect, exceptions
sys.path.append("/usr/lib/fence")
from fencing import *

#BEGIN_VERSION_GENERATION
FENCE_RELEASE_NAME="2.0.115";
REDHAT_COPYRIGHT=("Copyright (C) Red Hat, Inc.  2004  All rights reserved.")
BUILD_DATE="(built Tue Aug 23 13:16:07 EDT 2016)";
#END_VERSION_GENERATION

### CONSTANTS ####
# VMware type is ESX/ESXi/VC
VMWARE_TYPE_ESX=0
# VMware type is Server 1.x
VMWARE_TYPE_SERVER1=1
# VMware type is Server 2.x and/or ESX 3.5 up2, ESXi 3.5 up2, VC 2.5 up2
VMWARE_TYPE_SERVER2=2

# Minimum required version of vmrun command
VMRUN_MINIMUM_REQUIRED_VERSION=2

# Default path to vmhelper command
VMHELPER_COMMAND="fence_vmware_helper"
# Default path to vmrun command
VMRUN_COMMAND="/usr/bin/vmrun"
# Default type of vmware
VMWARE_DEFAULT_TYPE="esx"

#### GLOBAL VARIABLES ####
# Internal type. One of VMWARE_TYPE_, set by #vmware_check_vmware_type
vmware_internal_type=VMWARE_TYPE_ESX

# If ESX is disconnected, say, that VM is off (don't return previous state)
vmware_disconnected_hack=False

### FUNCTIONS ####

#Split string in simplified DSV format to array of items
def dsv_split(dsv_str):
	delimiter_c=':'
	escape_c='\\'

	res=[]
	status=0
	tmp_str=""

	for x in dsv_str:
		if (status==0):
			if (x==delimiter_c):
				res.append(tmp_str)
				tmp_str=""
			elif (x==escape_c):
				status=1
			else:
				tmp_str+=x
		elif (status==1):
			if (x==delimiter_c):
				tmp_str+=delimiter_c
			elif (x==escape_c):
				tmp_str+=escape_c
			else:
				tmp_str+=escape_c+x
			status=0

	if (tmp_str!=""):
		res.append(tmp_str)

	return res

# Quote string for proper existence in quoted string used for pexpect.run function
# Ex. test'this will return test'\''this. So pexpect run will really pass ' to argument
def quote_for_run(str):
	dstr=''

	for c in str:
		if c==r"'":
			dstr+="'\\''"
		else:
			dstr+=c

	return dstr

# Return string with command and additional parameters (something like vmrun -h 'host'
def vmware_prepare_command(options,add_login_params,additional_params):
	res=options["-e"]

	if (add_login_params):
		if (vmware_internal_type==VMWARE_TYPE_ESX):
			res+=" --server '%s' --username '%s' --password '%s' "%(quote_for_run(options["-a"]),
										quote_for_run(options["-l"]),
										quote_for_run(options["-p"]))
		elif (vmware_internal_type==VMWARE_TYPE_SERVER2):
			res+=" -h 'https://%s/sdk' -u '%s' -p '%s' -T server "%(quote_for_run(options["-a"]),
										quote_for_run(options["-l"]),
										quote_for_run(options["-p"]))
		elif (vmware_internal_type==VMWARE_TYPE_SERVER1):
			host_name_array=options["-a"].split(':')

			res+=" -h '%s' -u '%s' -p '%s' -T server1 "%(quote_for_run(host_name_array[0]),
								     quote_for_run(options["-l"]),
								     quote_for_run(options["-p"]))
			if (len(host_name_array)>1):
				res+="-P '%s' "%(quote_for_run(host_name_array[1]))

	if ((options.has_key("-s")) and (vmware_internal_type==VMWARE_TYPE_ESX)):
		res+="--datacenter '%s' "%(quote_for_run(options["-s"]))

	if (additional_params!=""):
		res+=additional_params

	return res

# Log message if user set verbose option
def vmware_log(options, message):
	if options["log"] >= LOG_MODE_VERBOSE:
		options["debug_fh"].write(message+"\n")

# Run command with timeout and parameters. Internaly uses vmware_prepare_command. Returns string
# with output from vmrun command. If something fails (command not found, exit code is not 0), fail_usage
# function is called (and never return).
def vmware_run_command(options,add_login_params,additional_params,additional_timeout):
	command=vmware_prepare_command(options,add_login_params,additional_params)

	try:
		vmware_log(options,command)

		(res_output,res_code)=pexpect.run(command,int(options["-Y"])+int(options["-y"])+additional_timeout,True)

		if (res_code==None):
			fail(EC_TIMED_OUT)
		if ((res_code!=0) and (add_login_params)):
			vmware_log(options,res_output)
			fail_usage("%s returned %s"%(options["-e"],res_output))
		else:
			vmware_log(options,res_output)

	except pexpect.ExceptionPexpect:
		fail_usage("Cannot run command %s"%(options["-e"]))

	return res_output

# Get outlet list with status as hash table. If you will use add_vm_name, only VM with vmname is
# returned. This is used in get_status function
def vmware_get_outlets_vi(conn, options, add_vm_name):
	outlets={}

	if (add_vm_name):
		all_machines=vmware_run_command(options,True,("--operation status --vmname '%s'"%(quote_for_run(options["-n"]))),0)
	else:
		all_machines=vmware_run_command(options,True,"--operation list",int(options["-g"]))

	all_machines_array=all_machines.splitlines()

	for machine in all_machines_array:
		machine_array=dsv_split(machine)
		if (len(machine_array)==4):
			if (machine_array[0] in outlets):
				fail_usage("Failed. More machines with same name %s found!"%(machine_array[0]))

			if (vmware_disconnected_hack):
				outlets[machine_array[0]]=("",(
						((machine_array[2].lower() in ["poweredon"]) and
						 (machine_array[3].lower()=="connected"))
						and "on" or "off"))
			else:
				outlets[machine_array[0]]=("",((machine_array[2].lower() in ["poweredon"]) and "on" or "off"))
	return outlets

# Get outlet list with status as hash table.
def vmware_get_outlets_vix(conn,options):
	outlets={}

	running_machines=vmware_run_command(options,True,"list",0)
	running_machines_array=running_machines.splitlines()[1:]

	if (vmware_internal_type==VMWARE_TYPE_SERVER2):
		all_machines=vmware_run_command(options,True,"listRegisteredVM",0)
		all_machines_array=all_machines.splitlines()[1:]
	elif (vmware_internal_type==VMWARE_TYPE_SERVER1):
		all_machines_array=running_machines_array

	for machine in all_machines_array:
		if (machine!=""):
			outlets[machine]=("",((machine in running_machines_array) and "on" or "off"))

	return outlets

def get_outlets_status(conn, options):
	if (vmware_internal_type==VMWARE_TYPE_ESX):
		return vmware_get_outlets_vi(conn,options,False)
	if ((vmware_internal_type==VMWARE_TYPE_SERVER1) or (vmware_internal_type==VMWARE_TYPE_SERVER2)):
		return vmware_get_outlets_vix(conn,options)

def get_power_status(conn,options):
	if (vmware_internal_type==VMWARE_TYPE_ESX):
		outlets=vmware_get_outlets_vi(conn,options,True)
	else:
		outlets=get_outlets_status(conn,options)

	if ((vmware_internal_type==VMWARE_TYPE_SERVER2) or (vmware_internal_type==VMWARE_TYPE_ESX)):
		if (not (options["-n"] in outlets)):
			fail_usage("Failed: You have to enter existing name of virtual machine!")
		else:
			return outlets[options["-n"]][1]
	elif (vmware_internal_type==VMWARE_TYPE_SERVER1):
		return ((options["-n"] in outlets) and "on" or "off")

def set_power_status(conn, options):
	if (vmware_internal_type==VMWARE_TYPE_ESX):
		additional_params="--operation %s --vmname '%s'"%((options["-o"]=="on" and "on" or "off"),quote_for_run(options["-n"]))
	elif ((vmware_internal_type==VMWARE_TYPE_SERVER1) or (vmware_internal_type==VMWARE_TYPE_SERVER2)):
		additional_params="%s '%s'"%((options["-o"]=="on" and "start" or "stop"),quote_for_run(options["-n"]))
		if (options["-o"]=="off"):
			additional_params+=" hard"

	vmware_run_command(options,True,additional_params,int(options["-g"]))

# Returns True, if user uses supported vmrun version (currently >=2.0.0) otherwise False.
def vmware_is_supported_vmrun_version(options):
	vmware_help_str=vmware_run_command(options,False,"",0)
	version_re=re.search("vmrun version (\d\.(\d[\.]*)*)",vmware_help_str.lower())
	if (version_re==None):
		    return False   # Looks like this "vmrun" is not real vmrun

	version_array=version_re.group(1).split(".")

	try:
		if (int(version_array[0])<VMRUN_MINIMUM_REQUIRED_VERSION):
			return False
	except Exception:
		return False

	return True

# Define new options
def vmware_define_defaults():
	all_opt["vmware_type"]["default"]=VMWARE_DEFAULT_TYPE

# Check vmware type, set vmware_internal_type to one of VMWARE_TYPE_ value and
# options["-e"] to path (if not specified)
def vmware_check_vmware_type(options):
	global vmware_internal_type

	options["-d"]=options["-d"].lower()

	if (options["-d"]=="esx"):
		vmware_internal_type=VMWARE_TYPE_ESX
		if (not options.has_key("-e")):
			options["-e"]=VMHELPER_COMMAND
	elif (options["-d"]=="server2"):
		vmware_internal_type=VMWARE_TYPE_SERVER2
		if (not options.has_key("-e")):
			options["-e"]=VMRUN_COMMAND
	elif (options["-d"]=="server1"):
		vmware_internal_type=VMWARE_TYPE_SERVER1
		if (not options.has_key("-e")):
			options["-e"]=VMRUN_COMMAND
	else:
		fail_usage("vmware_type can be esx,server2 or server1!")

# Main agent method
def main():
	device_opt = [ "help", "version", "agent", "quiet", "verbose", "debug",
		       "action", "ipaddr", "login", "passwd", "passwd_script",
		       "test", "port", "separator", "exec", "vmware_type",
		       "vmware_datacenter", "secure", "identity_file",
		       "power_timeout", "shell_timeout", "login_timeout", "power_wait" ]

	atexit.register(atexit_handler)

	vmware_define_defaults()

	options = check_input(device_opt, process_input(device_opt))

	# Default is secure connection
	options["-x"] = 1

	docs = { }
	docs["shortdesc"] = "Fence agent for VMWare"
	docs["longdesc"] = "fence_vmware is an I/O Fencing agent \
which can be used with the VMware ESX, VMware ESXi or VMware Server \
to fence virtual machines.\
\n.P\n\
Before you can use this agent, it must be installed VI Perl Toolkit or \
vmrun command on every node you want to make fencing.\
\n.P\n\
VI Perl Toolkit is preferred for VMware ESX/ESXi and Virtual Center. Vmrun \
command is only solution for VMware Server 1/2 (this command will works against \
ESX/ESXi 3.5 up2 and VC up2 too, but not cluster aware!) and is available as part \
of VMware VIX API SDK package. VI Perl and VIX API SDK are both available from \
VMware web pages (not int RHEL repository!). \
\n.P\n\
You can specify type of VMware you are connecting to with \\fB-d\\fP switch \
(or \\fIvmware_type\\fR for stdin). Possible values are esx, server2 and server1.\
Default value is esx, which will use VI Perl. With server1 and server2, vmrun \
command is used.\
\n.P\n\
After you have successfully installed VI Perl Toolkit or VIX API, you should \
be able to run fence_vmware_helper (part of this agent) or vmrun command. \
This agent supports only vmrun from version 2.0.0 (VIX API 1.6.0)."
	docs["vendorurl"] = "http://www.vmware.com"
	show_docs(options, docs)

	# Check vmware type and set path
	vmware_check_vmware_type(options)

	# Test user vmrun command version
	if ((vmware_internal_type==VMWARE_TYPE_SERVER1) or (vmware_internal_type==VMWARE_TYPE_SERVER2)):
		if (not (vmware_is_supported_vmrun_version(options))):
			fail_usage("Unsupported version of vmrun command! You must use at least version %d!"%(VMRUN_MINIMUM_REQUIRED_VERSION))

	# Operate the fencing device
	result = fence_action(None, options, set_power_status, get_power_status, get_outlets_status)
	
	sys.exit(result)

if __name__ == "__main__":
	main()
