Blob Blame History Raw
diff -uNr a/fence/agents/compute/fence_compute.py b/fence/agents/compute/fence_compute.py
--- a/fence/agents/compute/fence_compute.py	2017-09-27 15:01:34.974642469 +0200
+++ b/fence/agents/compute/fence_compute.py	2017-09-27 15:24:57.482819900 +0200
@@ -18,173 +18,115 @@
 #END_VERSION_GENERATION
 
 override_status = ""
-nova = None
 
 EVACUABLE_TAG = "evacuable"
 TRUE_TAGS = ['true']
 
-def get_power_status(_, options):
-	global override_status
-
-	status = "unknown"
-	logging.debug("get action: " + options["--action"])
+def get_power_status(connection, options):
 
 	if len(override_status):
 		logging.debug("Pretending we're " + override_status)
 		return override_status
 
-	if nova:
+	status = "unknown"
+	logging.debug("get action: " + options["--action"])
+
+	if connection:
 		try:
-			services = nova.services.list(host=options["--plug"])
+			services = connection.services.list(host=options["--plug"], binary="nova-compute")
 			for service in services:
-				logging.debug("Status of %s is %s" % (service.binary, service.state))
-				if service.binary == "nova-compute":
-					if service.state == "up":
-						status = "on"
-					elif service.state == "down":
-						status = "off"
-					else:
-						logging.debug("Unknown status detected from nova: " + service.state)
-					break
+				logging.debug("Status of %s on %s is %s, %s" % (service.binary, options["--plug"], service.state, service.status))
+				if service.state == "up" and service.status == "enabled":
+					# Up and operational
+					status = "on"
+					
+				elif service.state == "down" and service.status == "disabled":
+					# Down and fenced
+					status = "off"
+
+				elif service.state == "down":
+					# Down and requires fencing
+					status = "failed"
+
+				elif service.state == "up":
+					# Up and requires unfencing
+					status = "running"
+				else:
+					logging.warning("Unknown status detected from nova for %s: %s, %s" % (options["--plug"], service.state, service.status))
+					status = "%s %s" % (service.state, service.status)
+				break
 		except requests.exception.ConnectionError as err:
 			logging.warning("Nova connection failed: " + str(err))
+	logging.debug("Final status of %s is %s" % (options["--plug"], status))
 	return status
 
-# NOTE(sbauza); We mimic the host-evacuate module since it's only a contrib
-# module which is not stable
-def _server_evacuate(server, on_shared_storage):
-	success = False
-	error_message = ""
-	try:
-		logging.debug("Resurrecting instance: %s" % server)
-		(response, dictionary) = nova.servers.evacuate(server=server, on_shared_storage=on_shared_storage)
-
-		if response == None:
-			error_message = "No response while evacuating instance"
-		elif response.status_code == 200:
-			success = True
-			error_message = response.reason
-		else:
-			error_message = response.reason
-
-	except Exception as e:
-		error_message = "Error while evacuating instance: %s" % e
-
-	return {
-		"uuid": server,
-		"accepted": success,
-		"reason": error_message,
-		}
-
-def _is_server_evacuable(server, evac_flavors, evac_images):
-	if server.flavor.get('id') in evac_flavors:
-		return True
-	if server.image.get('id') in evac_images:
-		return True
-	logging.debug("Instance %s is not evacuable" % server.image.get('id'))
-	return False
-
-def _get_evacuable_flavors():
-	result = []
-	flavors = nova.flavors.list()
-	# Since the detailed view for all flavors doesn't provide the extra specs,
-	# we need to call each of the flavor to get them.
-	for flavor in flavors:
-		tag = flavor.get_keys().get(EVACUABLE_TAG)
-		if tag and tag.strip().lower() in TRUE_TAGS:
-			result.append(flavor.id)
-	return result
-
-def _get_evacuable_images():
-	result = []
-	images = nova.images.list(detailed=True)
-	for image in images:
-		if hasattr(image, 'metadata'):
-			tag = image.metadata.get(EVACUABLE_TAG)
-			if tag and tag.strip().lower() in TRUE_TAGS:
-				result.append(image.id)
-	return result
-
-def _host_evacuate(options):
-	result = True
-	images = _get_evacuable_images()
-	flavors = _get_evacuable_flavors()
-	servers = nova.servers.list(search_opts={'host': options["--plug"], 'all_tenants': 1 })
-
-	if options["--instance-filtering"] == "False":
-		logging.debug("Not evacuating anything")
-		evacuables = []
-	elif len(flavors) or len(images):
-		logging.debug("Filtering images and flavors: %s %s" % (repr(flavors), repr(images)))
-		# Identify all evacuable servers
-		logging.debug("Checking %s" % repr(servers))
-		evacuables = [server for server in servers
-				if _is_server_evacuable(server, flavors, images)]
-		logging.debug("Evacuating %s" % repr(evacuables))
-	else:
-		logging.debug("Evacuating all images and flavors")
-		evacuables = servers
-
-	if options["--no-shared-storage"] != "False":
-		on_shared_storage = False
-	else:
-		on_shared_storage = True
-
-	for server in evacuables:
-		logging.debug("Processing %s" % server)
-		if hasattr(server, 'id'):
-			response = _server_evacuate(server.id, on_shared_storage)
-			if response["accepted"]:
-				logging.debug("Evacuated %s from %s: %s" %
-					      (response["uuid"], options["--plug"], response["reason"]))
-			else:
-				logging.error("Evacuation of %s on %s failed: %s" %
-					      (response["uuid"], options["--plug"], response["reason"]))
-				result = False
-		else:
-			logging.error("Could not evacuate instance: %s" % server.to_dict())
-			# Should a malformed instance result in a failed evacuation?
-			# result = False
-	return result
+def get_power_status_simple(connection, options):
+	status = get_power_status(connection, options)
+	if status in [ "off" ]:
+		return status
+	return "on"
 
 def set_attrd_status(host, status, options):
 	logging.debug("Setting fencing status for %s to %s" % (host, status))
 	run_command(options, "attrd_updater -p -n evacuate -Q -N %s -U %s" % (host, status))
 
-def set_power_status(_, options):
-	global override_status
-
-	override_status = ""
-	logging.debug("set action: " + options["--action"])
+def get_attrd_status(host, options):
+	(status, pipe_stdout, pipe_stderr) = run_command(options, "attrd_updater -p -n evacuate -Q -N %s" % (host))
+	fields = pipe_stdout.split('"')
+	if len(fields) > 6:
+		return fields[5]
+	logging.debug("Got %s: o:%s e:%s n:%d" % (status, pipe_stdout, pipe_stderr, len(fields)))
+	return ""
+
+def set_power_status_on(connection, options):
+	# Wait for any evacuations to complete
+	while True:
+		current = get_attrd_status(options["--plug"], options)
+		if current in ["no", ""]:
+			logging.info("Evacuation complete for: %s '%s'" % (options["--plug"], current))
+			break
+		else:
+			logging.info("Waiting for %s to complete evacuations: %s" % (options["--plug"], current))
+			time.sleep(2)
 
-	if not nova:
-		return
+	status = get_power_status(connection, options)
+	# Should we do it for 'failed' too?
+	if status in [ "off", "running", "failed" ]:
+		try:
+			# Forcing the host back up
+			logging.info("Forcing nova-compute back up on "+options["--plug"])
+			connection.services.force_down(options["--plug"], "nova-compute", force_down=False)
+			logging.info("Forced nova-compute back up on "+options["--plug"])
+		except Exception as e:
+			# In theory, if force_down=False fails, that's for the exact
+			# same possible reasons that below with force_down=True
+			# eg. either an incompatible version or an old client.
+			# Since it's about forcing back to a default value, there is
+			# no real worries to just consider it's still okay even if the
+			# command failed
+			logging.warn("Exception from attempt to force "
+				      "host back up via nova API: "
+				      "%s: %s" % (e.__class__.__name__, e))
+
+		# Forcing the service back up in case it was disabled
+		logging.info("Enabling nova-compute on "+options["--plug"])
+		connection.services.enable(options["--plug"], 'nova-compute')
 
-	if options["--action"] == "on":
-		if get_power_status(_, options) != "on":
-			# Forcing the service back up in case it was disabled
-			nova.services.enable(options["--plug"], 'nova-compute')
-			try:
-				# Forcing the host back up
-				nova.services.force_down(
-					options["--plug"], "nova-compute", force_down=False)
-			except Exception as e:
-				# In theory, if force_down=False fails, that's for the exact
-				# same possible reasons that below with force_down=True
-				# eg. either an incompatible version or an old client.
-				# Since it's about forcing back to a default value, there is
-				# no real worries to just consider it's still okay even if the
-				# command failed
-				logging.info("Exception from attempt to force "
-					      "host back up via nova API: "
-					      "%s: %s" % (e.__class__.__name__, e))
-		else:
-			# Pretend we're 'on' so that the fencing library doesn't loop forever waiting for the node to boot
-			override_status = "on"
+		# Pretend we're 'on' so that the fencing library doesn't loop forever waiting for the node to boot
+		override_status = "on"
+	elif status not in ["on"]:
+		# Not safe to unfence, don't waste time looping to see if the status changes to "on"
+		options["--power-timeout"] = "0"
+
+def set_power_status_off(connection, options):
+	status = get_power_status(connection, options)
+	if status in [ "off" ]:
 		return
 
+	connection.services.disable(options["--plug"], 'nova-compute')
 	try:
-		nova.services.force_down(
+		# Until 2.53
+		connection.services.force_down(
 			options["--plug"], "nova-compute", force_down=True)
 	except Exception as e:
 		# Something went wrong when we tried to force the host down.
@@ -198,7 +140,7 @@
 			      "%s: %s" % (e.__class__.__name__, e))
 		# need to wait for nova to update its internal status or we
 		# cannot call host-evacuate
-		while get_power_status(_, options) != "off":
+		while get_power_status(connection, options) not in ["off"]:
 			# Loop forever if need be.
 			#
 			# Some callers (such as Pacemaker) will have a timer
@@ -206,47 +148,55 @@
 			logging.debug("Waiting for nova to update its internal state for %s" % options["--plug"])
 			time.sleep(1)
 
-	if not _host_evacuate(options):
-		sys.exit(1)
+	set_attrd_status(options["--plug"], "yes", options)
+
+def set_power_status(connection, options):
+	global override_status
 
-	return
+	override_status = ""
+	logging.debug("set action: " + options["--action"])
+
+	if not connection:
+		return
 
+	if options["--action"] in ["off", "reboot"]:
+		set_power_status_off(connection, options)
+	else:
+		set_power_status_on(connection, options)
+	logging.debug("set action passed: " + options["--action"])
+	sys.exit(0)
 
-def fix_domain(options):
+def fix_domain(connection, options):
 	domains = {}
 	last_domain = None
 
-	if nova:
+	if connection:
 		# Find it in nova
 
-		hypervisors = nova.hypervisors.list()
-		for hypervisor in hypervisors:
-			shorthost = hypervisor.hypervisor_hostname.split('.')[0]
+		services = connection.services.list(binary="nova-compute")
+		for service in services:
+			shorthost = service.host.split('.')[0]
 
-			if shorthost == hypervisor.hypervisor_hostname:
+			if shorthost == service.host:
 				# Nova is not using FQDN 
 				calculated = ""
 			else:
 				# Compute nodes are named as FQDN, strip off the hostname
-				calculated = hypervisor.hypervisor_hostname.replace(shorthost+".", "")
-
-			domains[calculated] = shorthost
+				calculated = service.host.replace(shorthost+".", "")
 
 			if calculated == last_domain:
 				# Avoid complaining for each compute node with the same name
 				# One hopes they don't appear interleaved as A.com B.com A.com B.com
-				logging.debug("Calculated the same domain from: %s" % hypervisor.hypervisor_hostname)
+				logging.debug("Calculated the same domain from: %s" % service.host)
+				continue
 
-			elif "--domain" in options and options["--domain"] == calculated:
-				# Supplied domain name is valid 
-				return
+			domains[calculated] = service.host
+			last_domain = calculated
 
-			elif "--domain" in options:
+			if "--domain" in options and options["--domain"] != calculated:
 				# Warn in case nova isn't available at some point
 				logging.warning("Supplied domain '%s' does not match the one calculated from: %s"
-					      % (options["--domain"], hypervisor.hypervisor_hostname))
-
-			last_domain = calculated
+					      % (options["--domain"], service.host))
 
 	if len(domains) == 0 and "--domain" not in options:
 		logging.error("Could not calculate the domain names used by compute nodes in nova")
@@ -254,9 +204,9 @@
 	elif len(domains) == 1 and "--domain" not in options:
 		options["--domain"] = last_domain
 
-	elif len(domains) == 1:
-		logging.error("Overriding supplied domain '%s' does not match the one calculated from: %s"
-			      % (options["--domain"], hypervisor.hypervisor_hostname))
+	elif len(domains) == 1 and options["--domain"] != last_domain:
+		logging.error("Overriding supplied domain '%s' as it does not match the one calculated from: %s"
+			      % (options["--domain"], domains[last_domain]))
 		options["--domain"] = last_domain
 
 	elif len(domains) > 1:
@@ -264,47 +214,49 @@
 			      % (options["--domain"], repr(domains)))
 		sys.exit(1)
 
-def fix_plug_name(options):
+	return last_domain
+
+def fix_plug_name(connection, options):
 	if options["--action"] == "list":
 		return
 
 	if "--plug" not in options:
 		return
 
-	fix_domain(options)
-	short_plug = options["--plug"].split('.')[0]
-	logging.debug("Checking target '%s' against calculated domain '%s'"% (options["--plug"], options["--domain"]))
-
-	if "--domain" not in options:
+	calculated = fix_domain(connection, options)
+	if calculated is None or "--domain" not in options:
 		# Nothing supplied and nova not available... what to do... nothing
 		return
 
-	elif options["--domain"] == "":
+	short_plug = options["--plug"].split('.')[0]
+	logging.debug("Checking target '%s' against calculated domain '%s'"% (options["--plug"], calculated))
+
+	if options["--domain"] == "":
 		# Ensure any domain is stripped off since nova isn't using FQDN
 		options["--plug"] = short_plug
 
-	elif options["--domain"] in options["--plug"]:
-		# Plug already contains the domain, don't re-add 
+	elif options["--plug"].endswith(options["--domain"]):
+		# Plug already uses the domain, don't re-add
 		return
 
 	else:
 		# Add the domain to the plug
 		options["--plug"] = short_plug + "." + options["--domain"]
 
-def get_plugs_list(_, options):
+def get_plugs_list(connection, options):
 	result = {}
 
-	if nova:
-		hypervisors = nova.hypervisors.list()
-		for hypervisor in hypervisors:
-			longhost = hypervisor.hypervisor_hostname
+	if connection:
+		services = connection.services.list(binary="nova-compute")
+		for service in services:
+			longhost = service.host
 			shorthost = longhost.split('.')[0]
 			result[longhost] = ("", None)
 			result[shorthost] = ("", None)
 	return result
 
 def create_nova_connection(options):
-	global nova
+	nova = None
 
 	try:
 		from novaclient import client
@@ -330,41 +282,42 @@
 		if clientargs:
 			# OSP < 11
 			# ArgSpec(args=['version', 'username', 'password', 'project_id', 'auth_url'],
-			#         varargs=None,
-			#         keywords='kwargs', defaults=(None, None, None, None))
+			#	 varargs=None,
+			#	 keywords='kwargs', defaults=(None, None, None, None))
 			nova = client.Client(version,
-					options["--username"],
-					options["--password"],
-					options["--tenant-name"],
-					options["--auth-url"],
-					insecure=options["--insecure"],
-					region_name=options["--region-name"],
-					endpoint_type=options["--endpoint-type"],
-					http_log_debug=options.has_key("--verbose"))
+					     options["--username"],
+					     options["--password"],
+					     options["--tenant-name"],
+					     options["--auth-url"],
+					     insecure=options["--insecure"],
+					     region_name=options["--region-name"],
+					     endpoint_type=options["--endpoint-type"],
+					     http_log_debug=options.has_key("--verbose"))
 		else:
 			# OSP >= 11
 			# ArgSpec(args=['version'], varargs='args', keywords='kwargs', defaults=None)
 			nova = client.Client(version,
-					username=options["--username"],
-					password=options["--password"],
-					tenant_name=options["--tenant-name"],
-					auth_url=options["--auth-url"],
-					insecure=options["--insecure"],
-					region_name=options["--region-name"],
-					endpoint_type=options["--endpoint-type"],
-					http_log_debug=options.has_key("--verbose"))
+					     username=options["--username"],
+					     password=options["--password"],
+					     tenant_name=options["--tenant-name"],
+					     auth_url=options["--auth-url"],
+					     insecure=options["--insecure"],
+					     region_name=options["--region-name"],
+					     endpoint_type=options["--endpoint-type"],
+					     http_log_debug=options.has_key("--verbose"))
 
 		try:
 			nova.hypervisors.list()
-			return
+			return nova
 
 		except NotAcceptable as e:
 			logging.warning(e)
 
 		except Exception as e:
 			logging.warning("Nova connection failed. %s: %s" % (e.__class__.__name__, e))
-			
+
 	logging.warning("Couldn't obtain a supported connection to nova, tried: %s\n" % repr(versions))
+	return None
 
 def define_new_opts():
 	all_opt["endpoint_type"] = {
@@ -448,11 +401,23 @@
 		"order": 5,
 	}
 
+def set_multi_power_fn(connection, options, set_power_fn, get_power_fn, retry_attempts=1):
+	for _ in range(retry_attempts):
+		set_power_fn(connection, options)
+		time.sleep(int(options["--power-wait"]))
+
+		for _ in range(int(options["--power-timeout"])):
+			if get_power_fn(connection, options) != options["--action"]:
+				time.sleep(1)
+			else:
+				return True
+	return False
+
 def main():
 	global override_status
 	atexit.register(atexit_handler)
 
-	device_opt = ["login", "passwd", "tenant_name", "auth_url", "fabric_fencing", 
+	device_opt = ["login", "passwd", "tenant_name", "auth_url", "fabric_fencing",
 		"no_login", "no_password", "port", "domain", "no_shared_storage", "endpoint_type",
 		"record_only", "instance_filtering", "insecure", "region_name"]
 	define_new_opts()
@@ -472,30 +437,28 @@
 
 	run_delay(options)
 
-	create_nova_connection(options)
+	logging.debug("Running "+options["--action"])
+	connection = create_nova_connection(options)
 
-	fix_plug_name(options)
-	if options["--record-only"] in [ "1", "True", "true", "Yes", "yes"]:
-		if options["--action"] == "on":
-			set_attrd_status(options["--plug"], "no", options)
-			sys.exit(0)
-
-		elif options["--action"] in ["off", "reboot"]:
-			set_attrd_status(options["--plug"], "yes", options)
-			sys.exit(0)
+	if options["--action"] in ["off", "on", "reboot", "status"]:
+		fix_plug_name(connection, options)
 
-		elif options["--action"] in ["monitor", "status"]:
-			sys.exit(0)
 
-	if options["--action"] in ["off", "reboot"]:
-		# Pretend we're 'on' so that the fencing library will always call set_power_status(off)
-		override_status = "on"
-
-	if options["--action"] == "on":
-		# Pretend we're 'off' so that the fencing library will always call set_power_status(on)
-		override_status = "off"
+	if options["--action"] in ["reboot"]:
+		options["--action"]="off"
+
+	if options["--action"] in ["off", "on"]:
+		# No status first, call our own version
+		result = not set_multi_power_fn(connection, options, set_power_status, get_power_status_simple,
+						1 + int(options["--retry-on"]))
+	elif options["--action"] in ["monitor"]:
+		result = 0
+	else:
+		result = fence_action(connection, options, set_power_status, get_power_status_simple, get_plugs_list, None)
 
-	result = fence_action(None, options, set_power_status, get_power_status, get_plugs_list, None)
+	logging.debug("Result for "+options["--action"]+": "+repr(result))
+	if result == None:
+		result = 0
 	sys.exit(result)
 
 if __name__ == "__main__":
diff -uNr a/fence/agents/compute/fence_evacuate.py b/fence/agents/compute/fence_evacuate.py
--- a/fence/agents/compute/fence_evacuate.py	1970-01-01 01:00:00.000000000 +0100
+++ b/fence/agents/compute/fence_evacuate.py	2017-09-27 15:25:54.234304769 +0200
@@ -0,0 +1,366 @@
+#!/usr/bin/python -tt
+
+import sys
+import time
+import atexit
+import logging
+import inspect
+import requests.exceptions
+
+sys.path.append("@FENCEAGENTSLIBDIR@")
+from fencing import *
+from fencing import fail_usage, is_executable, run_command, run_delay
+
+EVACUABLE_TAG = "evacuable"
+TRUE_TAGS = ['true']
+
+def get_power_status(connection, options):
+
+	status = "unknown"
+	logging.debug("get action: " + options["--action"])
+
+	if connection:
+		try:
+			services = connection.services.list(host=options["--plug"], binary="nova-compute")
+			for service in services:
+				logging.debug("Status of %s is %s, %s" % (service.binary, service.state, service.status))
+				if service.state == "up" and service.status == "enabled":
+					# Up and operational
+					status = "on"
+					
+				elif service.state == "down" and service.status == "disabled":
+					# Down and fenced
+					status = "off"
+
+				elif service.state == "down":
+					# Down and requires fencing
+					status = "failed"
+
+				elif service.state == "up":
+					# Up and requires unfencing
+					status = "running"
+				else:
+					logging.warning("Unknown status detected from nova for %s: %s, %s" % (options["--plug"], service.state, service.status))
+					status = "%s %s" % (service.state, service.status)
+				break
+		except requests.exception.ConnectionError as err:
+			logging.warning("Nova connection failed: " + str(err))
+	return status
+
+# NOTE(sbauza); We mimic the host-evacuate module since it's only a contrib
+# module which is not stable
+def _server_evacuate(connection, server, on_shared_storage):
+	success = False
+	error_message = ""
+	try:
+		logging.debug("Resurrecting instance: %s" % server)
+		(response, dictionary) = connection.servers.evacuate(server=server, on_shared_storage=on_shared_storage)
+
+		if response == None:
+			error_message = "No response while evacuating instance"
+		elif response.status_code == 200:
+			success = True
+			error_message = response.reason
+		else:
+			error_message = response.reason
+
+	except Exception as e:
+		error_message = "Error while evacuating instance: %s" % e
+
+	return {
+		"uuid": server,
+		"accepted": success,
+		"reason": error_message,
+		}
+
+def _is_server_evacuable(server, evac_flavors, evac_images):
+	if server.flavor.get('id') in evac_flavors:
+		return True
+	if hasattr(server.image, 'get'):
+		if server.image.get('id') in evac_images:
+			return True
+	logging.debug("Instance %s is not evacuable" % server.image.get('id'))
+	return False
+
+def _get_evacuable_flavors(connection):
+	result = []
+	flavors = connection.flavors.list()
+	# Since the detailed view for all flavors doesn't provide the extra specs,
+	# we need to call each of the flavor to get them.
+	for flavor in flavors:
+		tag = flavor.get_keys().get(EVACUABLE_TAG)
+		if tag and tag.strip().lower() in TRUE_TAGS:
+			result.append(flavor.id)
+	return result
+
+def _get_evacuable_images(connection):
+	result = []
+	images = []
+	if hasattr(connection, "images"):
+		images = connection.images.list(detailed=True)
+	elif hasattr(connection, "glance"):
+		# OSP12+
+		images = connection.glance.list()
+
+	for image in images:
+		if hasattr(image, 'metadata'):
+			tag = image.metadata.get(EVACUABLE_TAG)
+			if tag and tag.strip().lower() in TRUE_TAGS:
+				result.append(image.id)
+		elif hasattr(image, 'tags'):
+			# OSP12+
+			if EVACUABLE_TAG in image.tags:
+				result.append(image.id)
+	return result
+
+def _host_evacuate(connection, options):
+	result = True
+	images = _get_evacuable_images(connection)
+	flavors = _get_evacuable_flavors(connection)
+	servers = connection.servers.list(search_opts={'host': options["--plug"], 'all_tenants': 1 })
+
+	if options["--instance-filtering"] == "False":
+		logging.debug("Not evacuating anything")
+		evacuables = []
+	elif len(flavors) or len(images):
+		logging.debug("Filtering images and flavors: %s %s" % (repr(flavors), repr(images)))
+		# Identify all evacuable servers
+		logging.debug("Checking %s" % repr(servers))
+		evacuables = [server for server in servers
+				if _is_server_evacuable(server, flavors, images)]
+		logging.debug("Evacuating %s" % repr(evacuables))
+	else:
+		logging.debug("Evacuating all images and flavors")
+		evacuables = servers
+
+	if options["--no-shared-storage"] != "False":
+		on_shared_storage = False
+	else:
+		on_shared_storage = True
+
+	for server in evacuables:
+		logging.debug("Processing %s" % server)
+		if hasattr(server, 'id'):
+			response = _server_evacuate(connection, server.id, on_shared_storage)
+			if response["accepted"]:
+				logging.debug("Evacuated %s from %s: %s" %
+					      (response["uuid"], options["--plug"], response["reason"]))
+			else:
+				logging.error("Evacuation of %s on %s failed: %s" %
+					      (response["uuid"], options["--plug"], response["reason"]))
+				result = False
+		else:
+			logging.error("Could not evacuate instance: %s" % server.to_dict())
+			# Should a malformed instance result in a failed evacuation?
+			# result = False
+	return result
+
+def set_attrd_status(host, status, options):
+	logging.debug("Setting fencing status for %s to %s" % (host, status))
+	run_command(options, "attrd_updater -p -n evacuate -Q -N %s -U %s" % (host, status))
+
+def set_power_status(connection, options):
+	logging.debug("set action: " + options["--action"])
+
+	if not connection:
+		return
+
+	if options["--action"] == "off" and not _host_evacuate(options):
+		sys.exit(1)
+
+	sys.exit(0)
+
+def get_plugs_list(connection, options):
+	result = {}
+
+	if connection:
+		services = connection.services.list(binary="nova-compute")
+		for service in services:
+			longhost = service.host
+			shorthost = longhost.split('.')[0]
+			result[longhost] = ("", None)
+			result[shorthost] = ("", None)
+	return result
+
+def create_nova_connection(options):
+	nova = None
+
+	try:
+		from novaclient import client
+		from novaclient.exceptions import NotAcceptable
+	except ImportError:
+		fail_usage("Nova not found or not accessible")
+
+	versions = [ "2.11", "2" ]
+	for version in versions:
+		clientargs = inspect.getargspec(client.Client).varargs
+
+		# Some versions of Openstack prior to Ocata only
+		# supported positional arguments for username,
+		# password and tenant.
+		#
+		# Versions since Ocata only support named arguments.
+		#
+		# So we need to use introspection to figure out how to
+		# create a Nova client.
+		#
+		# Happy days
+		#
+		if clientargs:
+			# OSP < 11
+			# ArgSpec(args=['version', 'username', 'password', 'project_id', 'auth_url'],
+			#	 varargs=None,
+			#	 keywords='kwargs', defaults=(None, None, None, None))
+			nova = client.Client(version,
+					     options["--username"],
+					     options["--password"],
+					     options["--tenant-name"],
+					     options["--auth-url"],
+					     insecure=options["--insecure"],
+					     region_name=options["--region-name"],
+					     endpoint_type=options["--endpoint-type"],
+					     http_log_debug=options.has_key("--verbose"))
+		else:
+			# OSP >= 11
+			# ArgSpec(args=['version'], varargs='args', keywords='kwargs', defaults=None)
+			nova = client.Client(version,
+					     username=options["--username"],
+					     password=options["--password"],
+					     tenant_name=options["--tenant-name"],
+					     auth_url=options["--auth-url"],
+					     insecure=options["--insecure"],
+					     region_name=options["--region-name"],
+					     endpoint_type=options["--endpoint-type"],
+					     http_log_debug=options.has_key("--verbose"))
+
+		try:
+			nova.hypervisors.list()
+			return nova
+
+		except NotAcceptable as e:
+			logging.warning(e)
+
+		except Exception as e:
+			logging.warning("Nova connection failed. %s: %s" % (e.__class__.__name__, e))
+
+	logging.warning("Couldn't obtain a supported connection to nova, tried: %s\n" % repr(versions))
+	return None
+
+def define_new_opts():
+	all_opt["endpoint_type"] = {
+		"getopt" : "e:",
+		"longopt" : "endpoint-type",
+		"help" : "-e, --endpoint-type=[endpoint] Nova Endpoint type (publicURL, internalURL, adminURL)",
+		"required" : "0",
+		"shortdesc" : "Nova Endpoint type",
+		"default" : "internalURL",
+		"order": 1,
+	}
+	all_opt["tenant_name"] = {
+		"getopt" : "t:",
+		"longopt" : "tenant-name",
+		"help" : "-t, --tenant-name=[tenant]     Keystone Admin Tenant",
+		"required" : "0",
+		"shortdesc" : "Keystone Admin Tenant",
+		"default" : "",
+		"order": 1,
+	}
+	all_opt["auth_url"] = {
+		"getopt" : "k:",
+		"longopt" : "auth-url",
+		"help" : "-k, --auth-url=[url]                   Keystone Admin Auth URL",
+		"required" : "0",
+		"shortdesc" : "Keystone Admin Auth URL",
+		"default" : "",
+		"order": 1,
+	}
+	all_opt["region_name"] = {
+		"getopt" : "",
+		"longopt" : "region-name",
+		"help" : "--region-name=[region]                 Region Name",
+		"required" : "0",
+		"shortdesc" : "Region Name",
+		"default" : "",
+		"order": 1,
+	}
+	all_opt["insecure"] = {
+		"getopt" : "",
+		"longopt" : "insecure",
+		"help" : "--insecure                                     Explicitly allow agent to perform \"insecure\" TLS (https) requests",
+		"required" : "0",
+		"shortdesc" : "Allow Insecure TLS Requests",
+		"default" : "False",
+		"order": 2,
+	}
+	all_opt["domain"] = {
+		"getopt" : "d:",
+		"longopt" : "domain",
+		"help" : "-d, --domain=[string]                  DNS domain in which hosts live, useful when the cluster uses short names and nova uses FQDN",
+		"required" : "0",
+		"shortdesc" : "DNS domain in which hosts live",
+		"order": 5,
+	}
+	all_opt["instance_filtering"] = {
+		"getopt" : "",
+		"longopt" : "instance-filtering",
+		"help" : "--instance-filtering                   Allow instances created from images and flavors with evacuable=true to be evacuated (or all if no images/flavors have been tagged)",
+		"required" : "0",
+		"shortdesc" : "Allow instances to be evacuated",
+		"default" : "True",
+		"order": 5,
+	}
+	all_opt["no_shared_storage"] = {
+		"getopt" : "",
+		"longopt" : "no-shared-storage",
+		"help" : "--no-shared-storage            Disable functionality for shared storage",
+		"required" : "0",
+		"shortdesc" : "Disable functionality for dealing with shared storage",
+		"default" : "False",
+		"order": 5,
+	}
+
+def main():
+	atexit.register(atexit_handler)
+
+	device_opt = ["login", "passwd", "tenant_name", "auth_url",
+		"no_login", "no_password", "port", "domain", "no_shared_storage", "endpoint_type",
+		"instance_filtering", "insecure", "region_name"]
+	define_new_opts()
+	all_opt["shell_timeout"]["default"] = "180"
+
+	options = check_input(device_opt, process_input(device_opt))
+
+	docs = {}
+	docs["shortdesc"] = "Fence agent for the automatic resurrection of OpenStack compute instances"
+	docs["longdesc"] = "Used to reschedule flagged instances"
+	docs["vendorurl"] = ""
+
+	show_docs(options, docs)
+
+	run_delay(options)
+
+	connection = create_nova_connection(options)
+
+	# Un-evacuating a server doesn't make sense
+	if options["--action"] in ["on"]:
+		logging.error("Action %s is not supported by this agent" % (options["--action"]))
+		sys.exit(1)
+
+	if options["--action"] in ["off", "reboot"]:
+		status = get_power_status(connection, options)
+		if status != "off":
+			logging.error("Cannot resurrect instances from %s in state '%s'" % (options["--plug"], status))
+			sys.exit(1)
+
+		elif not _host_evacuate(connection, options):
+			logging.error("Resurrection of instances from %s failed" % (options["--plug"]))
+			sys.exit(1)
+
+		logging.info("Resurrection of instances from %s complete" % (options["--plug"]))
+		sys.exit(0)
+
+	result = fence_action(connection, options, set_power_status, get_power_status, get_plugs_list, None)
+	sys.exit(result)
+
+if __name__ == "__main__":
+	main()
diff -uNr a/fence/agents/compute/Makefile.am b/fence/agents/compute/Makefile.am
--- a/fence/agents/compute/Makefile.am	2017-09-27 15:01:34.844643650 +0200
+++ b/fence/agents/compute/Makefile.am	2017-09-27 15:57:50.963839738 +0200
@@ -1,14 +1,14 @@
 MAINTAINERCLEANFILES	= Makefile.in
 
-TARGET			= fence_compute
+TARGET			= fence_compute fence_evacuate
 
-SRC			= $(TARGET).py
+SRC			= $(TARGET:=.py)
 
 EXTRA_DIST		= $(SRC)
 
 sbin_SCRIPTS		= $(TARGET)
 
-man_MANS		= $(TARGET).8
+man_MANS		= $(TARGET:=.8)
 
 FENCE_TEST_ARGS		= -l test -p test -n 1
 
diff -uNr a/tests/data/metadata/fence_evacuate.xml b/tests/data/metadata/fence_evacuate.xml
--- a/tests/data/metadata/fence_evacuate.xml	1970-01-01 01:00:00.000000000 +0100
+++ b/tests/data/metadata/fence_evacuate.xml	2017-09-27 15:28:10.978063549 +0200
@@ -0,0 +1,163 @@
+<?xml version="1.0" ?>
+<resource-agent name="fence_evacuate" shortdesc="Fence agent for the automatic resurrection of OpenStack compute instances" >
+<longdesc>Used to reschedule flagged instances</longdesc>
+<vendor-url></vendor-url>
+<parameters>
+	<parameter name="tenant_name" unique="0" required="0">
+		<getopt mixed="-t, --tenant-name=[tenant]" />
+		<content type="string"  />
+		<shortdesc lang="en">Keystone Admin Tenant</shortdesc>
+	</parameter>
+	<parameter name="auth_url" unique="0" required="0">
+		<getopt mixed="-k, --auth-url=[url]" />
+		<content type="string"  />
+		<shortdesc lang="en">Keystone Admin Auth URL</shortdesc>
+	</parameter>
+	<parameter name="port" unique="0" required="1" deprecated="1">
+		<getopt mixed="-n, --plug=[id]" />
+		<content type="string"  />
+		<shortdesc lang="en">Physical plug number, name of virtual machine or UUID</shortdesc>
+	</parameter>
+	<parameter name="passwd_script" unique="0" required="0" deprecated="1">
+		<getopt mixed="-S, --password-script=[script]" />
+		<content type="string"  />
+		<shortdesc lang="en">Script to retrieve password</shortdesc>
+	</parameter>
+	<parameter name="region_name" unique="0" required="0">
+		<getopt mixed="--region-name=[region]" />
+		<content type="boolean"  />
+		<shortdesc lang="en">Region Name</shortdesc>
+	</parameter>
+	<parameter name="passwd" unique="0" required="0" deprecated="1">
+		<getopt mixed="-p, --password=[password]" />
+		<content type="string"  />
+		<shortdesc lang="en">Login password or passphrase</shortdesc>
+	</parameter>
+	<parameter name="endpoint_type" unique="0" required="0">
+		<getopt mixed="-e, --endpoint-type=[endpoint]" />
+		<content type="string" default="internalURL"  />
+		<shortdesc lang="en">Nova Endpoint type</shortdesc>
+	</parameter>
+	<parameter name="action" unique="0" required="1">
+		<getopt mixed="-o, --action=[action]" />
+		<content type="string" default="reboot"  />
+		<shortdesc lang="en">Fencing Action</shortdesc>
+	</parameter>
+	<parameter name="login" unique="0" required="0" deprecated="1">
+		<getopt mixed="-l, --username=[name]" />
+		<content type="string"  />
+		<shortdesc lang="en">Login Name</shortdesc>
+	</parameter>
+	<parameter name="plug" unique="0" required="1" obsoletes="port">
+		<getopt mixed="-n, --plug=[id]" />
+		<content type="string"  />
+		<shortdesc lang="en">Physical plug number, name of virtual machine or UUID</shortdesc>
+	</parameter>
+	<parameter name="username" unique="0" required="0" obsoletes="login">
+		<getopt mixed="-l, --username=[name]" />
+		<content type="string"  />
+		<shortdesc lang="en">Login Name</shortdesc>
+	</parameter>
+	<parameter name="password" unique="0" required="0" obsoletes="passwd">
+		<getopt mixed="-p, --password=[password]" />
+		<content type="string"  />
+		<shortdesc lang="en">Login password or passphrase</shortdesc>
+	</parameter>
+	<parameter name="password_script" unique="0" required="0" obsoletes="passwd_script">
+		<getopt mixed="-S, --password-script=[script]" />
+		<content type="string"  />
+		<shortdesc lang="en">Script to retrieve password</shortdesc>
+	</parameter>
+	<parameter name="insecure" unique="0" required="0">
+		<getopt mixed="--insecure" />
+		<content type="boolean" default="False"  />
+		<shortdesc lang="en">Allow Insecure TLS Requests</shortdesc>
+	</parameter>
+	<parameter name="domain" unique="0" required="0">
+		<getopt mixed="-d, --domain=[string]" />
+		<content type="string"  />
+		<shortdesc lang="en">DNS domain in which hosts live</shortdesc>
+	</parameter>
+	<parameter name="instance_filtering" unique="0" required="0">
+		<getopt mixed="--instance-filtering" />
+		<content type="boolean" default="True"  />
+		<shortdesc lang="en">Allow instances to be evacuated</shortdesc>
+	</parameter>
+	<parameter name="no_shared_storage" unique="0" required="0">
+		<getopt mixed="--no-shared-storage" />
+		<content type="boolean" default="False"  />
+		<shortdesc lang="en">Disable functionality for dealing with shared storage</shortdesc>
+	</parameter>
+	<parameter name="verbose" unique="0" required="0">
+		<getopt mixed="-v, --verbose" />
+		<content type="boolean"  />
+		<shortdesc lang="en">Verbose mode</shortdesc>
+	</parameter>
+	<parameter name="debug" unique="0" required="0" deprecated="1">
+		<getopt mixed="-D, --debug-file=[debugfile]" />
+		<content type="string"  />
+		<shortdesc lang="en">Write debug information to given file</shortdesc>
+	</parameter>
+	<parameter name="debug_file" unique="0" required="0" obsoletes="debug">
+		<getopt mixed="-D, --debug-file=[debugfile]" />
+		<content type="string"  />
+		<shortdesc lang="en">Write debug information to given file</shortdesc>
+	</parameter>
+	<parameter name="version" unique="0" required="0">
+		<getopt mixed="-V, --version" />
+		<content type="boolean"  />
+		<shortdesc lang="en">Display version information and exit</shortdesc>
+	</parameter>
+	<parameter name="help" unique="0" required="0">
+		<getopt mixed="-h, --help" />
+		<content type="boolean"  />
+		<shortdesc lang="en">Display help and exit</shortdesc>
+	</parameter>
+	<parameter name="separator" unique="0" required="0">
+		<getopt mixed="-C, --separator=[char]" />
+		<content type="string" default=","  />
+		<shortdesc lang="en">Separator for CSV created by operation list</shortdesc>
+	</parameter>
+	<parameter name="power_wait" unique="0" required="0">
+		<getopt mixed="--power-wait=[seconds]" />
+		<content type="second" default="0"  />
+		<shortdesc lang="en">Wait X seconds after issuing ON/OFF</shortdesc>
+	</parameter>
+	<parameter name="login_timeout" unique="0" required="0">
+		<getopt mixed="--login-timeout=[seconds]" />
+		<content type="second" default="5"  />
+		<shortdesc lang="en">Wait X seconds for cmd prompt after login</shortdesc>
+	</parameter>
+	<parameter name="delay" unique="0" required="0">
+		<getopt mixed="--delay=[seconds]" />
+		<content type="second" default="0"  />
+		<shortdesc lang="en">Wait X seconds before fencing is started</shortdesc>
+	</parameter>
+	<parameter name="power_timeout" unique="0" required="0">
+		<getopt mixed="--power-timeout=[seconds]" />
+		<content type="second" default="20"  />
+		<shortdesc lang="en">Test X seconds for status change after ON/OFF</shortdesc>
+	</parameter>
+	<parameter name="shell_timeout" unique="0" required="0">
+		<getopt mixed="--shell-timeout=[seconds]" />
+		<content type="second" default="180"  />
+		<shortdesc lang="en">Wait X seconds for cmd prompt after issuing command</shortdesc>
+	</parameter>
+	<parameter name="retry_on" unique="0" required="0">
+		<getopt mixed="--retry-on=[attempts]" />
+		<content type="integer" default="1"  />
+		<shortdesc lang="en">Count of attempts to retry power on</shortdesc>
+	</parameter>
+</parameters>
+<actions>
+	<action name="on" automatic="0"/>
+	<action name="off" />
+	<action name="reboot" />
+	<action name="status" />
+	<action name="list" />
+	<action name="list-status" />
+	<action name="monitor" />
+	<action name="metadata" />
+	<action name="validate-all" />
+</actions>
+</resource-agent>