1#!/usr/bin/env python3
2
3r"""
4See redfish_plus class prolog below for details.
5"""
6
7import json
8
9import func_args as fa
10import gen_print as gp
11import requests
12from redfish.rest.v1 import HttpClient
13from robot.libraries.BuiltIn import BuiltIn
14
15host = BuiltIn().get_variable_value("${OPENBMC_HOST}")
16MTLS_ENABLED = BuiltIn().get_variable_value("${MTLS_ENABLED}")
17CERT_DIR_PATH = BuiltIn().get_variable_value("${CERT_DIR_PATH}")
18VALID_CERT = BuiltIn().get_variable_value("${VALID_CERT}")
19
20
21def valid_http_status_code(status, valid_status_codes):
22    r"""
23    Raise exception if status is not found in the valid_status_codes list.
24
25    Description of argument(s):
26    status                          An HTTP status code (e.g. 200, 400, etc.).
27    valid_status_codes              A list of status codes that the caller considers acceptable.  If this is
28                                    a null list, then any status code is considered acceptable.  Note that
29                                    for the convenience of the caller, valid_status_codes may be either a
30                                    python list or a string which can be evaluated to become a python list
31                                    (e.g. "[200]").
32    """
33
34    if type(valid_status_codes) is not list:
35        valid_status_codes = eval(valid_status_codes)
36    if len(valid_status_codes) == 0:
37        return
38    if status in valid_status_codes:
39        return
40
41    message = "The HTTP status code was not valid:\n"
42    message += gp.sprint_vars(status, valid_status_codes)
43    raise ValueError(message)
44
45
46class redfish_plus(HttpClient):
47    r"""
48    redfish_plus is a wrapper for redfish rest that provides the following benefits vs. using redfish
49    directly:
50
51    For rest_request functions (e.g. get, put, post, etc.):
52        - Function-call logging to stdout.
53        - Automatic valid_status_codes processing (i.e. an exception will be raised if the rest response
54          status code is not as expected.
55        - Easily used from robot programs.
56    """
57
58    ROBOT_LIBRARY_SCOPE = "TEST SUITE"
59
60    def rest_request(self, func, *args, **kwargs):
61        r"""
62        Perform redfish rest request and return response.
63
64        This function provides the following additional functionality.
65        - The calling function's call line is logged to standard out (provided that global variable "quiet"
66          is not set).
67        - The caller may include a valid_status_codes argument.
68        - Callers may include inline python code strings to define arguments.  This predominantly benefits
69          robot callers.
70
71          For example, instead of calling like this:
72            ${data}=  Create Dictionary  HostName=${hostname}
73            Redfish.patch  ${REDFISH_NW_PROTOCOL_URI}  body=&{data}
74
75          Callers may do this:
76
77            Redfish.patch  ${REDFISH_NW_PROTOCOL_URI}
78            ...  body=[('HostName', '${hostname}')]
79
80        Description of argument(s):
81        func                        A reference to the parent class function which is to be called (e.g. get,
82                                    put, etc.).
83        args                        This is passed directly to the function referenced by the func argument
84                                    (see the documentation for the corresponding redfish HttpClient method
85                                    for details).
86        kwargs                      This is passed directly to the function referenced by the func argument
87                                    (see the documentation for the corresponding redfish HttpClient method
88                                    for details) with the following exception:  If kwargs contains a
89                                    valid_status_codes key, it will be removed from kwargs and processed by
90                                    this function.  This allows the caller to indicate what rest status codes
91                                    are acceptable.  The default value is [200].  See the
92                                    valid_http_status_code function above for details.
93
94        Example uses:
95
96        From a python program:
97
98        response = bmc_redfish.get("/redfish/v1/Managers/${MANAGER_ID}/EthernetInterfaces", [200, 201])
99
100        If this call to the get method generates a response.status equal to anything other than 200 or 201,
101        an exception will be raised.
102
103        From a robot program:
104
105        BMC_Redfish.logout
106        ${response}=  BMC_Redfish.Get  /redfish/v1/Managers/${MANAGER_ID}/EthernetInterfaces  valid_status_codes=[401]
107
108        As part of a robot test, the programmer has logged out to verify that the get request will generate a
109        status code of 401 (i.e. "Unauthorized").
110
111        Timeout for GET/POST/PATCH/DELETE operations. By default 30 seconds, else user defined value.
112        Similarly, Max retry by default 10 attempt for the operation, else user defined value.
113        """
114        gp.qprint_executing(stack_frame_ix=3, style=gp.func_line_style_short)
115        # Convert python string object definitions to objects (mostly useful for robot callers).
116        args = fa.args_to_objects(args)
117        kwargs = fa.args_to_objects(kwargs)
118        timeout = kwargs.pop("timeout", 30)
119        self._timeout = timeout
120        max_retry = kwargs.pop("max_retry", 10)
121        self._max_retry = max_retry
122        valid_status_codes = kwargs.pop("valid_status_codes", [200])
123        response = func(*args, **kwargs)
124        valid_http_status_code(response.status, valid_status_codes)
125        return response
126
127    # Define rest function wrappers.
128    def get(self, *args, **kwargs):
129        if MTLS_ENABLED == "True":
130            return self.rest_request(self.get_with_mtls, *args, **kwargs)
131        else:
132            return self.rest_request(
133                super(redfish_plus, self).get, *args, **kwargs
134            )
135
136    def head(self, *args, **kwargs):
137        if MTLS_ENABLED == "True":
138            return self.rest_request(self.head_with_mtls, *args, **kwargs)
139        else:
140            return self.rest_request(
141                super(redfish_plus, self).head, *args, **kwargs
142            )
143
144    def post(self, *args, **kwargs):
145        if MTLS_ENABLED == "True":
146            return self.rest_request(self.post_with_mtls, *args, **kwargs)
147        else:
148            return self.rest_request(
149                super(redfish_plus, self).post, *args, **kwargs
150            )
151
152    def put(self, *args, **kwargs):
153        if MTLS_ENABLED == "True":
154            return self.rest_request(self.put_with_mtls, *args, **kwargs)
155        else:
156            return self.rest_request(
157                super(redfish_plus, self).put, *args, **kwargs
158            )
159
160    def patch(self, *args, **kwargs):
161        if MTLS_ENABLED == "True":
162            return self.rest_request(self.patch_with_mtls, *args, **kwargs)
163        else:
164            return self.rest_request(
165                super(redfish_plus, self).patch, *args, **kwargs
166            )
167
168    def delete(self, *args, **kwargs):
169        if MTLS_ENABLED == "True":
170            return self.rest_request(self.delete_with_mtls, *args, **kwargs)
171        else:
172            return self.rest_request(
173                super(redfish_plus, self).delete, *args, **kwargs
174            )
175
176    def __del__(self):
177        del self
178
179    def get_with_mtls(self, *args, **kwargs):
180        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
181        response = requests.get(
182            url="https://" + host + args[0],
183            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
184            verify=False,
185            headers={"Cache-Control": "no-cache"},
186        )
187
188        response.status = response.status_code
189        if response.status == 200:
190            response.dict = json.loads(response.text)
191
192        return response
193
194    def post_with_mtls(self, *args, **kwargs):
195        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
196        body = kwargs.pop("body", {})
197        response = requests.post(
198            url="https://" + host + args[0],
199            json=body,
200            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
201            verify=False,
202            headers={"Content-Type": "application/json"},
203        )
204
205        response.status = response.status_code
206
207        return response
208
209    def patch_with_mtls(self, *args, **kwargs):
210        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
211        body = kwargs.pop("body", {})
212        response = requests.patch(
213            url="https://" + host + args[0],
214            json=body,
215            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
216            verify=False,
217            headers={"Content-Type": "application/json"},
218        )
219
220        response.status = response.status_code
221
222        return response
223
224    def delete_with_mtls(self, *args, **kwargs):
225        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
226        response = requests.delete(
227            url="https://" + host + args[0],
228            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
229            verify=False,
230            headers={"Content-Type": "application/json"},
231        )
232
233        response.status = response.status_code
234
235        return response
236
237    def put_with_mtls(self, *args, **kwargs):
238        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
239        body = kwargs.pop("body", {})
240        response = requests.put(
241            url="https://" + host + args[0],
242            json=body,
243            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
244            verify=False,
245            headers={"Content-Type": "application/json"},
246        )
247
248        response.status = response.status_code
249
250        return response
251
252    def head_with_mtls(self, *args, **kwargs):
253        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
254        body = kwargs.pop("body", {})
255        response = requests.head(
256            url="https://" + host + args[0],
257            json=body,
258            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
259            verify=False,
260            headers={"Content-Type": "application/json"},
261        )
262
263        response.status = response.status_code
264
265        return response
266