#!/usr/local/sbin/charm-env python3 import os import re import shlex import subprocess import sys from charms import layer from etcdctl import EtcdCtl from charmhelpers.core.hookenv import action_get, action_set, action_fail, action_name CTL = EtcdCtl() def action_fail_now(*args, **kw): """Call action_fail() and exit immediately.""" action_fail(*args, **kw) sys.exit(0) def requires_etcd_version(version_regex, human_version=None): """Decorator that enforces a specific version of etcdctl be present. The decorated function will only be executed if the required version of etcdctl is present. Otherwise, action_fail() will be called and the process will exit immediately. """ def wrap(f): def wrapped_f(*args): version = CTL.version() if not re.match(version_regex, version): required_version = human_version or version_regex action_fail_now( "This action requires etcd version {}".format(required_version) ) f(*args) return wrapped_f return wrap requires_etcd_v2 = requires_etcd_version(r"2\..*", human_version="2.x") requires_etcd_v3 = requires_etcd_version(r"3\..*", human_version="3.x") @requires_etcd_v3 def alarm_disarm(): """Call `etcdctl alarm disarm`.""" try: output = CTL.run("alarm disarm") action_set(dict(output=output)) except subprocess.CalledProcessError as e: action_fail_now(e.output) @requires_etcd_v3 def alarm_list(): """Call `etcdctl alarm list`.""" try: output = CTL.run("alarm list") action_set(dict(output=output)) except subprocess.CalledProcessError as e: action_fail_now(e.output) @requires_etcd_v3 def compact(): """Call `etcdctl compact`.""" def get_latest_revision(): try: output = CTL.run("endpoint status --write-out json") except subprocess.CalledProcessError as e: action_fail_now( "Failed to determine latest revision for " "compaction: {}".format(e) ) m = re.search(r'"revision":(\d*)', output) if not m: action_fail_now( "Failed to get revision from 'endpoint status' " "output: {}".format(output) ) return m.group(1) revision = action_get("revision") or get_latest_revision() physical = "true" if action_get("physical") else "false" command = "compact {} --physical={}".format(revision, physical) try: output = CTL.run(command) action_set(dict(output=output)) except subprocess.CalledProcessError as e: action_fail_now(e.output) @requires_etcd_v3 def defrag(): """Call `etcdctl defrag`.""" try: output = CTL.run("defrag") action_set(dict(output=output)) except subprocess.CalledProcessError as e: action_fail_now(e.output) def health(): """Call etcdctl cluster-health""" try: output = CTL.cluster_health(True) action_set(dict(output=output)) except subprocess.CalledProcessError as e: action_fail_now(e.output) if __name__ == "__main__": ACTIONS = { "alarm-disarm": alarm_disarm, "alarm-list": alarm_list, "compact": compact, "defrag": defrag, "health": health, } action = action_name() ACTIONS[action]()