xref: /openbmc/openbmc-test-automation/lib/redfish_plus.py (revision 01a503ccb180d12f943526cf0a0ccf86fcaaa8d8)
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        # Convert python string object definitions to objects (mostly useful for robot callers).
115        args = fa.args_to_objects(args)
116        kwargs = fa.args_to_objects(kwargs)
117        timeout = kwargs.pop("timeout", 30)
118        self._timeout = timeout
119        max_retry = kwargs.pop("max_retry", 10)
120        self._max_retry = max_retry
121        valid_status_codes = kwargs.pop("valid_status_codes", [200])
122
123        # /redfish/v1/ does not require authentication and start of the test
124        # is dumping 3 entries at the beginning which is excessive.
125        # args[0] position is always the URI for redfish request
126        # Example:
127        #      ('/redfish/v1/',)
128        #      ('/redfish/v1/Managers/bmc',)
129        # Skip logging if matches /redfish/v1/
130        if args[0] != "/redfish/v1/":
131            gp.qprint_executing(
132                stack_frame_ix=3, style=gp.func_line_style_short
133            )
134
135        try:
136            response = func(*args, **kwargs)
137        except Exception as e:
138            error_response = type(e).__name__ + " from redfish_plus class"
139            BuiltIn().log_to_console(error_response)
140            return
141
142        valid_http_status_code(response.status, valid_status_codes)
143        return response
144
145    # Define rest function wrappers.
146    def get(self, *args, **kwargs):
147        if MTLS_ENABLED == "True":
148            return self.rest_request(self.get_with_mtls, *args, **kwargs)
149        else:
150            return self.rest_request(
151                super(redfish_plus, self).get, *args, **kwargs
152            )
153
154    def head(self, *args, **kwargs):
155        if MTLS_ENABLED == "True":
156            return self.rest_request(self.head_with_mtls, *args, **kwargs)
157        else:
158            return self.rest_request(
159                super(redfish_plus, self).head, *args, **kwargs
160            )
161
162    def post(self, *args, **kwargs):
163        if MTLS_ENABLED == "True":
164            return self.rest_request(self.post_with_mtls, *args, **kwargs)
165        else:
166            return self.rest_request(
167                super(redfish_plus, self).post, *args, **kwargs
168            )
169
170    def put(self, *args, **kwargs):
171        if MTLS_ENABLED == "True":
172            return self.rest_request(self.put_with_mtls, *args, **kwargs)
173        else:
174            return self.rest_request(
175                super(redfish_plus, self).put, *args, **kwargs
176            )
177
178    def patch(self, *args, **kwargs):
179        if MTLS_ENABLED == "True":
180            return self.rest_request(self.patch_with_mtls, *args, **kwargs)
181        else:
182            return self.rest_request(
183                super(redfish_plus, self).patch, *args, **kwargs
184            )
185
186    def delete(self, *args, **kwargs):
187        if MTLS_ENABLED == "True":
188            return self.rest_request(self.delete_with_mtls, *args, **kwargs)
189        else:
190            return self.rest_request(
191                super(redfish_plus, self).delete, *args, **kwargs
192            )
193
194    def __del__(self):
195        del self
196
197    def get_with_mtls(self, *args, **kwargs):
198        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
199        response = requests.get(
200            url="https://" + host + args[0],
201            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
202            verify=False,
203            headers={"Cache-Control": "no-cache"},
204        )
205
206        response.status = response.status_code
207        if response.status == 200:
208            response.dict = json.loads(response.text)
209
210        return response
211
212    def post_with_mtls(self, *args, **kwargs):
213        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
214        body = kwargs.pop("body", {})
215        response = requests.post(
216            url="https://" + host + args[0],
217            json=body,
218            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
219            verify=False,
220            headers={"Content-Type": "application/json"},
221        )
222
223        response.status = response.status_code
224
225        return response
226
227    def patch_with_mtls(self, *args, **kwargs):
228        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
229        body = kwargs.pop("body", {})
230        response = requests.patch(
231            url="https://" + host + args[0],
232            json=body,
233            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
234            verify=False,
235            headers={"Content-Type": "application/json"},
236        )
237
238        response.status = response.status_code
239
240        return response
241
242    def delete_with_mtls(self, *args, **kwargs):
243        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
244        response = requests.delete(
245            url="https://" + host + args[0],
246            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
247            verify=False,
248            headers={"Content-Type": "application/json"},
249        )
250
251        response.status = response.status_code
252
253        return response
254
255    def put_with_mtls(self, *args, **kwargs):
256        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
257        body = kwargs.pop("body", {})
258        response = requests.put(
259            url="https://" + host + args[0],
260            json=body,
261            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
262            verify=False,
263            headers={"Content-Type": "application/json"},
264        )
265
266        response.status = response.status_code
267
268        return response
269
270    def head_with_mtls(self, *args, **kwargs):
271        cert_dict = kwargs.pop("certificate", {"certificate_name": VALID_CERT})
272        body = kwargs.pop("body", {})
273        response = requests.head(
274            url="https://" + host + args[0],
275            json=body,
276            cert=CERT_DIR_PATH + "/" + cert_dict["certificate_name"],
277            verify=False,
278            headers={"Content-Type": "application/json"},
279        )
280
281        response.status = response.status_code
282
283        return response
284