#!/usr/bin/python
#
#############################################################################
# Copyright 2011 Matthew Clark
# This file is part of fence-xenserver
#
# fence-xenserver 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 2 of the License, or
# (at your option) any later version.
# 
# fence-xenserver 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/>.

# Please let me know if you are using this script so that I can work out
# whether I should continue support for it. mattjclark0407 at hotmail dot com
#############################################################################

#############################################################################
# It's only just begun...
# Current status: completely usable. This script is now working well and,
# has a lot of functionality as a result of the fencing.py library and the
# XenAPI libary.

#############################################################################
# Please let me know if you are using this script so that I can work out
# whether I should continue support for it. mattjclark0407 at hotmail dot com

import sys
sys.path.append("/usr/share/fence")
from fencing import *
import XenAPI

#BEGIN_VERSION_GENERATION
RELEASE_VERSION="3.1.5"
BUILD_DATE="(built Mon Feb 6 23:36:24 UTC 2012)"
REDHAT_COPYRIGHT="Copyright (C) Red Hat, Inc. 2004-2010 All rights reserved."
#END_VERSION_GENERATION

EC_BAD_SESSION 		= 1
# Find the status of the port given in the -U flag of options.
def get_power_fn(session, options):
	if options.has_key("-v"):
		verbose = True
	else:
		verbose = False
		
	try:
		# Get a reference to the vm specified in the UUID or vm_name/port parameter
		vm = return_vm_reference(session, options)
		# Query the VM for its' associated parameters
		record = session.xenapi.VM.get_record(vm);
		# Check that we are not trying to manipulate a template or a control
		# domain as they show up as VM's with specific properties.
		if not(record["is_a_template"]) and not(record["is_control_domain"]):
			status = record["power_state"]
			if verbose: print "UUID:", record["uuid"], "NAME:", record["name_label"], "POWER STATUS:", record["power_state"]
			# Note that the VM can be in the following states (from the XenAPI document)
			# Halted: VM is offline and not using any resources.
			# Paused: All resources have been allocated but the VM itself is paused and its vCPUs are not running
			# Running: Running
			# Paused: VM state has been saved to disk and it is nolonger running. Note that disks remain in-Use while
			# We want to make sure that we only return the status "off" if the machine is actually halted as the status
			# is checked before a fencing action. Only when the machine is Halted is it not consuming resources which
			# may include whatever you are trying to protect with this fencing action.
			return (status=="Halted" and "off" or "on")
	except Exception, exn:
		print str(exn)

	return "Error"

# Set the state of the port given in the -U flag of options.
def set_power_fn(session, options):
	action = options["-o"].lower()
	if options.has_key("-v"):
		verbose = True
	else:
		verbose = False
	
	try:
		# Get a reference to the vm specified in the UUID or vm_name/port parameter
		vm = return_vm_reference(session, options)
		# Query the VM for its' associated parameters
		record = session.xenapi.VM.get_record(vm)
		# Check that we are not trying to manipulate a template or a control
		# domain as they show up as VM's with specific properties.
		if not(record["is_a_template"]) and not(record["is_control_domain"]):
			if( action == "on" ):
				# Start the VM 
				session.xenapi.VM.start(vm, False, True)
			elif( action == "off" ):
				# Force shutdown the VM
				session.xenapi.VM.hard_shutdown(vm)
			elif( action == "reboot" ):
				# Force reboot the VM
				session.xenapi.VM.hard_reboot(vm)
	except Exception, exn:
		print str(exn);

# Function to populate an array of virtual machines and their status
def get_outlet_list(session, options):
	result = {}
	if options.has_key("-v"):
		verbose = True
	else:
		verbose = False

	try:
		# Return an array of all the VM's on the host
		vms = session.xenapi.VM.get_all()
		for vm in vms:
			# Query the VM for its' associated parameters
			record = session.xenapi.VM.get_record(vm);
			# Check that we are not trying to manipulate a template or a control
			# domain as they show up as VM's with specific properties.
			if not(record["is_a_template"]) and not(record["is_control_domain"]):
				name = record["name_label"]
				uuid = record["uuid"]
				status = record["power_state"]
				result[uuid] = (name, status)
				if verbose: print "UUID:", record["uuid"], "NAME:", name, "POWER STATUS:", record["power_state"]
	except Exception, exn:
		print str(exn);

	return result

# Function to initiate the XenServer session via the XenAPI library.
def connect_and_login(options):
	url = options["-s"]
	username = options["-l"]
	password = options["-p"]

	try:
		# Create the XML RPC session to the specified URL.
		session = XenAPI.Session(url);
		# Login using the supplied credentials.
		session.xenapi.login_with_password(username, password);
	except Exception, exn:
		print str(exn);
		# http://sources.redhat.com/cluster/wiki/FenceAgentAPI says that for no connectivity
		# the exit value should be 1. It doesn't say anything about failed logins, so 
		# until I hear otherwise it is best to keep this exit the same to make sure that
		# anything calling this script (that uses the same information in the web page
		# above) knows that this is an error condition, not a msg signifying a down port.
		sys.exit(EC_BAD_SESSION); 
	return session;

# return a reference to the VM by either using the UUID or the vm_name/port. If the UUID is set then
# this is tried first as this is the only properly unique identifier.
# Exceptions are not handled in this function, code that calls this must be ready to handle them.
def return_vm_reference(session, options):
	if options.has_key("-v"):
		verbose = True
	else:
		verbose = False

	# Case where the UUID has been specified
	if options.has_key("-U"):
		uuid = options["-U"].lower()
		# When using the -n parameter for name, we get an error message (in verbose
		# mode) that tells us that we didn't find a VM. To immitate that here we
		# need to catch and re-raise the exception produced by get_by_uuid.
		try:
			return session.xenapi.VM.get_by_uuid(uuid)
		except Exception,exn:
			if verbose: print "No VM's found with a UUID of \"%s\"" %uuid
			raise
		

	# Case where the vm_name/port has been specified
	if options.has_key("-n"):
		vm_name = options["-n"]
		vm_arr = session.xenapi.VM.get_by_name_label(vm_name)
		# Need to make sure that we only have one result as the vm_name may
		# not be unique. Average case, so do it first.
		if len(vm_arr) == 1:
			return vm_arr[0]
		else:
			if len(vm_arr) == 0:
				if verbose: print "No VM's found with a name of \"%s\"" %vm_name
				# NAME_INVALID used as the XenAPI throws a UUID_INVALID if it can't find
				# a VM with the specified UUID. This should make the output look fairly
				# consistent.
				raise Exception("NAME_INVALID")
			else:
				if verbose: print "Multiple VM's have the name \"%s\", use UUID instead" %vm_name
				raise Exception("MULTIPLE_VMS_FOUND")

	# We should never get to this case as the input processing checks that either the UUID or
	# the name parameter is set. Regardless of whether or not a VM is found the above if
	# statements will return to the calling function (either by exception or by a reference
	# to the VM).
	raise Exception("VM_LOGIC_ERROR")

def main():

	device_opt = [ "help", "version", "agent", "quiet", "verbose", "debug", "action",
			"login", "passwd", "passwd_script", "port", "test", "separator",
			"no_login", "no_password", "power_timeout", "shell_timeout",
			"login_timeout", "power_wait", "session_url", "uuid" ]

	atexit.register(atexit_handler)

	options=process_input(device_opt)

	options = check_input(device_opt, options)

	docs = { }
	docs["shortdesc"] = "XenAPI based fencing for the Citrix XenServer virtual machines."
	docs["longdesc"] = "\
fence_cxs is an I/O Fencing agent used on Citrix XenServer hosts. \
It uses the XenAPI, supplied by Citrix, to establish an XML-RPC sesssion \
to a XenServer host. Once the session is established, further XML-RPC \
commands are issued in order to switch on, switch off, restart and query \
the status of virtual machines running on the host." 
	show_docs(options, docs)

	xenSession = connect_and_login(options)
	
	# Operate the fencing device
	result = fence_action(xenSession, options, set_power_fn, get_power_fn, get_outlet_list)

	sys.exit(result)

if __name__ == "__main__":
	main()
