diff --git a/agents/ibm_powervs/fence_ibm_powervs.py b/agents/ibm_powervs/fence_ibm_powervs.py index 819ab889..b0caed7c 100755 --- a/agents/ibm_powervs/fence_ibm_powervs.py +++ b/agents/ibm_powervs/fence_ibm_powervs.py @@ -1,267 +1,267 @@ #!@PYTHON@ -tt import sys import pycurl, io, json import logging import atexit import time sys.path.append("@FENCEAGENTSLIBDIR@") from fencing import * from fencing import fail, run_delay, EC_LOGIN_DENIED, EC_STATUS state = { "ACTIVE": "on", "SHUTOFF": "off", "ERROR": "unknown" } def get_token(conn, options): try: command = "identity/token" action = "grant_type=urn%3Aibm%3Aparams%3Aoauth%3Agrant-type%3Aapikey&apikey={}".format(options["--token"]) res = send_command(conn, command, "POST", action, printResult=False) except Exception as e: logging.debug("Failed: {}".format(e)) return "TOKEN_IS_MISSING_OR_WRONG" return res["access_token"] def get_list(conn, options): outlets = {} try: command = "cloud-instances/{}/pvm-instances".format(options["--instance"]) res = send_command(conn, command) except Exception as e: logging.debug("Failed: {}".format(e)) return outlets for r in res["pvmInstances"]: if "--verbose" in options: logging.debug(json.dumps(r, indent=2)) outlets[r["pvmInstanceID"]] = (r["serverName"], state[r["status"]]) return outlets def get_power_status(conn, options): try: command = "cloud-instances/{}/pvm-instances/{}".format( options["--instance"], options["--plug"]) res = send_command(conn, command) result = get_list(conn, options)[options["--plug"]][1] except KeyError as e: logging.debug("Failed: Unable to get status for {}".format(e)) fail(EC_STATUS) return result def set_power_status(conn, options): action = { "on" : '{"action" : "start"}', "off" : '{"action" : "immediate-shutdown"}', }[options["--action"]] try: send_command(conn, "cloud-instances/{}/pvm-instances/{}/action".format( options["--instance"], options["--plug"]), "POST", action) except Exception as e: logging.debug("Failed: Unable to set power to {} for {}".format(options["--action"], e)) fail(EC_STATUS) def connect(opt, token): conn = pycurl.Curl() ## setup correct URL conn.base_url = "https://" + opt["--region"] + ".power-iaas.cloud.ibm.com/pcloud/v1/" if opt["--api-type"] == "private": conn.base_url = "https://private." + opt["--region"] + ".power-iaas.cloud.ibm.com/pcloud/v1/" if opt["--verbose-level"] < 3: conn.setopt(pycurl.VERBOSE, 0) conn.setopt(pycurl.CONNECTTIMEOUT,int(opt["--shell-timeout"])) conn.setopt(pycurl.TIMEOUT, int(opt["--shell-timeout"])) conn.setopt(pycurl.SSL_VERIFYPEER, 1) conn.setopt(pycurl.SSL_VERIFYHOST, 2) conn.setopt(pycurl.PROXY, "{}".format(opt["--proxy"])) # set auth token for later requests conn.setopt(pycurl.HTTPHEADER, [ "Content-Type: application/json", "Authorization: Bearer {}".format(token), "CRN: {}".format(opt["--crn"]), "User-Agent: curl", ]) - + return conn def auth_connect(opt): conn = pycurl.Curl() # setup correct URL conn.base_url = "https://iam.cloud.ibm.com/" if opt["--verbose-level"] > 1: conn.setopt(pycurl.VERBOSE, 1) conn.setopt(pycurl.CONNECTTIMEOUT,int(opt["--shell-timeout"])) conn.setopt(pycurl.TIMEOUT, int(opt["--shell-timeout"])) conn.setopt(pycurl.SSL_VERIFYPEER, 1) conn.setopt(pycurl.SSL_VERIFYHOST, 2) conn.setopt(pycurl.PROXY, "{}".format(opt["--proxy"])) # set auth token for later requests conn.setopt(pycurl.HTTPHEADER, [ "Content-type: application/x-www-form-urlencoded", "Accept: application/json", "User-Agent: curl", ]) return conn def disconnect(conn): conn.close() def send_command(conn, command, method="GET", action=None, printResult=True): url = conn.base_url + command conn.setopt(pycurl.URL, url.encode("ascii")) web_buffer = io.BytesIO() if method == "GET": conn.setopt(pycurl.POST, 0) if method == "POST": conn.setopt(pycurl.POSTFIELDS, action) if method == "DELETE": conn.setopt(pycurl.CUSTOMREQUEST, "DELETE") conn.setopt(pycurl.WRITEFUNCTION, web_buffer.write) try: conn.perform() except Exception as e: logging.error("send_command(): {}".format(e)) raise(e) rc = conn.getinfo(pycurl.HTTP_CODE) result = web_buffer.getvalue().decode("UTF-8") web_buffer.close() if rc != 200: if len(result) > 0: raise Exception("{}: {}".format(rc,result)) else: raise Exception("Remote returned {} for request to {}".format(rc, url)) if len(result) > 0: result = json.loads(result) logging.debug("url: {}".format(url)) logging.debug("method: {}".format(method)) logging.debug("response code: {}".format(rc)) if printResult: logging.debug("result: {}\n".format(result)) return result def define_new_opts(): all_opt["token"] = { "getopt" : ":", "longopt" : "token", "help" : "--token=[token] API Token", "required" : "1", "shortdesc" : "API Token", "order" : 0 } all_opt["crn"] = { "getopt" : ":", "longopt" : "crn", "help" : "--crn=[crn] CRN", "required" : "1", "shortdesc" : "CRN", "order" : 0 } all_opt["instance"] = { "getopt" : ":", "longopt" : "instance", "help" : "--instance=[instance] PowerVS Instance", "required" : "1", "shortdesc" : "PowerVS Instance", "order" : 0 } all_opt["region"] = { "getopt" : ":", "longopt" : "region", "help" : "--region=[region] Region", "required" : "1", "shortdesc" : "Region", "order" : 0 } all_opt["api-type"] = { "getopt" : ":", "longopt" : "api-type", "help" : "--api-type=[public|private] API-type: 'public' (default) or 'private'", "required" : "0", "shortdesc" : "API-type (public|private)", "order" : 0 } all_opt["proxy"] = { "getopt" : ":", "longopt" : "proxy", "help" : "--proxy=[http://:] Proxy: 'http://:'", "required" : "0", "shortdesc" : "Network proxy", "order" : 0 } def main(): device_opt = [ "token", "crn", "instance", "region", "api-type", "proxy", "port", "no_password", ] atexit.register(atexit_handler) define_new_opts() all_opt["shell_timeout"]["default"] = "15" all_opt["power_timeout"]["default"] = "30" all_opt["power_wait"]["default"] = "1" all_opt["stonith_status_sleep"]["default"] = "3" all_opt["api-type"]["default"] = "private" all_opt["proxy"]["default"] = "" options = check_input(device_opt, process_input(device_opt)) docs = {} docs["shortdesc"] = "Fence agent for IBM PowerVS" docs["longdesc"] = """fence_ibm_powervs is an I/O Fencing agent which can be \ used with IBM PowerVS to fence virtual machines.""" docs["vendorurl"] = "https://www.ibm.com" show_docs(options, docs) #### ## Fence operations #### run_delay(options) auth_conn = auth_connect(options) token = get_token(auth_conn, options) disconnect(auth_conn) conn = connect(options, token) atexit.register(disconnect, conn) result = fence_action(conn, options, set_power_status, get_power_status, get_list) sys.exit(result) if __name__ == "__main__": main() diff --git a/agents/ibm_vpc/fence_ibm_vpc.py b/agents/ibm_vpc/fence_ibm_vpc.py index 3da3ce05..84701058 100755 --- a/agents/ibm_vpc/fence_ibm_vpc.py +++ b/agents/ibm_vpc/fence_ibm_vpc.py @@ -1,232 +1,316 @@ #!@PYTHON@ -tt import sys import pycurl, io, json import logging import atexit +import hashlib sys.path.append("@FENCEAGENTSLIBDIR@") from fencing import * -from fencing import fail, run_delay, EC_LOGIN_DENIED, EC_STATUS +from fencing import fail, run_delay, EC_LOGIN_DENIED, EC_STATUS, EC_GENERIC_ERROR state = { "running": "on", "stopped": "off", "starting": "unknown", "stopping": "unknown", "restarting": "unknown", "pending": "unknown", } def get_list(conn, options): outlets = {} try: command = "instances?version=2021-05-25&generation=2&limit={}".format(options["--limit"]) - res = send_command(conn, command) + res = send_command(conn, options, command) except Exception as e: logging.debug("Failed: Unable to get list: {}".format(e)) return outlets for r in res["instances"]: if options["--verbose-level"] > 1: logging.debug("Node:\n{}".format(json.dumps(r, indent=2))) logging.debug("Status: " + state[r["status"]]) outlets[r["id"]] = (r["name"], state[r["status"]]) return outlets def get_power_status(conn, options): try: command = "instances/{}?version=2021-05-25&generation=2".format(options["--plug"]) - res = send_command(conn, command) + res = send_command(conn, options, command) result = state[res["status"]] if options["--verbose-level"] > 1: logging.debug("Result:\n{}".format(json.dumps(res, indent=2))) logging.debug("Status: " + result) except Exception as e: logging.debug("Failed: Unable to get status for {}: {}".format(options["--plug"], e)) fail(EC_STATUS) return result def set_power_status(conn, options): action = { "on" : '{"type" : "start"}', "off" : '{"type" : "stop"}', }[options["--action"]] try: command = "instances/{}/actions?version=2021-05-25&generation=2".format(options["--plug"]) - send_command(conn, command, "POST", action, 201) + send_command(conn, options, command, "POST", action, 201) except Exception as e: logging.debug("Failed: Unable to set power to {} for {}".format(options["--action"], e)) fail(EC_STATUS) def get_bearer_token(conn, options): + import os, errno + + try: + # FIPS requires usedforsecurity=False and might not be + # available on all distros: https://bugs.python.org/issue9216 + hash = hashlib.sha256(options["--apikey"].encode("utf-8"), usedforsecurity=False).hexdigest() + except (AttributeError, TypeError): + hash = hashlib.sha256(options["--apikey"].encode("utf-8")).hexdigest() + file_path = options["--token-file"].replace("[hash]", hash) token = None + + if not os.path.isdir(os.path.dirname(file_path)): + os.makedirs(os.path.dirname(file_path)) + + # For security, remove file with potentially elevated mode try: - conn.setopt(pycurl.HTTPHEADER, [ - "Content-Type: application/x-www-form-urlencoded", - "User-Agent: curl", - ]) - 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"] - except Exception: - logging.error("Failed: Unable to authenticate") - fail(EC_LOGIN_DENIED) + os.remove(file_path) + except OSError: + pass + + try: + oldumask = os.umask(0) + file_handle = os.open(file_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) + except OSError as e: + if e.errno == errno.EEXIST: # Failed as the file already exists. + logging.error("Failed: File already exists: {}".format(e)) + sys.exit(EC_GENERIC_ERROR) + else: # Something unexpected went wrong + logging.error("Failed: Unable to open file: {}".format(e)) + sys.exit(EC_GENERIC_ERROR) + else: # No exception, so the file must have been created successfully. + with os.fdopen(file_handle, 'w') as file_obj: + try: + conn.setopt(pycurl.HTTPHEADER, [ + "Content-Type: application/x-www-form-urlencoded", + "User-Agent: curl", + ]) + token = send_command(conn, options, "https://iam.cloud.ibm.com/identity/token", "POST", "grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey={}".format(options["--apikey"]))["access_token"] + except Exception as e: + logging.error("Failed: Unable to authenticate: {}".format(e)) + fail(EC_LOGIN_DENIED) + file_obj.write(token) + finally: + os.umask(oldumask) return token +def set_bearer_token(conn, bearer_token): + conn.setopt(pycurl.HTTPHEADER, [ + "Content-Type: application/json", + "Authorization: Bearer {}".format(bearer_token), + "User-Agent: curl", + ]) + + return conn + def connect(opt): conn = pycurl.Curl() + bearer_token = "" ## setup correct URL conn.base_url = "https://" + opt["--region"] + ".iaas.cloud.ibm.com/v1/" if opt["--verbose-level"] > 1: conn.setopt(pycurl.VERBOSE, 1) conn.setopt(pycurl.TIMEOUT, int(opt["--shell-timeout"])) conn.setopt(pycurl.SSL_VERIFYPEER, 1) conn.setopt(pycurl.SSL_VERIFYHOST, 2) conn.setopt(pycurl.PROXY, "{}".format(opt["--proxy"])) # get bearer token - bearer_token = get_bearer_token(conn, opt) + try: + try: + # FIPS requires usedforsecurity=False and might not be + # available on all distros: https://bugs.python.org/issue9216 + hash = hashlib.sha256(opt["--apikey"].encode("utf-8"), usedforsecurity=False).hexdigest() + except (AttributeError, TypeError): + hash = hashlib.sha256(opt["--apikey"].encode("utf-8")).hexdigest() + f = open(opt["--token-file"].replace("[hash]", hash)) + bearer_token = f.read() + f.close() + except IOError: + bearer_token = get_bearer_token(conn, opt) # set auth token for later requests - conn.setopt(pycurl.HTTPHEADER, [ - "Content-Type: application/json", - "Authorization: Bearer {}".format(bearer_token), - "User-Agent: curl", - ]) + conn = set_bearer_token(conn, bearer_token) return conn def disconnect(conn): conn.close() -def send_command(conn, command, method="GET", action=None, expected_rc=200): +def send_command(conn, options, command, method="GET", action=None, expected_rc=200): if not command.startswith("https"): url = conn.base_url + command else: url = command conn.setopt(pycurl.URL, url.encode("ascii")) web_buffer = io.BytesIO() if method == "GET": conn.setopt(pycurl.POST, 0) if method == "POST": conn.setopt(pycurl.POSTFIELDS, action) if method == "DELETE": conn.setopt(pycurl.CUSTOMREQUEST, "DELETE") conn.setopt(pycurl.WRITEFUNCTION, web_buffer.write) try: conn.perform() except Exception as e: raise(e) rc = conn.getinfo(pycurl.HTTP_CODE) + + # auth if token has expired + if rc in [400, 401, 415]: + tokenconn = pycurl.Curl() + token = get_bearer_token(tokenconn, options) + tokenconn.close() + conn = set_bearer_token(conn, token) + + # flush web_buffer + web_buffer.close() + web_buffer = io.BytesIO() + conn.setopt(pycurl.WRITEFUNCTION, web_buffer.write) + + try: + conn.perform() + except Exception as e: + raise(e) + + rc = conn.getinfo(pycurl.HTTP_CODE) + result = web_buffer.getvalue().decode("UTF-8") web_buffer.close() # actions (start/stop/reboot) report 201 when they've been created if rc != expected_rc: logging.debug("rc: {}, result: {}".format(rc, result)) if len(result) > 0: raise Exception("{}: {}".format(rc, result["value"]["messages"][0]["default_message"])) else: raise Exception("Remote returned {} for request to {}".format(rc, url)) if len(result) > 0: result = json.loads(result) logging.debug("url: {}".format(url)) logging.debug("method: {}".format(method)) logging.debug("response code: {}".format(rc)) logging.debug("result: {}\n".format(result)) return result def define_new_opts(): all_opt["apikey"] = { "getopt" : ":", "longopt" : "apikey", "help" : "--apikey=[key] API Key", "required" : "1", "shortdesc" : "API Key", "order" : 0 } all_opt["region"] = { "getopt" : ":", "longopt" : "region", "help" : "--region=[region] Region", "required" : "1", "shortdesc" : "Region", "order" : 0 } all_opt["proxy"] = { "getopt" : ":", "longopt" : "proxy", - "help" : "--proxy=[http://:] Proxy: 'http://:'", + "help" : "--proxy=[http://:] Proxy: 'http://:'", "required" : "0", "default": "", "shortdesc" : "Network proxy", "order" : 0 } all_opt["limit"] = { "getopt" : ":", "longopt" : "limit", "help" : "--limit=[number] Limit number of nodes returned by API", "required" : "0", "default": 50, "shortdesc" : "Number of nodes returned by API", "order" : 0 } + all_opt["token_file"] = { + "getopt" : ":", + "longopt" : "token-file", + "help" : "--token-file=[path] Path to the token cache file\n" + "\t\t\t\t (Default: @FENCETMPDIR@/fence_ibm_vpc/[hash].token)\n" + "\t\t\t\t [hash] will be replaced by a hashed value", + "required" : "0", + "default": "@FENCETMPDIR@/fence_ibm_vpc/[hash].token", + "shortdesc" : "Path to the token cache file", + "order" : 0 + } def main(): device_opt = [ "apikey", "region", - "limit", "proxy", + "limit", + "token_file", "port", "no_password", ] atexit.register(atexit_handler) define_new_opts() all_opt["shell_timeout"]["default"] = "15" all_opt["power_timeout"]["default"] = "30" all_opt["power_wait"]["default"] = "1" options = check_input(device_opt, process_input(device_opt)) docs = {} docs["shortdesc"] = "Fence agent for IBM Cloud VPC" docs["longdesc"] = """fence_ibm_vpc is an I/O Fencing agent which can be \ used with IBM Cloud VPC to fence virtual machines.""" docs["vendorurl"] = "https://www.ibm.com" show_docs(options, docs) #### ## Fence operations #### run_delay(options) conn = connect(options) atexit.register(disconnect, conn) result = fence_action(conn, options, set_power_status, get_power_status, get_list) sys.exit(result) if __name__ == "__main__": main() diff --git a/tests/data/metadata/fence_ibm_vpc.xml b/tests/data/metadata/fence_ibm_vpc.xml index acf4925f..c35bc461 100644 --- a/tests/data/metadata/fence_ibm_vpc.xml +++ b/tests/data/metadata/fence_ibm_vpc.xml @@ -1,133 +1,137 @@ fence_ibm_vpc is an I/O Fencing agent which can be used with IBM Cloud VPC to fence virtual machines. https://www.ibm.com API Key Number of nodes returned by API Network proxy Region + + + Path to the token cache file + Fencing action Physical plug number on device, UUID or identification of machine Physical plug number on device, UUID or identification of machine Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog. Verbose mode. Multiple -v flags can be stacked on the command line (e.g., -vvv) to increase verbosity. 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). Write debug information to given file Write debug information to given file Display version information and exit Display help and exit Separator for CSV created by 'list' operation Wait X seconds before fencing is started Disable timeout (true/false) (default: true when run from Pacemaker 2.0+) Wait X seconds for cmd prompt after login Test X seconds for status change after ON/OFF Wait X seconds after issuing ON/OFF Wait X seconds for cmd prompt after issuing command Sleep X seconds between status calls during a STONITH action Count of attempts to retry power on