diff --git a/agents/shelly/fence_shelly_gen2.py b/agents/shelly/fence_shelly_gen2.py new file mode 100644 index 000000000..0185cd491 --- /dev/null +++ b/agents/shelly/fence_shelly_gen2.py @@ -0,0 +1,172 @@ +#!@PYTHON@ -tt +import sys +import pycurl +import io +import json +import logging +import atexit + +sys.path.append("@FENCEAGENTSLIBDIR@") +from fencing import * +from fencing import fail, run_delay, EC_LOGIN_DENIED, EC_STATUS + + +state = {True: "on", False: "off"} + + +def get_power_status(conn, opt): + try: + result = send_command(conn, gen_payload(opt, "Switch.GetStatus")) + except Exception as e: + fail(EC_STATUS) + # Shelly Documentation Uses `params` subkey: + # https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch/#switchgetstatus-example + # Observed behavior (ShellyHTTP/1.0.0) uses `result` subkey + # Used Shelly Plus Plug US - FW 1.5.1 + if "params" in result: + subkey = "params" + elif "result" in result: + subkey = "result" + return state[result[subkey]["output"]] + + +def set_power_status(conn, opt): + action = {state[k]: k for k in state} + output = action[opt["--action"]] + try: + result = send_command(conn, gen_payload(opt, "Switch.Set", output=output)) + except Exception as e: + fail(EC_STATUS) + + +# We use method here as the RPC procedure not HTTP method as all commands use POST +def gen_payload(opt, method, output=None): + if "--sw_id" in opt: + sw_id = opt["--sw_id"] + else: + sw_id = 0 + ret = {"id": 1, + "method": method, + "params": {"id": sw_id}} + if output is not None: + ret["params"]["on"] = output + return ret + + +def connect(opt): + conn = pycurl.Curl() + + ## setup correct URL + if "--ssl-secure" in opt or "--ssl-insecure" in opt: + conn.base_url = "https:" + else: + conn.base_url = "http:" + + api_path = "/rpc" + conn.base_url += "//" + opt["--ip"] + ":" + str(opt["--ipport"]) + api_path + + ## send command through pycurl + conn.setopt(pycurl.HTTPHEADER, [ + "Accept: application/json", + ]) + + # Shelly always uses a default user of admin + # ex. https://shelly-api-docs.shelly.cloud/gen2/General/Authentication#successful-request-with-authentication-details + if "--password" in opt: + conn.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_ANY) + conn.setopt(pycurl.USERPWD, "admin:" + opt["--password"]) + conn.setopt(pycurl.TIMEOUT, int(opt["--shell-timeout"])) + if "--ssl-secure" in opt: + conn.setopt(pycurl.SSL_VERIFYPEER, 1) + conn.setopt(pycurl.SSL_VERIFYHOST, 2) + elif "--ssl-insecure" in opt: + conn.setopt(pycurl.SSL_VERIFYPEER, 0) + conn.setopt(pycurl.SSL_VERIFYHOST, 0) + + # Check general reachability (unprotected method) + try: + device_info_payload = {"id": 1, "method": "Shelly.GetDeviceInfo"} + _ = send_command(conn, device_info_payload) + except Exception as e: + logging.debug("Failed: {}".format(e)) + fail(EC_LOGIN_DENIED) + + # Check method requiring authentication + try: + _ = send_command(conn, gen_payload(opt, "Switch.GetStatus")) + except Exception as e: + logging.debug("Invalid Authentication: {}".format(e)) + return conn + + +def send_command(conn, payload): + conn.setopt(pycurl.URL, conn.base_url.encode("ascii")) + conn.setopt(pycurl.POSTFIELDS, json.dumps(payload)) + 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() + if rc != 200: + if len(result) > 0: + raise Exception("Remote returned {}: {}".format(rc, result)) + else: + raise Exception("Remote returned {} for request to {}".format(rc, conn.base_url)) + if len(result) > 0: + result = json.loads(result) + logging.debug("url: {}".format(conn.base_url)) + logging.debug("POST method payload: {}".format(payload)) + logging.debug("response code: {}".format(rc)) + logging.debug("result: {}\n".format(result)) + return result + +def main(): + device_opt = [ + "ipaddr", + "passwd", + "ssl", + "notls", + "web", + "port", + "sw_id", + ] + + all_opt["sw_id"] = { + "getopt": ":", + "longopt": "sw-id", + "help": "--sw-id=[id] Id of the Switch component instance", + "default": "0", + "required": "0", + "shortdesc": "The id of the Switch component instance", + "order": 2 + } + + atexit.register(atexit_handler) + all_opt["shell_timeout"]["default"] = "5" + all_opt["power_wait"]["default"] = "1" + + options = check_input(device_opt, process_input(device_opt)) + + docs = {} + docs["shortdesc"] = "Fence agent for Shelly Gen 2+ Switches" + docs["longdesc"] = """fence_shelly_gen2 is a Power Fencing agent which can be \ +used with Shelly Switches supporting the gen 2+ API to fence attached hardware.""" + docs["vendorurl"] = "https://shelly-api-docs.shelly.cloud/gen2/" + show_docs(options, docs) + + #### + ## Fence operations + #### + run_delay(options) + conn = connect(options) + atexit.register(conn.close) + result = fence_action(conn, options, set_power_status, get_power_status) + sys.exit(result) + +if __name__ == "__main__": + main() + diff --git a/fence-agents.spec.in b/fence-agents.spec.in index f8ef5f279..9b6691b61 100644 --- a/fence-agents.spec.in +++ b/fence-agents.spec.in @@ -80,6 +80,7 @@ fence-agents-rsb \\ fence-agents-sanbox2 \\ fence-agents-sbd \\ fence-agents-scsi \\ +fence-agents-shelly \\ fence-agents-vbox \\ fence-virt \\ fence-agents-vmware \\ @@ -1117,6 +1118,17 @@ Fence agent for SCSI persistent reservations. %{_datadir}/cluster/fence_scsi_check_hardreboot %{_mandir}/man8/fence_scsi.8* +%package shelly +License: GPL-2.0-or-later AND LGPL-2.0-or-later +Summary: Fence agent for Shelly Switches +Requires: fence-agents-common = %{version}-%{release} +BuildArch: noarch +%description shelly +Fence agent for Shelly Switches. +%files shelly +%{_sbindir}/fence_shelly_gen2 +%{_mandir}/man8/fence_shelly_gen2.8* + %package vbox License: GPL-2.0-or-later AND LGPL-2.0-or-later Summary: Fence agent for VirtualBox diff --git a/tests/data/metadata/fence_shelly_gen2.xml b/tests/data/metadata/fence_shelly_gen2.xml new file mode 100644 index 000000000..2d1e6b8ae --- /dev/null +++ b/tests/data/metadata/fence_shelly_gen2.xml @@ -0,0 +1,182 @@ + + +fence_shelly_gen2 is a Power Fencing agent which can be used with Shelly Switches supporting the gen 2+ API to fence attached hardware. +https://shelly-api-docs.shelly.cloud/gen2/ + + + + + Fencing action + + + + + IP address or hostname of fencing device + + + + + IP address or hostname of fencing device + + + + + TCP/UDP port to use for connection with device + + + + + Disable TLS negotiation and force SSL3.0. This should only be used for devices that do not support TLS1.0 and up. + + + + + Login password or passphrase + + + + + Script to run to retrieve password + + + + + Login password or passphrase + + + + + Script to run to retrieve password + + + + + Physical plug number on device, UUID or identification of machine + + + + + Physical plug number on device, UUID or identification of machine + + + + + Use SSL connection with verifying certificate + + + + + Use SSL connection without verifying certificate + + + + + Use SSL connection with verifying certificate + + + + + The id of the Switch component instance + + + + + 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 plug parameter when specifying more than 1 plug + + + + + 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 + + + + Path to gnutls-cli binary + + + + + + + + + + + + + + +