Blame SOURCES/bz2042496-fence_ibm_vpc-fence_ibm_powervs.patch

d4ebfc
From 3078e4d55d3bad2bbf9309785fdb2b53afac8d65 Mon Sep 17 00:00:00 2001
d4ebfc
From: Oyvind Albrigtsen <oalbrigt@redhat.com>
d4ebfc
Date: Tue, 13 Jul 2021 13:39:33 +0200
d4ebfc
Subject: [PATCH] fence_ibm_vpc/fence_ibm_powervs: new fence agents
d4ebfc
d4ebfc
---
d4ebfc
 agents/ibm_powervs/fence_ibm_powervs.py   | 202 +++++++++++++++++++
d4ebfc
 agents/ibm_vpc/fence_ibm_vpc.py           | 230 ++++++++++++++++++++++
d4ebfc
 tests/data/metadata/fence_ibm_powervs.xml | 134 +++++++++++++
d4ebfc
 tests/data/metadata/fence_ibm_vpc.xml     | 134 +++++++++++++
d4ebfc
 5 files changed, 724 insertions(+)
d4ebfc
 create mode 100755 agents/ibm_powervs/fence_ibm_powervs.py
d4ebfc
 create mode 100755 agents/ibm_vpc/fence_ibm_vpc.py
d4ebfc
 create mode 100644 tests/data/metadata/fence_ibm_powervs.xml
d4ebfc
 create mode 100644 tests/data/metadata/fence_ibm_vpc.xml
d4ebfc
d4ebfc
diff --git a/agents/ibm_powervs/fence_ibm_powervs.py b/agents/ibm_powervs/fence_ibm_powervs.py
d4ebfc
new file mode 100755
d4ebfc
index 000000000..6649771ea
d4ebfc
--- /dev/null
d4ebfc
+++ b/agents/ibm_powervs/fence_ibm_powervs.py
d4ebfc
@@ -0,0 +1,202 @@
d4ebfc
+#!@PYTHON@ -tt
d4ebfc
+
d4ebfc
+import sys
d4ebfc
+import pycurl, io, json
d4ebfc
+import logging
d4ebfc
+import atexit
d4ebfc
+sys.path.append("@FENCEAGENTSLIBDIR@")
d4ebfc
+from fencing import *
d4ebfc
+from fencing import fail, run_delay, EC_LOGIN_DENIED, EC_STATUS
d4ebfc
+
d4ebfc
+state = {
d4ebfc
+	 "ACTIVE": "on",
d4ebfc
+	 "SHUTOFF": "off",
d4ebfc
+	 "ERROR": "unknown"
d4ebfc
+}
d4ebfc
+
d4ebfc
+def get_list(conn, options):
d4ebfc
+	outlets = {}
d4ebfc
+
d4ebfc
+	try:
d4ebfc
+		command = "cloud-instances/{}/pvm-instances".format(options["--instance"])
d4ebfc
+		res = send_command(conn, command)
d4ebfc
+	except Exception as e:
d4ebfc
+		logging.debug("Failed: {}".format(e))
d4ebfc
+		return outlets
d4ebfc
+
d4ebfc
+	for r in res["pvmInstances"]:
d4ebfc
+		if "--verbose" in options:
d4ebfc
+			logging.debug(json.dumps(r, indent=2))
d4ebfc
+		outlets[r["pvmInstanceID"]] = (r["serverName"], state[r["status"]])
d4ebfc
+
d4ebfc
+	return outlets
d4ebfc
+
d4ebfc
+def get_power_status(conn, options):
d4ebfc
+	try:
d4ebfc
+		command = "cloud-instances/{}/pvm-instances/{}".format(
d4ebfc
+				options["--instance"], options["--plug"])
d4ebfc
+		res = send_command(conn, command)
d4ebfc
+		result = get_list(conn, options)[options["--plug"]][1]
d4ebfc
+	except KeyError as e:
d4ebfc
+		logging.debug("Failed: Unable to get status for {}".format(e))
d4ebfc
+		fail(EC_STATUS)
d4ebfc
+
d4ebfc
+	return result
d4ebfc
+
d4ebfc
+def set_power_status(conn, options):
d4ebfc
+	action = {
d4ebfc
+		"on" :  '{"action" : "start"}',
d4ebfc
+		"off" : '{"action" : "immediate-shutdown"}',
d4ebfc
+	}[options["--action"]]
d4ebfc
+
d4ebfc
+	try:
d4ebfc
+		send_command(conn, "cloud-instances/{}/pvm-instances/{}/action".format(
d4ebfc
+				options["--instance"], options["--plug"]), "POST", action)
d4ebfc
+	except Exception as e:
d4ebfc
+		logging.debug("Failed: Unable to set power to {} for {}".format(options["--action"], e))
d4ebfc
+		fail(EC_STATUS)
d4ebfc
+
d4ebfc
+def connect(opt):
d4ebfc
+	conn = pycurl.Curl()
d4ebfc
+
d4ebfc
+	## setup correct URL
d4ebfc
+	conn.base_url = "https://" + opt["--region"] + ".power-iaas.cloud.ibm.com/pcloud/v1/"
d4ebfc
+
d4ebfc
+	if opt["--verbose-level"] > 1:
d4ebfc
+		conn.setopt(pycurl.VERBOSE, 1)
d4ebfc
+
d4ebfc
+	conn.setopt(pycurl.TIMEOUT, int(opt["--shell-timeout"]))
d4ebfc
+	conn.setopt(pycurl.SSL_VERIFYPEER, 1)
d4ebfc
+	conn.setopt(pycurl.SSL_VERIFYHOST, 2)
d4ebfc
+
d4ebfc
+	# set auth token for later requests
d4ebfc
+	conn.setopt(pycurl.HTTPHEADER, [
d4ebfc
+		"Content-Type: application/json",
d4ebfc
+		"Authorization: Bearer {}".format(opt["--token"]),
d4ebfc
+		"CRN: {}".format(opt["--crn"]),
d4ebfc
+		"User-Agent: curl",
d4ebfc
+	])
d4ebfc
+
d4ebfc
+	return conn
d4ebfc
+
d4ebfc
+def disconnect(conn):
d4ebfc
+	conn.close()
d4ebfc
+
d4ebfc
+def send_command(conn, command, method="GET", action=None):
d4ebfc
+	url = conn.base_url + command
d4ebfc
+
d4ebfc
+	conn.setopt(pycurl.URL, url.encode("ascii"))
d4ebfc
+
d4ebfc
+	web_buffer = io.BytesIO()
d4ebfc
+
d4ebfc
+	if method == "GET":
d4ebfc
+		conn.setopt(pycurl.POST, 0)
d4ebfc
+	if method == "POST":
d4ebfc
+		conn.setopt(pycurl.POSTFIELDS, action)
d4ebfc
+	if method == "DELETE":
d4ebfc
+		conn.setopt(pycurl.CUSTOMREQUEST, "DELETE")
d4ebfc
+
d4ebfc
+	conn.setopt(pycurl.WRITEFUNCTION, web_buffer.write)
d4ebfc
+
d4ebfc
+	try:
d4ebfc
+		conn.perform()
d4ebfc
+	except Exception as e:
d4ebfc
+		raise(e)
d4ebfc
+
d4ebfc
+	rc = conn.getinfo(pycurl.HTTP_CODE)
d4ebfc
+	result = web_buffer.getvalue().decode("UTF-8")
d4ebfc
+
d4ebfc
+	web_buffer.close()
d4ebfc
+
d4ebfc
+	if rc != 200:
d4ebfc
+		if len(result) > 0:
d4ebfc
+			raise Exception("{}: {}".format(rc,
d4ebfc
+					result["value"]["messages"][0]["default_message"]))
d4ebfc
+		else:
d4ebfc
+			raise Exception("Remote returned {} for request to {}".format(rc, url))
d4ebfc
+
d4ebfc
+	if len(result) > 0:
d4ebfc
+		result = json.loads(result)
d4ebfc
+
d4ebfc
+	logging.debug("url: {}".format(url))
d4ebfc
+	logging.debug("method: {}".format(method))
d4ebfc
+	logging.debug("response code: {}".format(rc))
d4ebfc
+	logging.debug("result: {}\n".format(result))
d4ebfc
+
d4ebfc
+	return result
d4ebfc
+
d4ebfc
+def define_new_opts():
d4ebfc
+	all_opt["token"] = {
d4ebfc
+		"getopt" : ":",
d4ebfc
+		"longopt" : "token",
d4ebfc
+		"help" : "--token=[token]                Bearer Token",
d4ebfc
+		"required" : "1",
d4ebfc
+		"shortdesc" : "Bearer Token",
d4ebfc
+		"order" : 0
d4ebfc
+	}
d4ebfc
+	all_opt["crn"] = {
d4ebfc
+		"getopt" : ":",
d4ebfc
+		"longopt" : "crn",
d4ebfc
+		"help" : "--crn=[crn]                    CRN",
d4ebfc
+		"required" : "1",
d4ebfc
+		"shortdesc" : "CRN",
d4ebfc
+		"order" : 0
d4ebfc
+	}
d4ebfc
+	all_opt["instance"] = {
d4ebfc
+		"getopt" : ":",
d4ebfc
+		"longopt" : "instance",
d4ebfc
+		"help" : "--instance=[instance]          PowerVS Instance",
d4ebfc
+		"required" : "1",
d4ebfc
+		"shortdesc" : "PowerVS Instance",
d4ebfc
+		"order" : 0
d4ebfc
+	}
d4ebfc
+	all_opt["region"] = {
d4ebfc
+		"getopt" : ":",
d4ebfc
+		"longopt" : "region",
d4ebfc
+		"help" : "--region=[region]              Region",
d4ebfc
+		"required" : "1",
d4ebfc
+		"shortdesc" : "Region",
d4ebfc
+		"order" : 0
d4ebfc
+	}
d4ebfc
+
d4ebfc
+
d4ebfc
+def main():
d4ebfc
+	device_opt = [
d4ebfc
+		"token",
d4ebfc
+		"crn",
d4ebfc
+		"instance",
d4ebfc
+		"region",
d4ebfc
+		"port",
d4ebfc
+		"no_password",
d4ebfc
+	]
d4ebfc
+
d4ebfc
+	atexit.register(atexit_handler)
d4ebfc
+	define_new_opts()
d4ebfc
+
d4ebfc
+	all_opt["shell_timeout"]["default"] = "15"
d4ebfc
+	all_opt["power_timeout"]["default"] = "30"
d4ebfc
+	all_opt["power_wait"]["default"] = "1"
d4ebfc
+
d4ebfc
+	options = check_input(device_opt, process_input(device_opt))
d4ebfc
+
d4ebfc
+	docs = {}
d4ebfc
+	docs["shortdesc"] = "Fence agent for IBM PowerVS"
d4ebfc
+	docs["longdesc"] = """fence_ibm_powervs is an I/O Fencing agent which can be \
d4ebfc
+used with IBM PowerVS to fence virtual machines."""
d4ebfc
+	docs["vendorurl"] = "https://www.ibm.com"
d4ebfc
+	show_docs(options, docs)
d4ebfc
+
d4ebfc
+	####
d4ebfc
+	## Fence operations
d4ebfc
+	####
d4ebfc
+	run_delay(options)
d4ebfc
+
d4ebfc
+	conn = connect(options)
d4ebfc
+	atexit.register(disconnect, conn)
d4ebfc
+
d4ebfc
+	result = fence_action(conn, options, set_power_status, get_power_status, get_list)
d4ebfc
+
d4ebfc
+	sys.exit(result)
d4ebfc
+
d4ebfc
+if __name__ == "__main__":
d4ebfc
+	main()
d4ebfc
diff --git a/agents/ibm_vpc/fence_ibm_vpc.py b/agents/ibm_vpc/fence_ibm_vpc.py
d4ebfc
new file mode 100755
d4ebfc
index 000000000..9f84f7b2d
d4ebfc
--- /dev/null
d4ebfc
+++ b/agents/ibm_vpc/fence_ibm_vpc.py
d4ebfc
@@ -0,0 +1,230 @@
d4ebfc
+#!@PYTHON@ -tt
d4ebfc
+
d4ebfc
+import sys
d4ebfc
+import pycurl, io, json
d4ebfc
+import logging
d4ebfc
+import atexit
d4ebfc
+sys.path.append("@FENCEAGENTSLIBDIR@")
d4ebfc
+from fencing import *
d4ebfc
+from fencing import fail, run_delay, EC_LOGIN_DENIED, EC_STATUS
d4ebfc
+
d4ebfc
+state = {
d4ebfc
+	 "running": "on",
d4ebfc
+	 "stopped": "off",
d4ebfc
+	 "starting": "unknown",
d4ebfc
+	 "stopping": "unknown",
d4ebfc
+	 "restarting": "unknown",
d4ebfc
+	 "pending": "unknown",
d4ebfc
+}
d4ebfc
+
d4ebfc
+def get_list(conn, options):
d4ebfc
+	outlets = {}
d4ebfc
+
d4ebfc
+	try:
d4ebfc
+		command = "instances?version=2021-05-25&generation=2&limit={}".format(options["--limit"])
d4ebfc
+		res = send_command(conn, command)
d4ebfc
+	except Exception as e:
d4ebfc
+		logging.debug("Failed: Unable to get list: {}".format(e))
d4ebfc
+		return outlets
d4ebfc
+
d4ebfc
+	for r in res["instances"]:
d4ebfc
+		if options["--verbose-level"] > 1:
d4ebfc
+			logging.debug("Node:\n{}".format(json.dumps(r, indent=2)))
d4ebfc
+			logging.debug("Status: " + state[r["status"]])
d4ebfc
+		outlets[r["id"]] = (r["name"], state[r["status"]])
d4ebfc
+
d4ebfc
+	return outlets
d4ebfc
+
d4ebfc
+def get_power_status(conn, options):
d4ebfc
+	try:
d4ebfc
+		command = "instances/{}?version=2021-05-25&generation=2".format(options["--plug"])
d4ebfc
+		res = send_command(conn, command)
d4ebfc
+		result = state[res["status"]]
d4ebfc
+		if options["--verbose-level"] > 1:
d4ebfc
+			logging.debug("Result:\n{}".format(json.dumps(res, indent=2)))
d4ebfc
+			logging.debug("Status: " + result)
d4ebfc
+	except Exception as e:
d4ebfc
+		logging.debug("Failed: Unable to get status for {}: {}".format(options["--plug"], e))
d4ebfc
+		fail(EC_STATUS)
d4ebfc
+
d4ebfc
+	return result
d4ebfc
+
d4ebfc
+def set_power_status(conn, options):
d4ebfc
+	action = {
d4ebfc
+		"on" :  '{"type" : "start"}',
d4ebfc
+		"off" : '{"type" : "stop"}',
d4ebfc
+	}[options["--action"]]
d4ebfc
+
d4ebfc
+	try:
d4ebfc
+		command = "instances/{}/actions?version=2021-05-25&generation=2".format(options["--plug"])
d4ebfc
+		send_command(conn, command, "POST", action, 201)
d4ebfc
+	except Exception as e:
d4ebfc
+		logging.debug("Failed: Unable to set power to {} for {}".format(options["--action"], e))
d4ebfc
+		fail(EC_STATUS)
d4ebfc
+
d4ebfc
+def get_bearer_token(conn, options):
d4ebfc
+	token = None
d4ebfc
+	try:
d4ebfc
+		conn.setopt(pycurl.HTTPHEADER, [
d4ebfc
+			"Content-Type: application/x-www-form-urlencoded",
d4ebfc
+			"User-Agent: curl",
d4ebfc
+		])
d4ebfc
+		token = send_command(conn, "https://iam.cloud.ibm.com/identity/token", "POST", "grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey={}".format(options["--apikey"]))["access_token"]
d4ebfc
+	except Exception:
d4ebfc
+		logging.error("Failed: Unable to authenticate")
d4ebfc
+		fail(EC_LOGIN_DENIED)
d4ebfc
+
d4ebfc
+	return token
d4ebfc
+
d4ebfc
+def connect(opt):
d4ebfc
+	conn = pycurl.Curl()
d4ebfc
+
d4ebfc
+	## setup correct URL
d4ebfc
+	conn.base_url = "https://" + opt["--region"] + ".iaas.cloud.ibm.com/v1/"
d4ebfc
+
d4ebfc
+	if opt["--verbose-level"] > 1:
d4ebfc
+		conn.setopt(pycurl.VERBOSE, 1)
d4ebfc
+
d4ebfc
+	conn.setopt(pycurl.TIMEOUT, int(opt["--shell-timeout"]))
d4ebfc
+	conn.setopt(pycurl.SSL_VERIFYPEER, 1)
d4ebfc
+	conn.setopt(pycurl.SSL_VERIFYHOST, 2)
d4ebfc
+
d4ebfc
+	# get bearer token
d4ebfc
+	bearer_token = get_bearer_token(conn, opt)
d4ebfc
+
d4ebfc
+	# set auth token for later requests
d4ebfc
+	conn.setopt(pycurl.HTTPHEADER, [
d4ebfc
+		"Content-Type: application/json",
d4ebfc
+		"Authorization: Bearer {}".format(bearer_token),
d4ebfc
+		"User-Agent: curl",
d4ebfc
+	])
d4ebfc
+
d4ebfc
+	return conn
d4ebfc
+
d4ebfc
+def disconnect(conn):
d4ebfc
+	conn.close()
d4ebfc
+
d4ebfc
+def send_command(conn, command, method="GET", action=None, expected_rc=200):
d4ebfc
+	if not command.startswith("https"):
d4ebfc
+		url = conn.base_url + command
d4ebfc
+	else:
d4ebfc
+		url = command
d4ebfc
+
d4ebfc
+	conn.setopt(pycurl.URL, url.encode("ascii"))
d4ebfc
+
d4ebfc
+	web_buffer = io.BytesIO()
d4ebfc
+
d4ebfc
+	if method == "GET":
d4ebfc
+		conn.setopt(pycurl.POST, 0)
d4ebfc
+	if method == "POST":
d4ebfc
+		conn.setopt(pycurl.POSTFIELDS, action)
d4ebfc
+	if method == "DELETE":
d4ebfc
+		conn.setopt(pycurl.CUSTOMREQUEST, "DELETE")
d4ebfc
+
d4ebfc
+	conn.setopt(pycurl.WRITEFUNCTION, web_buffer.write)
d4ebfc
+
d4ebfc
+	try:
d4ebfc
+		conn.perform()
d4ebfc
+	except Exception as e:
d4ebfc
+		raise(e)
d4ebfc
+
d4ebfc
+	rc = conn.getinfo(pycurl.HTTP_CODE)
d4ebfc
+	result = web_buffer.getvalue().decode("UTF-8")
d4ebfc
+
d4ebfc
+	web_buffer.close()
d4ebfc
+
d4ebfc
+	# actions (start/stop/reboot) report 201 when they've been created
d4ebfc
+	if rc != expected_rc:
d4ebfc
+		logging.debug("rc: {}, result: {}".format(rc, result))
d4ebfc
+		if len(result) > 0:
d4ebfc
+			raise Exception("{}: {}".format(rc, 
d4ebfc
+					result["value"]["messages"][0]["default_message"]))
d4ebfc
+		else:
d4ebfc
+			raise Exception("Remote returned {} for request to {}".format(rc, url))
d4ebfc
+
d4ebfc
+	if len(result) > 0:
d4ebfc
+		result = json.loads(result)
d4ebfc
+
d4ebfc
+	logging.debug("url: {}".format(url))
d4ebfc
+	logging.debug("method: {}".format(method))
d4ebfc
+	logging.debug("response code: {}".format(rc))
d4ebfc
+	logging.debug("result: {}\n".format(result))
d4ebfc
+
d4ebfc
+	return result
d4ebfc
+
d4ebfc
+def define_new_opts():
d4ebfc
+	all_opt["apikey"] = {
d4ebfc
+		"getopt" : ":",
d4ebfc
+		"longopt" : "apikey",
d4ebfc
+		"help" : "--apikey=[key]                 API Key",
d4ebfc
+		"required" : "1",
d4ebfc
+		"shortdesc" : "API Key",
d4ebfc
+		"order" : 0
d4ebfc
+	}
d4ebfc
+	all_opt["instance"] = {
d4ebfc
+		"getopt" : ":",
d4ebfc
+		"longopt" : "instance",
d4ebfc
+		"help" : "--instance=[instance]          Cloud Instance",
d4ebfc
+		"required" : "1",
d4ebfc
+		"shortdesc" : "Cloud Instance",
d4ebfc
+		"order" : 0
d4ebfc
+	}
d4ebfc
+	all_opt["region"] = {
d4ebfc
+		"getopt" : ":",
d4ebfc
+		"longopt" : "region",
d4ebfc
+		"help" : "--region=[region]              Region",
d4ebfc
+		"required" : "1",
d4ebfc
+		"shortdesc" : "Region",
d4ebfc
+		"order" : 0
d4ebfc
+	}
d4ebfc
+	all_opt["limit"] = {
d4ebfc
+		"getopt" : ":",
d4ebfc
+		"longopt" : "limit",
d4ebfc
+		"help" : "--limit=[number]               Limit number of nodes returned by API",
d4ebfc
+		"required" : "1",
d4ebfc
+		"default": 50,
d4ebfc
+		"shortdesc" : "Number of nodes returned by API",
d4ebfc
+		"order" : 0
d4ebfc
+	}
d4ebfc
+
d4ebfc
+
d4ebfc
+def main():
d4ebfc
+	device_opt = [
d4ebfc
+		"apikey",
d4ebfc
+		"instance",
d4ebfc
+		"region",
d4ebfc
+		"limit",
d4ebfc
+		"port",
d4ebfc
+		"no_password",
d4ebfc
+	]
d4ebfc
+
d4ebfc
+	atexit.register(atexit_handler)
d4ebfc
+	define_new_opts()
d4ebfc
+
d4ebfc
+	all_opt["shell_timeout"]["default"] = "15"
d4ebfc
+	all_opt["power_timeout"]["default"] = "30"
d4ebfc
+	all_opt["power_wait"]["default"] = "1"
d4ebfc
+
d4ebfc
+	options = check_input(device_opt, process_input(device_opt))
d4ebfc
+
d4ebfc
+	docs = {}
d4ebfc
+	docs["shortdesc"] = "Fence agent for IBM Cloud VPC"
d4ebfc
+	docs["longdesc"] = """fence_ibm_vpc is an I/O Fencing agent which can be \
d4ebfc
+used with IBM Cloud VPC to fence virtual machines."""
d4ebfc
+	docs["vendorurl"] = "https://www.ibm.com"
d4ebfc
+	show_docs(options, docs)
d4ebfc
+
d4ebfc
+	####
d4ebfc
+	## Fence operations
d4ebfc
+	####
d4ebfc
+	run_delay(options)
d4ebfc
+
d4ebfc
+	conn = connect(options)
d4ebfc
+	atexit.register(disconnect, conn)
d4ebfc
+
d4ebfc
+	result = fence_action(conn, options, set_power_status, get_power_status, get_list)
d4ebfc
+
d4ebfc
+	sys.exit(result)
d4ebfc
+
d4ebfc
+if __name__ == "__main__":
d4ebfc
+	main()
d4ebfc
diff --git a/tests/data/metadata/fence_ibm_powervs.xml b/tests/data/metadata/fence_ibm_powervs.xml
d4ebfc
new file mode 100644
d4ebfc
index 000000000..fe86331bd
d4ebfc
--- /dev/null
d4ebfc
+++ b/tests/data/metadata/fence_ibm_powervs.xml
d4ebfc
@@ -0,0 +1,134 @@
d4ebfc
+
d4ebfc
+<resource-agent name="fence_ibm_powervs" shortdesc="Fence agent for IBM PowerVS" >
d4ebfc
+<longdesc>fence_ibm_powervs is an I/O Fencing agent which can be used with IBM PowerVS to fence virtual machines.</longdesc>
d4ebfc
+<vendor-url>https://www.ibm.com</vendor-url>
d4ebfc
+<parameters>
d4ebfc
+	<parameter name="crn" unique="0" required="1">
d4ebfc
+		<getopt mixed="--crn=[crn]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">CRN</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="instance" unique="0" required="1">
d4ebfc
+		<getopt mixed="--instance=[instance]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">PowerVS Instance</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="region" unique="0" required="1">
d4ebfc
+		<getopt mixed="--region=[region]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Region</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="token" unique="0" required="1">
d4ebfc
+		<getopt mixed="--token=[token]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Bearer Token</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="action" unique="0" required="1">
d4ebfc
+		<getopt mixed="-o, --action=[action]" />
d4ebfc
+		<content type="string" default="reboot"  />
d4ebfc
+		<shortdesc lang="en">Fencing action</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="plug" unique="0" required="1" obsoletes="port">
d4ebfc
+		<getopt mixed="-n, --plug=[id]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Physical plug number on device, UUID or identification of machine</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="port" unique="0" required="1" deprecated="1">
d4ebfc
+		<getopt mixed="-n, --plug=[id]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Physical plug number on device, UUID or identification of machine</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="quiet" unique="0" required="0">
d4ebfc
+		<getopt mixed="-q, --quiet" />
d4ebfc
+		<content type="boolean"  />
d4ebfc
+		<shortdesc lang="en">Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog.</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="verbose" unique="0" required="0">
d4ebfc
+		<getopt mixed="-v, --verbose" />
d4ebfc
+		<content type="boolean"  />
d4ebfc
+		<shortdesc lang="en">Verbose mode. Multiple -v flags can be stacked on the command line (e.g., -vvv) to increase verbosity.</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="verbose_level" unique="0" required="0">
d4ebfc
+		<getopt mixed="--verbose-level" />
d4ebfc
+		<content type="integer"  />
d4ebfc
+		<shortdesc lang="en">Level of debugging detail in output. Defaults to the number of --verbose flags specified on the command line, or to 1 if verbose=1 in a stonith device configuration (i.e., on stdin).</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="debug" unique="0" required="0" deprecated="1">
d4ebfc
+		<getopt mixed="-D, --debug-file=[debugfile]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Write debug information to given file</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="debug_file" unique="0" required="0" obsoletes="debug">
d4ebfc
+		<getopt mixed="-D, --debug-file=[debugfile]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Write debug information to given file</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="version" unique="0" required="0">
d4ebfc
+		<getopt mixed="-V, --version" />
d4ebfc
+		<content type="boolean"  />
d4ebfc
+		<shortdesc lang="en">Display version information and exit</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="help" unique="0" required="0">
d4ebfc
+		<getopt mixed="-h, --help" />
d4ebfc
+		<content type="boolean"  />
d4ebfc
+		<shortdesc lang="en">Display help and exit</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="separator" unique="0" required="0">
d4ebfc
+		<getopt mixed="-C, --separator=[char]" />
d4ebfc
+		<content type="string" default=","  />
d4ebfc
+		<shortdesc lang="en">Separator for CSV created by 'list' operation</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="delay" unique="0" required="0">
d4ebfc
+		<getopt mixed="--delay=[seconds]" />
d4ebfc
+		<content type="second" default="0"  />
d4ebfc
+		<shortdesc lang="en">Wait X seconds before fencing is started</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="disable_timeout" unique="0" required="0">
d4ebfc
+		<getopt mixed="--disable-timeout=[true/false]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Disable timeout (true/false) (default: true when run from Pacemaker 2.0+)</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="login_timeout" unique="0" required="0">
d4ebfc
+		<getopt mixed="--login-timeout=[seconds]" />
d4ebfc
+		<content type="second" default="5"  />
d4ebfc
+		<shortdesc lang="en">Wait X seconds for cmd prompt after login</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="power_timeout" unique="0" required="0">
d4ebfc
+		<getopt mixed="--power-timeout=[seconds]" />
d4ebfc
+		<content type="second" default="30"  />
d4ebfc
+		<shortdesc lang="en">Test X seconds for status change after ON/OFF</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="power_wait" unique="0" required="0">
d4ebfc
+		<getopt mixed="--power-wait=[seconds]" />
d4ebfc
+		<content type="second" default="1"  />
d4ebfc
+		<shortdesc lang="en">Wait X seconds after issuing ON/OFF</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="shell_timeout" unique="0" required="0">
d4ebfc
+		<getopt mixed="--shell-timeout=[seconds]" />
d4ebfc
+		<content type="second" default="15"  />
d4ebfc
+		<shortdesc lang="en">Wait X seconds for cmd prompt after issuing command</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="stonith_status_sleep" unique="0" required="0">
d4ebfc
+		<getopt mixed="--stonith-status-sleep=[seconds]" />
d4ebfc
+		<content type="second" default="1"  />
d4ebfc
+		<shortdesc lang="en">Sleep X seconds between status calls during a STONITH action</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="retry_on" unique="0" required="0">
d4ebfc
+		<getopt mixed="--retry-on=[attempts]" />
d4ebfc
+		<content type="integer" default="1"  />
d4ebfc
+		<shortdesc lang="en">Count of attempts to retry power on</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+</parameters>
d4ebfc
+<actions>
d4ebfc
+	<action name="on" automatic="0"/>
d4ebfc
+	<action name="off" />
d4ebfc
+	<action name="reboot" />
d4ebfc
+	<action name="status" />
d4ebfc
+	<action name="list" />
d4ebfc
+	<action name="list-status" />
d4ebfc
+	<action name="monitor" />
d4ebfc
+	<action name="metadata" />
d4ebfc
+	<action name="manpage" />
d4ebfc
+	<action name="validate-all" />
d4ebfc
+</actions>
d4ebfc
+</resource-agent>
d4ebfc
diff --git a/tests/data/metadata/fence_ibm_vpc.xml b/tests/data/metadata/fence_ibm_vpc.xml
d4ebfc
new file mode 100644
d4ebfc
index 000000000..926efcaa0
d4ebfc
--- /dev/null
d4ebfc
+++ b/tests/data/metadata/fence_ibm_vpc.xml
d4ebfc
@@ -0,0 +1,134 @@
d4ebfc
+
d4ebfc
+<resource-agent name="fence_ibm_vpc" shortdesc="Fence agent for IBM Cloud VPC" >
d4ebfc
+<longdesc>fence_ibm_vpc is an I/O Fencing agent which can be used with IBM Cloud VPC to fence virtual machines.</longdesc>
d4ebfc
+<vendor-url>https://www.ibm.com</vendor-url>
d4ebfc
+<parameters>
d4ebfc
+	<parameter name="apikey" unique="0" required="1">
d4ebfc
+		<getopt mixed="--apikey=[key]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">API Key</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="instance" unique="0" required="1">
d4ebfc
+		<getopt mixed="--instance=[instance]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Cloud Instance</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="limit" unique="0" required="1">
d4ebfc
+		<getopt mixed="--limit=[number]" />
d4ebfc
+		<content type="string" default="50"  />
d4ebfc
+		<shortdesc lang="en">Number of nodes returned by API</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="region" unique="0" required="1">
d4ebfc
+		<getopt mixed="--region=[region]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Region</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="action" unique="0" required="1">
d4ebfc
+		<getopt mixed="-o, --action=[action]" />
d4ebfc
+		<content type="string" default="reboot"  />
d4ebfc
+		<shortdesc lang="en">Fencing action</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="plug" unique="0" required="1" obsoletes="port">
d4ebfc
+		<getopt mixed="-n, --plug=[id]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Physical plug number on device, UUID or identification of machine</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="port" unique="0" required="1" deprecated="1">
d4ebfc
+		<getopt mixed="-n, --plug=[id]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Physical plug number on device, UUID or identification of machine</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="quiet" unique="0" required="0">
d4ebfc
+		<getopt mixed="-q, --quiet" />
d4ebfc
+		<content type="boolean"  />
d4ebfc
+		<shortdesc lang="en">Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog.</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="verbose" unique="0" required="0">
d4ebfc
+		<getopt mixed="-v, --verbose" />
d4ebfc
+		<content type="boolean"  />
d4ebfc
+		<shortdesc lang="en">Verbose mode. Multiple -v flags can be stacked on the command line (e.g., -vvv) to increase verbosity.</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="verbose_level" unique="0" required="0">
d4ebfc
+		<getopt mixed="--verbose-level" />
d4ebfc
+		<content type="integer"  />
d4ebfc
+		<shortdesc lang="en">Level of debugging detail in output. Defaults to the number of --verbose flags specified on the command line, or to 1 if verbose=1 in a stonith device configuration (i.e., on stdin).</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="debug" unique="0" required="0" deprecated="1">
d4ebfc
+		<getopt mixed="-D, --debug-file=[debugfile]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Write debug information to given file</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="debug_file" unique="0" required="0" obsoletes="debug">
d4ebfc
+		<getopt mixed="-D, --debug-file=[debugfile]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Write debug information to given file</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="version" unique="0" required="0">
d4ebfc
+		<getopt mixed="-V, --version" />
d4ebfc
+		<content type="boolean"  />
d4ebfc
+		<shortdesc lang="en">Display version information and exit</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="help" unique="0" required="0">
d4ebfc
+		<getopt mixed="-h, --help" />
d4ebfc
+		<content type="boolean"  />
d4ebfc
+		<shortdesc lang="en">Display help and exit</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="separator" unique="0" required="0">
d4ebfc
+		<getopt mixed="-C, --separator=[char]" />
d4ebfc
+		<content type="string" default=","  />
d4ebfc
+		<shortdesc lang="en">Separator for CSV created by 'list' operation</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="delay" unique="0" required="0">
d4ebfc
+		<getopt mixed="--delay=[seconds]" />
d4ebfc
+		<content type="second" default="0"  />
d4ebfc
+		<shortdesc lang="en">Wait X seconds before fencing is started</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="disable_timeout" unique="0" required="0">
d4ebfc
+		<getopt mixed="--disable-timeout=[true/false]" />
d4ebfc
+		<content type="string"  />
d4ebfc
+		<shortdesc lang="en">Disable timeout (true/false) (default: true when run from Pacemaker 2.0+)</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="login_timeout" unique="0" required="0">
d4ebfc
+		<getopt mixed="--login-timeout=[seconds]" />
d4ebfc
+		<content type="second" default="5"  />
d4ebfc
+		<shortdesc lang="en">Wait X seconds for cmd prompt after login</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="power_timeout" unique="0" required="0">
d4ebfc
+		<getopt mixed="--power-timeout=[seconds]" />
d4ebfc
+		<content type="second" default="30"  />
d4ebfc
+		<shortdesc lang="en">Test X seconds for status change after ON/OFF</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="power_wait" unique="0" required="0">
d4ebfc
+		<getopt mixed="--power-wait=[seconds]" />
d4ebfc
+		<content type="second" default="1"  />
d4ebfc
+		<shortdesc lang="en">Wait X seconds after issuing ON/OFF</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="shell_timeout" unique="0" required="0">
d4ebfc
+		<getopt mixed="--shell-timeout=[seconds]" />
d4ebfc
+		<content type="second" default="15"  />
d4ebfc
+		<shortdesc lang="en">Wait X seconds for cmd prompt after issuing command</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="stonith_status_sleep" unique="0" required="0">
d4ebfc
+		<getopt mixed="--stonith-status-sleep=[seconds]" />
d4ebfc
+		<content type="second" default="1"  />
d4ebfc
+		<shortdesc lang="en">Sleep X seconds between status calls during a STONITH action</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+	<parameter name="retry_on" unique="0" required="0">
d4ebfc
+		<getopt mixed="--retry-on=[attempts]" />
d4ebfc
+		<content type="integer" default="1"  />
d4ebfc
+		<shortdesc lang="en">Count of attempts to retry power on</shortdesc>
d4ebfc
+	</parameter>
d4ebfc
+</parameters>
d4ebfc
+<actions>
d4ebfc
+	<action name="on" automatic="0"/>
d4ebfc
+	<action name="off" />
d4ebfc
+	<action name="reboot" />
d4ebfc
+	<action name="status" />
d4ebfc
+	<action name="list" />
d4ebfc
+	<action name="list-status" />
d4ebfc
+	<action name="monitor" />
d4ebfc
+	<action name="metadata" />
d4ebfc
+	<action name="manpage" />
d4ebfc
+	<action name="validate-all" />
d4ebfc
+</actions>
d4ebfc
+</resource-agent>