Blob Blame History Raw
--- 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()
-