1    	#!@PYTHON@ -tt
2    	
3    	# Copyright (c) 2018 Dell Inc. or its subsidiaries. All Rights Reserved.
4    	
5    	# Fence agent for devices that support the Redfish API Specification.
6    	
7    	import sys
8    	import re
9    	import logging
10   	import json
11   	import requests
12   	import atexit
13   	sys.path.append("@FENCEAGENTSLIBDIR@")
14   	
15   	from fencing import *
16   	from fencing import fail_usage, run_delay
17   	
18   	GET_HEADERS = {'accept': 'application/json', 'OData-Version': '4.0'}
19   	POST_HEADERS = {'content-type': 'application/json', 'accept': 'application/json',
20   	                'OData-Version': '4.0'}
21   	
22   	
23   	def get_power_status(conn, options):
24   	    response = send_get_request(options, options["--systems-uri"])
25   	    if response['ret'] is False:
26   	        fail_usage("Couldn't get power information")
27   	    data = response['data']
28   	
29   	    try:
30   	        logging.debug("PowerState is: " + data[u'PowerState'])
31   	    except Exception:
32   	        fail_usage("Unable to get PowerState: " + "https://" + options["--ip"] + ":" + str(options["--ipport"]) + options["--systems-uri"])
33   	
34   	    if data[u'PowerState'].strip() == "Off":
35   	        return "off"
36   	    else:
37   	        return "on"
38   	
39   	def set_power_status(conn, options):
40   	    action = {
41   	        'on' : "On",
42   	        'off': "ForceOff",
43   	        'reboot': "ForceRestart",
44   	        'diag': "Nmi"
45   	    }[options.get("original-action") or options["--action"]]
46   	
47   	    payload = {'ResetType': action}
48   	
49   	    # Search for 'Actions' key and extract URI from it
50   	    response = send_get_request(options, options["--systems-uri"])
51   	    if response['ret'] is False:
52   	        return {'ret': False}
53   	    data = response['data']
54   	    action_uri = data["Actions"]["#ComputerSystem.Reset"]["target"]
55   	
56   	    response = send_post_request(options, action_uri, payload)
57   	    if response['ret'] is False:
58   	        fail_usage("Error sending power command")
59   	    if options.get("original-action") == "diag":
60   	        return True
61   	    return
62   	
63   	def send_get_request(options, uri):
64   	    full_uri = "https://" + options["--ip"] + ":" + str(options["--ipport"]) + uri
65   	    try:
(1) Event Sigma main event: The application sets `Basic Authentication` credentials in the HTTP request. This means that an HTTP header containing the Base64 encoded username and password will be included in requests made to the server. This exposes user credentials and prevents token rotation.
(2) Event remediation: Modify the application so that it does not rely on `Basic Authentication`. Consider using a modern authentication scheme that that will not leak user credentials easily.
66   	        resp = requests.get(full_uri, verify=not "--ssl-insecure" in options,
67   	                            headers=GET_HEADERS,
68   	                            auth=(options["--username"], options["--password"]))
69   	        data = resp.json()
70   	    except Exception as e:
71   	        fail_usage("Failed: send_get_request: " + str(e))
72   	    return {'ret': True, 'data': data}
73   	
74   	def send_post_request(options, uri, payload):
75   	    full_uri = "https://" + options["--ip"] + ":" + str(options["--ipport"]) + uri
76   	    try:
77   	        requests.post(full_uri, data=json.dumps(payload),
78   	                      headers=POST_HEADERS, verify=not "--ssl-insecure" in options,
79   	                      auth=(options["--username"], options["--password"]))
80   	    except Exception as e:
81   	        fail_usage("Failed: send_post_request: " + str(e))
82   	    return {'ret': True}
83   	
84   	def find_systems_resource(options):
85   	    response = send_get_request(options, options["--redfish-uri"])
86   	    if response['ret'] is False:
87   	        return {'ret': False}
88   	    data = response['data']
89   	
90   	    if 'Systems' not in data:
91   	        # Systems resource not found"
92   	        return {'ret': False}
93   	    else:
94   	        response = send_get_request(options, data["Systems"]["@odata.id"])
95   	        if response['ret'] is False:
96   	            return {'ret': False}
97   	        data = response['data']
98   	
99   	        # need to be able to handle more than one entry
100  	        for member in data[u'Members']:
101  	            system_uri = member[u'@odata.id']
102  	        return {'ret': True, 'uri': system_uri}
103  	
104  	def define_new_opts():
105  	    all_opt["redfish-uri"] = {
106  	        "getopt" : ":",
107  	        "longopt" : "redfish-uri",
108  	        "help" : "--redfish-uri=[uri]            Base or starting Redfish URI",
109  	        "required" : "0",
110  	        "default" : "/redfish/v1",
111  	        "shortdesc" : "Base or starting Redfish URI",
112  	        "order": 1
113  	    }
114  	    all_opt["systems-uri"] = {
115  	        "getopt" : ":",
116  	        "longopt" : "systems-uri",
117  	        "help" : "--systems-uri=[uri]            Redfish Systems resource URI",
118  	        "required" : "0",
119  	        "shortdesc" : "Redfish Systems resource URI, i.e. /redfish/v1/Systems/System.Embedded.1",
120  	        "order": 1
121  	    }
122  	
123  	def main():
124  	    atexit.register(atexit_handler)
125  	    device_opt = ["ipaddr", "login", "passwd", "redfish-uri", "systems-uri",
126  	                  "ssl", "diag"]
127  	    define_new_opts()
128  	
129  	    opt = process_input(device_opt)
130  	
131  	    all_opt["ssl"]["default"] = "1"
132  	    options = check_input(device_opt, opt)
133  	
134  	    docs = {}
135  	    docs["shortdesc"] = "Power Fencing agent for Redfish"
136  	    docs["longdesc"] = "fence_redfish is a Power Fencing agent which can be used with \
137  	Out-of-Band controllers that support Redfish APIs. These controllers provide remote \
138  	access to control power on a server."
139  	    docs["vendorurl"] = "http://www.dmtf.org"
140  	    show_docs(options, docs)
141  	    run_delay(options)
142  	
143  	    ##
144  	    ## Operate the fencing device
145  	    ####
146  	
147  	    # Disable insecure-certificate-warning message
148  	    if "--ssl-insecure" in opt:
149  	        import urllib3
150  	        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
151  	
152  	    # backwards compatibility for <ip>:<port>
153  	    if options["--ip"].count(":") == 1:
154  	        (options["--ip"], options["--ipport"]) = options["--ip"].split(":")
155  	
156  	    if "--systems-uri" not in opt:
157  	        # Systems URI not provided, find it
158  	        sysresult = find_systems_resource(options)
159  	        if sysresult['ret'] is False:
160  	            sys.exit(1)
161  	        else:
162  	            options["--systems-uri"] = sysresult["uri"]
163  	
164  	    reboot_fn = None
165  	    if options["--action"] == "diag":
166  	        # Diag is a special action that can't be verified so we will reuse reboot functionality
167  	        # to minimize impact on generic library
168  	        options["original-action"] = options["--action"]
169  	        options["--action"] = "reboot"
170  	        options["--method"] = "cycle"
171  	        reboot_fn = set_power_status
172  	
173  	    result = fence_action(None, options, set_power_status, get_power_status, None, reboot_fn)
174  	    sys.exit(result)
175  	
176  	if __name__ == "__main__":
177  	    main()
178