-
Notifications
You must be signed in to change notification settings - Fork 166
fence_shelly: new fence agent for Shelly Gen2+ Switches #628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kaelancotter
wants to merge
1
commit into
ClusterLabs:main
Choose a base branch
from
kaelancotter:fence_shelly_gen2-new-fence-agent
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
<?xml version="1.0" ?> | ||
<resource-agent name="fence_shelly_gen2" shortdesc="Fence agent for Shelly Gen 2+ Switches" > | ||
<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.</longdesc> | ||
<vendor-url>https://shelly-api-docs.shelly.cloud/gen2/</vendor-url> | ||
<parameters> | ||
<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="ip" unique="0" required="1" obsoletes="ipaddr"> | ||
<getopt mixed="-a, --ip=[ip]" /> | ||
<content type="string" /> | ||
<shortdesc lang="en">IP address or hostname of fencing device</shortdesc> | ||
</parameter> | ||
<parameter name="ipaddr" unique="0" required="1" deprecated="1"> | ||
<getopt mixed="-a, --ip=[ip]" /> | ||
<content type="string" /> | ||
<shortdesc lang="en">IP address or hostname of fencing device</shortdesc> | ||
</parameter> | ||
<parameter name="ipport" unique="0" required="0"> | ||
<getopt mixed="-u, --ipport=[port]" /> | ||
<content type="integer" default="80" /> | ||
<shortdesc lang="en">TCP/UDP port to use for connection with device</shortdesc> | ||
</parameter> | ||
<parameter name="notls" unique="0" required="0"> | ||
<getopt mixed="-t, --notls" /> | ||
<content type="boolean" /> | ||
<shortdesc lang="en">Disable TLS negotiation and force SSL3.0. This should only be used for devices that do not support TLS1.0 and up.</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="passwd_script" unique="0" required="0" deprecated="1"> | ||
<getopt mixed="-S, --password-script=[script]" /> | ||
<content type="string" /> | ||
<shortdesc lang="en">Script to run to retrieve password</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 run to retrieve password</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 on device, UUID or identification of machine</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 on device, UUID or identification of machine</shortdesc> | ||
</parameter> | ||
<parameter name="ssl" unique="0" required="0"> | ||
<getopt mixed="-z, --ssl" /> | ||
<content type="boolean" /> | ||
<shortdesc lang="en">Use SSL connection with verifying certificate</shortdesc> | ||
</parameter> | ||
<parameter name="ssl_insecure" unique="0" required="0"> | ||
<getopt mixed="--ssl-insecure" /> | ||
<content type="boolean" /> | ||
<shortdesc lang="en">Use SSL connection without verifying certificate</shortdesc> | ||
</parameter> | ||
<parameter name="ssl_secure" unique="0" required="0"> | ||
<getopt mixed="--ssl-secure" /> | ||
<content type="boolean" /> | ||
<shortdesc lang="en">Use SSL connection with verifying certificate</shortdesc> | ||
</parameter> | ||
<parameter name="sw_id" unique="0" required="0"> | ||
<getopt mixed="--sw-id=[id]" /> | ||
<content type="string" default="0" /> | ||
<shortdesc lang="en">The id of the Switch component instance</shortdesc> | ||
</parameter> | ||
<parameter name="quiet" unique="0" required="0"> | ||
<getopt mixed="-q, --quiet" /> | ||
<content type="boolean" /> | ||
<shortdesc lang="en">Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog.</shortdesc> | ||
</parameter> | ||
<parameter name="verbose" unique="0" required="0"> | ||
<getopt mixed="-v, --verbose" /> | ||
<content type="boolean" /> | ||
<shortdesc lang="en">Verbose mode. Multiple -v flags can be stacked on the command line (e.g., -vvv) to increase verbosity.</shortdesc> | ||
</parameter> | ||
<parameter name="verbose_level" unique="0" required="0"> | ||
<getopt mixed="--verbose-level" /> | ||
<content type="integer" /> | ||
<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> | ||
</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]" /> | ||
<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="plug_separator" unique="0" required="0"> | ||
<getopt mixed="--plug-separator=[char]" /> | ||
<content type="string" default="," /> | ||
<shortdesc lang="en">Separator for plug parameter when specifying more than 1 plug</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 'list' operation</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="disable_timeout" unique="0" required="0"> | ||
<getopt mixed="--disable-timeout=[true/false]" /> | ||
<content type="string" /> | ||
<shortdesc lang="en">Disable timeout (true/false) (default: true when run from Pacemaker 2.0+)</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="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="power_wait" unique="0" required="0"> | ||
<getopt mixed="--power-wait=[seconds]" /> | ||
<content type="second" default="1" /> | ||
<shortdesc lang="en">Wait X seconds after issuing ON/OFF</shortdesc> | ||
</parameter> | ||
<parameter name="shell_timeout" unique="0" required="0"> | ||
<getopt mixed="--shell-timeout=[seconds]" /> | ||
<content type="second" default="5" /> | ||
<shortdesc lang="en">Wait X seconds for cmd prompt after issuing command</shortdesc> | ||
</parameter> | ||
<parameter name="stonith_status_sleep" unique="0" required="0"> | ||
<getopt mixed="--stonith-status-sleep=[seconds]" /> | ||
<content type="second" default="1" /> | ||
<shortdesc lang="en">Sleep X seconds between status calls during a STONITH action</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> | ||
<parameter name="gnutlscli_path" unique="0" required="0"> | ||
<getopt mixed="--gnutlscli-path=[path]" /> | ||
<shortdesc lang="en">Path to gnutls-cli binary</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="manpage" /> | ||
<action name="validate-all" /> | ||
</actions> | ||
</resource-agent> |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use plug like we do for the other agents.
You can override "help" and "shortdesc" just like you've done for a couple of default values below.
Remember to run
make xml-upload
after and add the updated metadata to the commit.