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