1    	#!@PYTHON@ -tt
2    	
3    	import atexit
4    	import logging
5    	import sys
6    	import os
7    	
8    	import urllib3
9    	
10   	sys.path.append("@FENCEAGENTSLIBDIR@")
11   	from fencing import *
12   	from fencing import fail_usage, run_delay, source_env
13   	
14   	try:
15   	    from novaclient import client
16   	    from novaclient.exceptions import Conflict, NotFound
17   	except ImportError:
18   	    pass
19   	
20   	urllib3.disable_warnings(urllib3.exceptions.SecurityWarning)
21   	
22   	
23   	def translate_status(instance_status):
24   	    if instance_status == "ACTIVE":
25   	        return "on"
26   	    elif instance_status == "SHUTOFF":
27   	        return "off"
28   	    return "unknown"
29   	
30   	def get_cloud(options):
31   	    import yaml
32   	
33   	    clouds_yaml = "~/.config/openstack/clouds.yaml"
34   	    if not os.path.exists(os.path.expanduser(clouds_yaml)):
35   	        clouds_yaml = "/etc/openstack/clouds.yaml"
36   	    if not os.path.exists(os.path.expanduser(clouds_yaml)):
37   	        fail_usage("Failed: ~/.config/openstack/clouds.yaml and /etc/openstack/clouds.yaml does not exist")
38   	
39   	    clouds_yaml = os.path.expanduser(clouds_yaml)
40   	    if os.path.exists(clouds_yaml):
41   	        with open(clouds_yaml, "r") as yaml_stream:
42   	            try:
43   	                clouds = yaml.safe_load(yaml_stream)
44   	            except yaml.YAMLError as exc:
45   	                fail_usage("Failed: Unable to read: " + clouds_yaml)
46   	
47   	    cloud = clouds.get("clouds").get(options["--cloud"])
48   	    if not cloud:
49   	        fail_usage("Cloud: {} not found.".format(options["--cloud"]))
50   	
51   	    return cloud
52   	
53   	
54   	def get_nodes_list(conn, options):
55   	    logging.info("Running %s action", options.get("--original-action", options.get("--action")))
56   	    result = {}
57   	    search_opts = {}
58   	    max_results = 1 if options.get("--original-action") == "monitor" else None
59   	
60   	    if "--plug" in options:
61   	        search_opts["uuid"] = options["--plug"]
62   	
63   	    try:
64   	        response = conn.servers.list(detailed=True, search_opts=search_opts, limit=max_results)
65   	        if response is not None:
66   	            for item in response:
67   	                instance_id = item.id
68   	                instance_name = item.name
69   	                instance_status = item.status
70   	                result[instance_id] = (instance_name, translate_status(instance_status))
71   	    except Exception as e:
72   	        logging.error("Failed to retrieve node list: %s", e)
73   	    return result
74   	
75   	
76   	def get_power_status(conn, options):
77   	    logging.info("Running %s action on %s", options["--action"], options["--plug"])
78   	    server = None
79   	    try:
(1) Event path: Falling through to end of try statement.
80   	        server = conn.servers.get(options["--plug"])
81   	    except NotFound as e:
82   	        fail_usage("Failed: Not Found: " + str(e))
(2) Event path: Condition "server === None", taking true branch.
(3) Event null_check: Comparing "server" to a null-like value implies that "server" might be null-like.
Also see events: [property_access]
83   	    if server is None:
84   	        fail_usage("Server %s not found", options["--plug"])
CID (unavailable; MK=b0077c5747be6ffbad90067b77ab6eac) (#1 of 1): Bad use of null-like value (FORWARD_NULL):
(4) Event property_access: Accessing a property of null-like value "server".
Also see events: [null_check]
85   	    state = server.status
86   	    status = translate_status(state)
87   	    logging.info("get_power_status: %s (state: %s)" % (status, state))
88   	    return status
89   	
90   	
91   	def set_power_status(conn, options):
92   	    logging.info("Running %s action on %s", options["--action"], options["--plug"])
93   	    action = options["--action"]
94   	    server = None
95   	    try:
96   	        server = conn.servers.get(options["--plug"])
97   	    except NotFound as e:
98   	        fail_usage("Failed: Not Found: " + str(e))
99   	    if server is None:
100  	        fail_usage("Server %s not found", options["--plug"])
101  	    if action == "on":
102  	        logging.info("Starting instance " + server.name)
103  	        try:
104  	            server.start()
105  	        except Conflict as e:
106  	            fail_usage(e)
107  	        logging.info("Called start API call for " + server.id)
108  	    if action == "off":
109  	        logging.info("Stopping instance " + server.name)
110  	        try:
111  	            server.stop()
112  	        except Conflict as e:
113  	            fail_usage(e)
114  	        logging.info("Called stop API call for " + server.id)
115  	    if action == "reboot":
116  	        logging.info("Rebooting instance " + server.name)
117  	        try:
118  	            server.reboot("HARD")
119  	        except Conflict as e:
120  	            fail_usage(e)
121  	        logging.info("Called reboot hard API call for " + server.id)
122  	
123  	
124  	def nova_login(username, password, projectname, auth_url, user_domain_name,
125  	               project_domain_name, ssl_insecure, cacert, apitimeout,
126  	               auth_plugin="password", auth_options=None):
127  	    legacy_import = False
128  	
129  	    try:
130  	        from keystoneauth1 import loading
131  	        from keystoneauth1 import session as ksc_session
132  	        from keystoneauth1.exceptions.discovery import DiscoveryFailure
133  	        from keystoneauth1.exceptions.http import Unauthorized
134  	    except ImportError:
135  	        try:
136  	            from keystoneclient import session as ksc_session
137  	            from keystoneclient.auth.identity import v3
138  	
139  	            legacy_import = True
140  	        except ImportError:
141  	            fail_usage("Failed: Keystone client not found or not accessible")
142  	
143  	    if not legacy_import:
144  	        loader = loading.get_plugin_loader(auth_plugin)
145  	        if auth_options is None:
146  	            auth_options = dict(
147  	                auth_url=auth_url,
148  	                username=username,
149  	                password=password,
150  	                project_name=projectname,
151  	                user_domain_name=user_domain_name,
152  	                project_domain_name=project_domain_name,
153  	            )
154  	        auth = loader.load_from_options(**auth_options)
155  	    else:
156  	        if auth_plugin != "password":
157  	            fail_usage("Failed: Keystone auth plugins require keystoneauth1")
158  	        auth = v3.Password(
159  	            auth_url=auth_url,
160  	            username=username,
161  	            password=password,
162  	            project_name=projectname,
163  	            user_domain_name=user_domain_name,
164  	            project_domain_name=project_domain_name,
165  	            cacert=cacert,
166  	        )
167  	
168  	    caverify=True
169  	    if ssl_insecure:
170  	        caverify=False
171  	    elif cacert:
172  	        caverify=cacert
173  	
174  	    session = ksc_session.Session(auth=auth, verify=caverify, timeout=apitimeout)
175  	    nova = client.Client("2", session=session, timeout=apitimeout)
176  	    apiversion = None
177  	    try:
178  	        apiversion = nova.versions.get_current()
179  	    except DiscoveryFailure as e:
180  	        fail_usage("Failed: Discovery Failure: " + str(e))
181  	    except Unauthorized as e:
182  	        fail_usage("Failed: Unauthorized: " + str(e))
183  	    except Exception as e:
184  	        logging.error(e)
185  	    logging.debug("Nova version: %s", apiversion)
186  	    return nova
187  	
188  	
189  	def define_new_opts():
190  	    all_opt["auth-url"] = {
191  	        "getopt": ":",
192  	        "longopt": "auth-url",
193  	        "help": "--auth-url=[authurl]           Keystone Auth URL",
194  	        "required": "0",
195  	        "shortdesc": "Keystone Auth URL",
196  	        "order": 2,
197  	    }
198  	    all_opt["project-name"] = {
199  	        "getopt": ":",
200  	        "longopt": "project-name",
201  	        "help": "--project-name=[project]       Tenant Or Project Name",
202  	        "required": "0",
203  	        "shortdesc": "Keystone Project",
204  	        "default": "admin",
205  	        "order": 3,
206  	    }
207  	    all_opt["user-domain-name"] = {
208  	        "getopt": ":",
209  	        "longopt": "user-domain-name",
210  	        "help": "--user-domain-name=[domain]    Keystone User Domain Name",
211  	        "required": "0",
212  	        "shortdesc": "Keystone User Domain Name",
213  	        "default": "Default",
214  	        "order": 4,
215  	    }
216  	    all_opt["project-domain-name"] = {
217  	        "getopt": ":",
218  	        "longopt": "project-domain-name",
219  	        "help": "--project-domain-name=[domain] Keystone Project Domain Name",
220  	        "required": "0",
221  	        "shortdesc": "Keystone Project Domain Name",
222  	        "default": "Default",
223  	        "order": 5,
224  	    }
225  	    all_opt["cloud"] = {
226  	        "getopt": ":",
227  	        "longopt": "cloud",
228  	        "help": "--cloud=[cloud]              Openstack cloud (from ~/.config/openstack/clouds.yaml or /etc/openstack/clouds.yaml).",
229  	        "required": "0",
230  	        "shortdesc": "Cloud from clouds.yaml",
231  	        "order": 6,
232  	    }
233  	    all_opt["openrc"] = {
234  	        "getopt": ":",
235  	        "longopt": "openrc",
236  	        "help": "--openrc=[openrc]              Path to the openrc config file",
237  	        "required": "0",
238  	        "shortdesc": "openrc config file",
239  	        "order": 7,
240  	    }
241  	    all_opt["uuid"] = {
242  	        "getopt": ":",
243  	        "longopt": "uuid",
244  	        "help": "--uuid=[uuid]                  Replaced by -n, --plug",
245  	        "required": "0",
246  	        "shortdesc": "Replaced by port/-n/--plug",
247  	        "order": 8,
248  	    }
249  	    all_opt["cacert"] = {
250  	        "getopt": ":",
251  	        "longopt": "cacert",
252  	        "help": "--cacert=[cacert]              Path to the PEM file with trusted authority certificates (override global CA trust)",
253  	        "required": "0",
254  	        "shortdesc": "SSL X.509 certificates file",
255  	        "default": "",
256  	        "order": 9,
257  	    }
258  	    all_opt["apitimeout"] = {
259  	        "getopt": ":",
260  	        "type": "second",
261  	        "longopt": "apitimeout",
262  	        "help": "--apitimeout=[seconds]         Timeout to use for API calls",
263  	        "shortdesc": "Timeout in seconds to use for API calls, default is 60.",
264  	        "required": "0",
265  	        "default": 60,
266  	        "order": 10,
267  	    }
268  	    all_opt["auth-plugin"] = {
269  	        "getopt": ":",
270  	        "longopt": "auth-plugin",
271  	        "help": "--auth-plugin=[plugin]         Keystone auth plugin",
272  	        "required": "0",
273  	        "shortdesc": "Keystone auth plugin",
274  	        "default": "password",
275  	        "order": 11,
276  	    }
277  	
278  	
279  	def main():
280  	    conn = None
281  	
282  	    device_opt = [
283  	        "login",
284  	        "no_login",
285  	        "passwd",
286  	        "no_password",
287  	        "auth-url",
288  	        "project-name",
289  	        "user-domain-name",
290  	        "project-domain-name",
291  	        "auth-plugin",
292  	        "cloud",
293  	        "openrc",
294  	        "port",
295  	        "no_port",
296  	        "uuid",
297  	        "ssl_insecure",
298  	        "cacert",
299  	        "apitimeout",
300  	    ]
301  	
302  	    atexit.register(atexit_handler)
303  	
304  	    define_new_opts()
305  	
306  	    all_opt["port"]["required"] = "0"
307  	    all_opt["port"]["help"] = "-n, --plug=[UUID]              UUID of the node to be fenced"
308  	    all_opt["port"]["shortdesc"] = "UUID of the node to be fenced."
309  	    all_opt["power_timeout"]["default"] = "60"
310  	
311  	    options = check_input(device_opt, process_input(device_opt))
312  	
313  	    # workaround to avoid regressions
314  	    if "--uuid" in options:
315  	        options["--plug"] = options["--uuid"]
316  	        del options["--uuid"]
317  	    elif ("--help" not in options
318  	          and options["--action"] in ["off", "on", "reboot", "status", "validate-all"]
319  	          and "--plug" not in options):
320  	        stop_after_error = False if options["--action"] == "validate-all" else True
321  	        fail_usage(
322  	            "Failed: You have to enter plug number or machine identification",
323  	            stop_after_error,
324  	        )
325  	
326  	    docs = {}
327  	    docs["shortdesc"] = "Fence agent for OpenStack's Nova service"
328  	    docs["longdesc"] = "fence_openstack is a Power Fencing agent \
329  	which can be used with machines controlled by the Openstack's Nova service. \
330  	This agent calls the python-novaclient and it is mandatory to be installed "
331  	    docs["vendorurl"] = "https://wiki.openstack.org/wiki/Nova"
332  	    show_docs(options, docs)
333  	
334  	    run_delay(options)
335  	
336  	    auth_options = None
337  	    if options.get("--cloud"):
338  	        cloud = get_cloud(options)
339  	        if options["--auth-plugin"] == "password":
340  	            options["--auth-plugin"] = cloud.get("auth_type") or options["--auth-plugin"]
341  	        if options["--auth-plugin"] != "password":
342  	            auth_options = cloud.get("auth")
343  	        username = cloud.get("auth").get("username")
344  	        password = cloud.get("auth").get("password")
345  	        projectname = cloud.get("auth").get("project_name")
346  	        auth_url = None
347  	        try:
348  	            auth_url = cloud.get("auth").get("auth_url")
349  	        except KeyError:
350  	            fail_usage("Failed: You have to set the Keystone service endpoint for authorization")
351  	        user_domain_name = cloud.get("auth").get("user_domain_name")
352  	        project_domain_name = cloud.get("auth").get("project_domain_name")
353  	        caverify = cloud.get("verify")
354  	        if caverify in [True, False]:
355  	                options["--ssl-insecure"] = caverify
356  	        else:
357  	                options["--cacert"] = caverify
358  	    elif options.get("--openrc"):
359  	        if not os.path.exists(os.path.expanduser(options["--openrc"])):
360  	            fail_usage("Failed: {} does not exist".format(options.get("--openrc")))
361  	        source_env(options["--openrc"])
362  	        env = os.environ
363  	        username = env.get("OS_USERNAME")
364  	        password = env.get("OS_PASSWORD")
365  	        projectname = env.get("OS_PROJECT_NAME")
366  	        auth_url = None
367  	        try:
368  	            auth_url = env["OS_AUTH_URL"]
369  	        except KeyError:
370  	            fail_usage("Failed: You have to set the Keystone service endpoint for authorization")
371  	        user_domain_name = env.get("OS_USER_DOMAIN_NAME")
372  	        project_domain_name = env.get("OS_PROJECT_DOMAIN_NAME")
373  	    else:
374  	        username = options["--username"]
375  	        password = options["--password"]
376  	        projectname = options["--project-name"]
377  	        auth_url = None
378  	        try:
379  	            auth_url = options["--auth-url"]
380  	        except KeyError:
381  	            fail_usage("Failed: You have to set the Keystone service endpoint for authorization")
382  	        user_domain_name = options["--user-domain-name"]
383  	        project_domain_name = options["--project-domain-name"]
384  	
385  	    ssl_insecure = "--ssl-insecure" in options
386  	    cacert = options["--cacert"]
387  	    apitimeout = options["--apitimeout"]
388  	
389  	    try:
390  	        conn = nova_login(
391  	            username,
392  	            password,
393  	            projectname,
394  	            auth_url,
395  	            user_domain_name,
396  	            project_domain_name,
397  	            ssl_insecure,
398  	            cacert,
399  	            apitimeout,
400  	            options["--auth-plugin"],
401  	            auth_options,
402  	        )
403  	    except Exception as e:
404  	        fail_usage("Failed: Unable to connect to Nova: " + str(e))
405  	
406  	    # Operate the fencing device
407  	    result = fence_action(conn, options, set_power_status, get_power_status, get_nodes_list)
408  	    sys.exit(result)
409  	
410  	
411  	if __name__ == "__main__":
412  	    main()
413