--- a/agents/openstack/fence_openstack.py 2018-06-28 14:24:54.000000000 +0200 +++ b/agents/openstack/fence_openstack.py 2022-04-22 10:15:28.069884360 +0200 @@ -2,110 +2,303 @@ import atexit import logging -import os -import re import sys -from pipes import quote -sys.path.append("/usr/share/fence") +import os + +import urllib3 + +sys.path.append("@FENCEAGENTSLIBDIR@") from fencing import * -from fencing import fail_usage, is_executable, run_command, run_delay -from keystoneclient.v3 import client as ksclient -from novaclient import client as novaclient -from keystoneclient import session as ksc_session -from keystoneclient.auth.identity import v3 - -def get_name_or_uuid(options): - return options["--uuid"] if "--uuid" in options else options["--plug"] - -def get_power_status(_, options): - output = nova_run_command(options, "status") - if (output == 'ACTIVE'): - return 'on' - else: - return 'off' +from fencing import fail_usage, run_delay, source_env + +try: + from novaclient import client + from novaclient.exceptions import Conflict, NotFound +except ImportError: + pass + +urllib3.disable_warnings(urllib3.exceptions.SecurityWarning) + + +def translate_status(instance_status): + if instance_status == "ACTIVE": + return "on" + elif instance_status == "SHUTOFF": + return "off" + return "unknown" + +def get_cloud(options): + import yaml + + clouds_yaml = "~/.config/openstack/clouds.yaml" + if not os.path.exists(os.path.expanduser(clouds_yaml)): + clouds_yaml = "/etc/openstack/clouds.yaml" + if not os.path.exists(os.path.expanduser(clouds_yaml)): + fail_usage("Failed: ~/.config/openstack/clouds.yaml and /etc/openstack/clouds.yaml does not exist") + + clouds_yaml = os.path.expanduser(clouds_yaml) + if os.path.exists(clouds_yaml): + with open(clouds_yaml, "r") as yaml_stream: + try: + clouds = yaml.safe_load(yaml_stream) + except yaml.YAMLError as exc: + fail_usage("Failed: Unable to read: " + clouds_yaml) + + cloud = clouds.get("clouds").get(options["--cloud"]) + if not cloud: + fail_usage("Cloud: {} not found.".format(options["--cloud"])) + + return cloud + + +def get_nodes_list(conn, options): + logging.info("Running %s action", options["--action"]) + result = {} + response = conn.servers.list(detailed=True) + if response is not None: + for item in response: + instance_id = item.id + instance_name = item.name + instance_status = item.status + result[instance_id] = (instance_name, translate_status(instance_status)) + return result + + +def get_power_status(conn, options): + logging.info("Running %s action on %s", options["--action"], options["--plug"]) + server = None + try: + server = conn.servers.get(options["--plug"]) + except NotFound as e: + fail_usage("Failed: Not Found: " + str(e)) + if server is None: + fail_usage("Server %s not found", options["--plug"]) + state = server.status + status = translate_status(state) + logging.info("get_power_status: %s (state: %s)" % (status, state)) + return status + + +def set_power_status(conn, options): + logging.info("Running %s action on %s", options["--action"], options["--plug"]) + action = options["--action"] + server = None + try: + server = conn.servers.get(options["--plug"]) + except NotFound as e: + fail_usage("Failed: Not Found: " + str(e)) + if server is None: + fail_usage("Server %s not found", options["--plug"]) + if action == "on": + logging.info("Starting instance " + server.name) + try: + server.start() + except Conflict as e: + fail_usage(e) + logging.info("Called start API call for " + server.id) + if action == "off": + logging.info("Stopping instance " + server.name) + try: + server.stop() + except Conflict as e: + fail_usage(e) + logging.info("Called stop API call for " + server.id) + if action == "reboot": + logging.info("Rebooting instance " + server.name) + try: + server.reboot("HARD") + except Conflict as e: + fail_usage(e) + logging.info("Called reboot hard API call for " + server.id) + + +def nova_login(username, password, projectname, auth_url, user_domain_name, + project_domain_name, ssl_insecure, cacert, apitimeout): + legacy_import = False + + try: + from keystoneauth1 import loading + from keystoneauth1 import session as ksc_session + from keystoneauth1.exceptions.discovery import DiscoveryFailure + from keystoneauth1.exceptions.http import Unauthorized + except ImportError: + try: + from keystoneclient import session as ksc_session + from keystoneclient.auth.identity import v3 + + legacy_import = True + except ImportError: + fail_usage("Failed: Keystone client not found or not accessible") + + if not legacy_import: + loader = loading.get_plugin_loader("password") + auth = loader.load_from_options( + auth_url=auth_url, + username=username, + password=password, + project_name=projectname, + user_domain_name=user_domain_name, + project_domain_name=project_domain_name, + ) + else: + auth = v3.Password( + auth_url=auth_url, + username=username, + password=password, + project_name=projectname, + user_domain_name=user_domain_name, + project_domain_name=project_domain_name, + cacert=cacert, + ) + + caverify=True + if ssl_insecure: + caverify=False + elif cacert: + caverify=cacert + + session = ksc_session.Session(auth=auth, verify=caverify, timeout=apitimeout) + nova = client.Client("2", session=session, timeout=apitimeout) + apiversion = None + try: + apiversion = nova.versions.get_current() + except DiscoveryFailure as e: + fail_usage("Failed: Discovery Failure: " + str(e)) + except Unauthorized as e: + fail_usage("Failed: Unauthorized: " + str(e)) + except Exception as e: + logging.error(e) + logging.debug("Nova version: %s", apiversion) + return nova -def set_power_status(_, options): - nova_run_command(options, options["--action"]) - return - -def nova_login(username,password,projectname,auth_url,user_domain_name,project_domain_name): - auth=v3.Password(username=username,password=password,project_name=projectname,user_domain_name=user_domain_name,project_domain_name=project_domain_name,auth_url=auth_url) - session = ksc_session.Session(auth=auth) - keystone = ksclient.Client(session=session) - nova = novaclient.Client(session=session) - return nova - -def nova_run_command(options,action,timeout=None): - username=options["--username"] - password=options["--password"] - projectname=options["--project-name"] - auth_url=options["--auth-url"] - user_domain_name=options["--user-domain-name"] - project_domain_name=options["--project-domain-name"] - novaclient=nova_login(username,password,projectname,auth_url,user_domain_name,project_domain_name) - server = novaclient.servers.get(options["--uuid"]) - if action == "status": - return server.status - if action == "on": - server.start() - if action == "off": - server.stop() - if action == "reboot": - server.reboot('REBOOT_HARD') def define_new_opts(): all_opt["auth-url"] = { - "getopt" : ":", - "longopt" : "auth-url", - "help" : "--auth-url=[authurl] Keystone Auth URL", - "required" : "1", - "shortdesc" : "Keystone Auth URL", - "order": 1 + "getopt": ":", + "longopt": "auth-url", + "help": "--auth-url=[authurl] Keystone Auth URL", + "required": "0", + "shortdesc": "Keystone Auth URL", + "order": 2, } all_opt["project-name"] = { - "getopt" : ":", - "longopt" : "project-name", - "help" : "--project-name=[project] Tenant Or Project Name", - "required" : "1", - "shortdesc" : "Keystone Project", + "getopt": ":", + "longopt": "project-name", + "help": "--project-name=[project] Tenant Or Project Name", + "required": "0", + "shortdesc": "Keystone Project", "default": "admin", - "order": 1 + "order": 3, } all_opt["user-domain-name"] = { - "getopt" : ":", - "longopt" : "user-domain-name", - "help" : "--user-domain-name=[user-domain] Keystone User Domain Name", - "required" : "0", - "shortdesc" : "Keystone User Domain Name", + "getopt": ":", + "longopt": "user-domain-name", + "help": "--user-domain-name=[domain] Keystone User Domain Name", + "required": "0", + "shortdesc": "Keystone User Domain Name", "default": "Default", - "order": 1 + "order": 4, } all_opt["project-domain-name"] = { - "getopt" : ":", - "longopt" : "project-domain-name", - "help" : "--project-domain-name=[project-domain] Keystone Project Domain Name", - "required" : "0", - "shortdesc" : "Keystone Project Domain Name", + "getopt": ":", + "longopt": "project-domain-name", + "help": "--project-domain-name=[domain] Keystone Project Domain Name", + "required": "0", + "shortdesc": "Keystone Project Domain Name", "default": "Default", - "order": 1 + "order": 5, + } + all_opt["cloud"] = { + "getopt": ":", + "longopt": "cloud", + "help": "--cloud=[cloud] Openstack cloud (from ~/.config/openstack/clouds.yaml or /etc/openstack/clouds.yaml).", + "required": "0", + "shortdesc": "Cloud from clouds.yaml", + "order": 6, + } + all_opt["openrc"] = { + "getopt": ":", + "longopt": "openrc", + "help": "--openrc=[openrc] Path to the openrc config file", + "required": "0", + "shortdesc": "openrc config file", + "order": 7, } all_opt["uuid"] = { - "getopt" : ":", - "longopt" : "uuid", - "help" : "--uuid=[uuid] UUID of the nova instance", - "required" : "1", - "shortdesc" : "UUID of the nova instance", - "order": 1 + "getopt": ":", + "longopt": "uuid", + "help": "--uuid=[uuid] Replaced by -n, --plug", + "required": "0", + "shortdesc": "Replaced by port/-n/--plug", + "order": 8, + } + all_opt["cacert"] = { + "getopt": ":", + "longopt": "cacert", + "help": "--cacert=[cacert] Path to the PEM file with trusted authority certificates (override global CA trust)", + "required": "0", + "shortdesc": "SSL X.509 certificates file", + "default": "", + "order": 9, + } + all_opt["apitimeout"] = { + "getopt": ":", + "type": "second", + "longopt": "apitimeout", + "help": "--apitimeout=[seconds] Timeout to use for API calls", + "shortdesc": "Timeout in seconds to use for API calls, default is 60.", + "required": "0", + "default": 60, + "order": 10, } + def main(): + conn = None + + device_opt = [ + "login", + "no_login", + "passwd", + "no_password", + "auth-url", + "project-name", + "user-domain-name", + "project-domain-name", + "cloud", + "openrc", + "port", + "no_port", + "uuid", + "ssl_insecure", + "cacert", + "apitimeout", + ] + atexit.register(atexit_handler) - device_opt = ["login", "passwd", "auth-url", "project-name", "user-domain-name", "project-domain-name", "uuid"] define_new_opts() + all_opt["port"]["required"] = "0" + all_opt["port"]["help"] = "-n, --plug=[UUID] UUID of the node to be fenced" + all_opt["port"]["shortdesc"] = "UUID of the node to be fenced." + all_opt["power_timeout"]["default"] = "60" + options = check_input(device_opt, process_input(device_opt)) + # workaround to avoid regressions + if "--uuid" in options: + options["--plug"] = options["--uuid"] + del options["--uuid"] + elif ("--help" not in options + and options["--action"] in ["off", "on", "reboot", "status", "validate-all"] + and "--plug" not in options): + stop_after_error = False if options["--action"] == "validate-all" else True + fail_usage( + "Failed: You have to enter plug number or machine identification", + stop_after_error, + ) + docs = {} docs["shortdesc"] = "Fence agent for OpenStack's Nova service" docs["longdesc"] = "fence_openstack is a Fencing agent \ @@ -116,9 +309,73 @@ run_delay(options) - result = fence_action(None, options, set_power_status, get_power_status,None) + if options.get("--cloud"): + cloud = get_cloud(options) + username = cloud.get("auth").get("username") + password = cloud.get("auth").get("password") + projectname = cloud.get("auth").get("project_name") + auth_url = None + try: + auth_url = cloud.get("auth").get("auth_url") + except KeyError: + fail_usage("Failed: You have to set the Keystone service endpoint for authorization") + user_domain_name = cloud.get("auth").get("user_domain_name") + project_domain_name = cloud.get("auth").get("project_domain_name") + caverify = cloud.get("verify") + if caverify in [True, False]: + options["--ssl-insecure"] = caverify + else: + options["--cacert"] = caverify + elif options.get("--openrc"): + if not os.path.exists(os.path.expanduser(options["--openrc"])): + fail_usage("Failed: {} does not exist".format(options.get("--openrc"))) + source_env(options["--openrc"]) + env = os.environ + username = env.get("OS_USERNAME") + password = env.get("OS_PASSWORD") + projectname = env.get("OS_PROJECT_NAME") + auth_url = None + try: + auth_url = env["OS_AUTH_URL"] + except KeyError: + fail_usage("Failed: You have to set the Keystone service endpoint for authorization") + user_domain_name = env.get("OS_USER_DOMAIN_NAME") + project_domain_name = env.get("OS_PROJECT_DOMAIN_NAME") + else: + username = options["--username"] + password = options["--password"] + projectname = options["--project-name"] + auth_url = None + try: + auth_url = options["--auth-url"] + except KeyError: + fail_usage("Failed: You have to set the Keystone service endpoint for authorization") + user_domain_name = options["--user-domain-name"] + project_domain_name = options["--project-domain-name"] + + ssl_insecure = "--ssl-insecure" in options + cacert = options["--cacert"] + apitimeout = options["--apitimeout"] + + try: + conn = nova_login( + username, + password, + projectname, + auth_url, + user_domain_name, + project_domain_name, + ssl_insecure, + cacert, + apitimeout, + ) + except Exception as e: + fail_usage("Failed: Unable to connect to Nova: " + str(e)) + + # Operate the fencing device + result = fence_action(conn, options, set_power_status, get_power_status, get_nodes_list) sys.exit(result) + if __name__ == "__main__": main() -