blob: a4277b2f2c2cc204ac33acdf9fb49df689800402 [file] [log] [blame]
Yuiko Takadab6527002015-12-07 11:49:12 +09001# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
Yuiko Takadab6527002015-12-07 11:49:12 +090013
Takashi Kajinami6bddaab2022-05-10 00:58:56 +090014import functools
15from http import client as http_client
16from urllib import parse as urllib_parse
17
Yuiko Takadab6527002015-12-07 11:49:12 +090018from oslo_serialization import jsonutils as json
Yuiko Takadaff785002015-12-17 15:56:42 +090019from tempest.lib.common import api_version_utils
Lenny Verkhovsky88625042016-03-08 17:44:00 +020020from tempest.lib.common import rest_client
Yuiko Takadab6527002015-12-07 11:49:12 +090021
Vasyl Saienko4ddbeec2017-01-20 16:26:04 +000022# NOTE(vsaienko): concurrent tests work because they are launched in
23# separate processes so global variables are not shared among them.
Yuiko Takadaff785002015-12-17 15:56:42 +090024BAREMETAL_MICROVERSION = None
25
Dmitry Tantsurb294d962024-08-08 15:00:50 +020026# Interfaces that can be set via the baremetal client and by logic in scenario
27# managers.
28SUPPORTED_INTERFACES = ['bios', 'deploy', 'rescue', 'boot', 'raid',
29 'management', 'power', 'inspect']
30
Yuiko Takadab6527002015-12-07 11:49:12 +090031
Vasyl Saienko4ddbeec2017-01-20 16:26:04 +000032def set_baremetal_api_microversion(baremetal_microversion):
33 global BAREMETAL_MICROVERSION
34 BAREMETAL_MICROVERSION = baremetal_microversion
35
36
37def reset_baremetal_api_microversion():
38 global BAREMETAL_MICROVERSION
39 BAREMETAL_MICROVERSION = None
40
41
Yuiko Takadab6527002015-12-07 11:49:12 +090042def handle_errors(f):
43 """A decorator that allows to ignore certain types of errors."""
44
Takashi Kajinami6bddaab2022-05-10 00:58:56 +090045 @functools.wraps(f)
Yuiko Takadab6527002015-12-07 11:49:12 +090046 def wrapper(*args, **kwargs):
47 param_name = 'ignore_errors'
48 ignored_errors = kwargs.get(param_name, tuple())
49
50 if param_name in kwargs:
51 del kwargs[param_name]
52
53 try:
54 return f(*args, **kwargs)
55 except ignored_errors:
56 # Silently ignore errors
57 pass
58
59 return wrapper
60
61
62class BaremetalClient(rest_client.RestClient):
63 """Base Tempest REST client for Ironic API."""
64
Yuiko Takadaff785002015-12-17 15:56:42 +090065 api_microversion_header_name = 'X-OpenStack-Ironic-API-Version'
Yuiko Takadab6527002015-12-07 11:49:12 +090066 uri_prefix = ''
67
Yuiko Takadaff785002015-12-17 15:56:42 +090068 def get_headers(self):
69 headers = super(BaremetalClient, self).get_headers()
70 if BAREMETAL_MICROVERSION:
Julia Kreger0eb9ae72024-03-26 08:47:26 -070071 # NOTE(TheJulia): This is not great, because it can blind a test
72 # to the actual version supported.
Yuiko Takadaff785002015-12-17 15:56:42 +090073 headers[self.api_microversion_header_name] = BAREMETAL_MICROVERSION
74 return headers
75
Julia Kreger0eb9ae72024-03-26 08:47:26 -070076 def get_raw_headers(self):
77 """A proper get headers without guessing the microversion."""
78 return super(BaremetalClient, self).get_headers()
79
80 def get_min_max_api_microversions(self):
81 """Returns a tuple of minimum and remote microversions."""
Dmitry Tantsur7faed252024-09-05 09:57:52 +020082 if '/v1' in self.base_url:
83 root_uri = '/'
84 else:
85 # NOTE(dtantsur): we should just use / here but due to a bug in
86 # Ironic, / does not contain the microversion headers. See
87 # https://bugs.launchpad.net/ironic/+bug/2079023
88 root_uri = '/v1'
89 _, resp_body = self._show_request(None, uri=root_uri)
90 try:
91 version = resp_body['default_version']
92 except KeyError:
93 version = resp_body['version']
Julia Kreger0eb9ae72024-03-26 08:47:26 -070094 api_min = version.get('min_version')
95 api_max = version.get('version')
96 return (api_min, api_max)
97
Vasyl Saienkof20979c2016-05-27 11:25:01 +030098 def request(self, *args, **kwargs):
99 resp, resp_body = super(BaremetalClient, self).request(*args, **kwargs)
Riccardo Pittau441c5062020-03-30 15:06:28 +0200100 latest_microversion = api_version_utils.LATEST_MICROVERSION
101 if (BAREMETAL_MICROVERSION
102 and BAREMETAL_MICROVERSION != latest_microversion):
Yuiko Takadaff785002015-12-17 15:56:42 +0900103 api_version_utils.assert_version_header_matches_request(
104 self.api_microversion_header_name,
105 BAREMETAL_MICROVERSION,
106 resp)
107 return resp, resp_body
108
Yuiko Takadab6527002015-12-07 11:49:12 +0900109 def serialize(self, object_dict):
110 """Serialize an Ironic object."""
111
112 return json.dumps(object_dict)
113
114 def deserialize(self, object_str):
115 """Deserialize an Ironic object."""
116
117 return json.loads(object_str)
118
Dmitry Tantsure7548052018-07-16 17:48:32 +0200119 def _get_uri(self, resource_name, uuid=None, permanent=False,
120 params=None):
Yuiko Takadab6527002015-12-07 11:49:12 +0900121 """Get URI for a specific resource or object.
122
123 :param resource_name: The name of the REST resource, e.g., 'nodes'.
124 :param uuid: The unique identifier of an object in UUID format.
125 :returns: Relative URI for the resource or object.
126
127 """
128 prefix = self.uri_prefix if not permanent else ''
Dmitry Tantsure7548052018-07-16 17:48:32 +0200129 if params:
130 params = '?' + '&'.join('%s=%s' % tpl for tpl in params.items())
131 else:
132 params = ''
Yuiko Takadab6527002015-12-07 11:49:12 +0900133
Dmitry Tantsure7548052018-07-16 17:48:32 +0200134 return '{pref}/{res}{uuid}{params}'.format(
135 pref=prefix, res=resource_name,
136 uuid='/%s' % uuid if uuid else '',
137 params=params)
Yuiko Takadab6527002015-12-07 11:49:12 +0900138
139 def _make_patch(self, allowed_attributes, **kwargs):
140 """Create a JSON patch according to RFC 6902.
141
142 :param allowed_attributes: An iterable object that contains a set of
143 allowed attributes for an object.
144 :param **kwargs: Attributes and new values for them.
145 :returns: A JSON path that sets values of the specified attributes to
146 the new ones.
147
148 """
149 def get_change(kwargs, path='/'):
Luong Anh Tuane22fbe12016-09-12 16:32:14 +0700150 for name, value in kwargs.items():
Yuiko Takadab6527002015-12-07 11:49:12 +0900151 if isinstance(value, dict):
152 for ch in get_change(value, path + '%s/' % name):
153 yield ch
154 else:
155 if value is None:
156 yield {'path': path + name,
157 'op': 'remove'}
158 else:
159 yield {'path': path + name,
160 'value': value,
161 'op': 'replace'}
162
163 patch = [ch for ch in get_change(kwargs)
164 if ch['path'].lstrip('/') in allowed_attributes]
165
166 return patch
167
Sam Betts462e9e62016-11-30 18:43:35 +0000168 def _list_request(self, resource, permanent=False, headers=None,
169 extra_headers=False, **kwargs):
Yuiko Takadab6527002015-12-07 11:49:12 +0900170 """Get the list of objects of the specified type.
171
172 :param resource: The name of the REST resource, e.g., 'nodes'.
Sam Betts462e9e62016-11-30 18:43:35 +0000173 :param headers: List of headers to use in request.
174 :param extra_headers: Specify whether to use headers.
Yuiko Takadab6527002015-12-07 11:49:12 +0900175 :param **kwargs: Parameters for the request.
176 :returns: A tuple with the server response and deserialized JSON list
177 of objects
178
179 """
180 uri = self._get_uri(resource, permanent=permanent)
181 if kwargs:
Takashi Kajinami6bddaab2022-05-10 00:58:56 +0900182 uri += "?%s" % urllib_parse.urlencode(kwargs)
Yuiko Takadab6527002015-12-07 11:49:12 +0900183
Sam Betts462e9e62016-11-30 18:43:35 +0000184 resp, body = self.get(uri, headers=headers,
185 extra_headers=extra_headers)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600186 self.expected_success(http_client.OK, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900187
188 return resp, self.deserialize(body)
189
SofiiaAndriichenko569f0a42016-12-08 05:02:17 -0500190 def _show_request(self,
191 resource,
192 uuid=None,
193 permanent=False,
Mark Goddard56399cc2018-02-16 13:37:25 +0000194 headers=None,
195 extra_headers=False,
SofiiaAndriichenko569f0a42016-12-08 05:02:17 -0500196 **kwargs):
Yuiko Takadab6527002015-12-07 11:49:12 +0900197 """Gets a specific object of the specified type.
198
199 :param uuid: Unique identifier of the object in UUID format.
Mark Goddard56399cc2018-02-16 13:37:25 +0000200 :param headers: List of headers to use in request.
201 :param extra_headers: Specify whether to use headers.
Yuiko Takadab6527002015-12-07 11:49:12 +0900202 :returns: Serialized object as a dictionary.
203
204 """
205 if 'uri' in kwargs:
206 uri = kwargs['uri']
207 else:
208 uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
Mark Goddard56399cc2018-02-16 13:37:25 +0000209 resp, body = self.get(uri, headers=headers,
210 extra_headers=extra_headers)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600211 self.expected_success(http_client.OK, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900212
213 return resp, self.deserialize(body)
214
215 def _create_request(self, resource, object_dict):
216 """Create an object of the specified type.
217
218 :param resource: The name of the REST resource, e.g., 'nodes'.
219 :param object_dict: A Python dict that represents an object of the
220 specified type.
221 :returns: A tuple with the server response and the deserialized created
222 object.
223
224 """
225 body = self.serialize(object_dict)
226 uri = self._get_uri(resource)
227
228 resp, body = self.post(uri, body=body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600229 self.expected_success(http_client.CREATED, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900230
231 return resp, self.deserialize(body)
232
Sam Betts462e9e62016-11-30 18:43:35 +0000233 def _create_request_no_response_body(self, resource, object_dict):
234 """Create an object of the specified type.
235
236 Do not expect any body in the response.
237
238 :param resource: The name of the REST resource, e.g., 'nodes'.
239 :param object_dict: A Python dict that represents an object of the
240 specified type.
241 :returns: The server response.
242 """
243
244 body = self.serialize(object_dict)
245 uri = self._get_uri(resource)
246
247 resp, body = self.post(uri, body=body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600248 self.expected_success(http_client.NO_CONTENT, resp.status)
Sam Betts462e9e62016-11-30 18:43:35 +0000249
250 return resp
251
Yolanda Roblaabd90112018-05-15 17:11:54 +0200252 def _delete_request(self, resource, uuid,
253 expected_status=http_client.NO_CONTENT):
Yuiko Takadab6527002015-12-07 11:49:12 +0900254 """Delete specified object.
255
256 :param resource: The name of the REST resource, e.g., 'nodes'.
257 :param uuid: The unique identifier of an object in UUID format.
Yolanda Roblaabd90112018-05-15 17:11:54 +0200258 :param expected_status: Expected response status code. By default is
259 http_client.NO_CONTENT (204)
Yuiko Takadab6527002015-12-07 11:49:12 +0900260 :returns: A tuple with the server response and the response body.
261
262 """
263 uri = self._get_uri(resource, uuid)
264
265 resp, body = self.delete(uri)
Yolanda Roblaabd90112018-05-15 17:11:54 +0200266 self.expected_success(expected_status, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900267 return resp, body
268
Dmitry Tantsure7548052018-07-16 17:48:32 +0200269 def _patch_request(self, resource, uuid, patch_object, params=None):
Yuiko Takadab6527002015-12-07 11:49:12 +0900270 """Update specified object with JSON-patch.
271
272 :param resource: The name of the REST resource, e.g., 'nodes'.
273 :param uuid: The unique identifier of an object in UUID format.
Dmitry Tantsure7548052018-07-16 17:48:32 +0200274 :param params: query parameters to pass.
Yuiko Takadab6527002015-12-07 11:49:12 +0900275 :returns: A tuple with the server response and the serialized patched
276 object.
277
278 """
Dmitry Tantsure7548052018-07-16 17:48:32 +0200279 uri = self._get_uri(resource, uuid, params=params)
Yuiko Takadab6527002015-12-07 11:49:12 +0900280 patch_body = json.dumps(patch_object)
281
282 resp, body = self.patch(uri, body=patch_body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600283 self.expected_success(http_client.OK, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900284 return resp, self.deserialize(body)
285
286 @handle_errors
287 def get_api_description(self):
288 """Retrieves all versions of the Ironic API."""
289
290 return self._list_request('', permanent=True)
291
292 @handle_errors
293 def get_version_description(self, version='v1'):
Kyrylo Romanenko36000542016-09-16 15:07:40 +0300294 """Retrieves the description of the API.
Yuiko Takadab6527002015-12-07 11:49:12 +0900295
296 :param version: The version of the API. Default: 'v1'.
297 :returns: Serialized description of API resources.
298
299 """
300 return self._list_request(version, permanent=True)
301
302 def _put_request(self, resource, put_object):
303 """Update specified object with JSON-patch."""
304 uri = self._get_uri(resource)
305 put_body = json.dumps(put_object)
306
307 resp, body = self.put(uri, body=put_body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600308 self.expected_success([http_client.ACCEPTED, http_client.NO_CONTENT],
309 resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900310 return resp, body