blob: 2acad18d32a39d1c4b03a7c837b5199e7f6b7efd [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
Yuiko Takadab6527002015-12-07 11:49:12 +090026
Vasyl Saienko4ddbeec2017-01-20 16:26:04 +000027def set_baremetal_api_microversion(baremetal_microversion):
28 global BAREMETAL_MICROVERSION
29 BAREMETAL_MICROVERSION = baremetal_microversion
30
31
32def reset_baremetal_api_microversion():
33 global BAREMETAL_MICROVERSION
34 BAREMETAL_MICROVERSION = None
35
36
Yuiko Takadab6527002015-12-07 11:49:12 +090037def handle_errors(f):
38 """A decorator that allows to ignore certain types of errors."""
39
Takashi Kajinami6bddaab2022-05-10 00:58:56 +090040 @functools.wraps(f)
Yuiko Takadab6527002015-12-07 11:49:12 +090041 def wrapper(*args, **kwargs):
42 param_name = 'ignore_errors'
43 ignored_errors = kwargs.get(param_name, tuple())
44
45 if param_name in kwargs:
46 del kwargs[param_name]
47
48 try:
49 return f(*args, **kwargs)
50 except ignored_errors:
51 # Silently ignore errors
52 pass
53
54 return wrapper
55
56
57class BaremetalClient(rest_client.RestClient):
58 """Base Tempest REST client for Ironic API."""
59
Yuiko Takadaff785002015-12-17 15:56:42 +090060 api_microversion_header_name = 'X-OpenStack-Ironic-API-Version'
Yuiko Takadab6527002015-12-07 11:49:12 +090061 uri_prefix = ''
62
Yuiko Takadaff785002015-12-17 15:56:42 +090063 def get_headers(self):
64 headers = super(BaremetalClient, self).get_headers()
65 if BAREMETAL_MICROVERSION:
66 headers[self.api_microversion_header_name] = BAREMETAL_MICROVERSION
67 return headers
68
Vasyl Saienkof20979c2016-05-27 11:25:01 +030069 def request(self, *args, **kwargs):
70 resp, resp_body = super(BaremetalClient, self).request(*args, **kwargs)
Riccardo Pittau441c5062020-03-30 15:06:28 +020071 latest_microversion = api_version_utils.LATEST_MICROVERSION
72 if (BAREMETAL_MICROVERSION
73 and BAREMETAL_MICROVERSION != latest_microversion):
Yuiko Takadaff785002015-12-17 15:56:42 +090074 api_version_utils.assert_version_header_matches_request(
75 self.api_microversion_header_name,
76 BAREMETAL_MICROVERSION,
77 resp)
78 return resp, resp_body
79
Yuiko Takadab6527002015-12-07 11:49:12 +090080 def serialize(self, object_dict):
81 """Serialize an Ironic object."""
82
83 return json.dumps(object_dict)
84
85 def deserialize(self, object_str):
86 """Deserialize an Ironic object."""
87
88 return json.loads(object_str)
89
Dmitry Tantsure7548052018-07-16 17:48:32 +020090 def _get_uri(self, resource_name, uuid=None, permanent=False,
91 params=None):
Yuiko Takadab6527002015-12-07 11:49:12 +090092 """Get URI for a specific resource or object.
93
94 :param resource_name: The name of the REST resource, e.g., 'nodes'.
95 :param uuid: The unique identifier of an object in UUID format.
96 :returns: Relative URI for the resource or object.
97
98 """
99 prefix = self.uri_prefix if not permanent else ''
Dmitry Tantsure7548052018-07-16 17:48:32 +0200100 if params:
101 params = '?' + '&'.join('%s=%s' % tpl for tpl in params.items())
102 else:
103 params = ''
Yuiko Takadab6527002015-12-07 11:49:12 +0900104
Dmitry Tantsure7548052018-07-16 17:48:32 +0200105 return '{pref}/{res}{uuid}{params}'.format(
106 pref=prefix, res=resource_name,
107 uuid='/%s' % uuid if uuid else '',
108 params=params)
Yuiko Takadab6527002015-12-07 11:49:12 +0900109
110 def _make_patch(self, allowed_attributes, **kwargs):
111 """Create a JSON patch according to RFC 6902.
112
113 :param allowed_attributes: An iterable object that contains a set of
114 allowed attributes for an object.
115 :param **kwargs: Attributes and new values for them.
116 :returns: A JSON path that sets values of the specified attributes to
117 the new ones.
118
119 """
120 def get_change(kwargs, path='/'):
Luong Anh Tuane22fbe12016-09-12 16:32:14 +0700121 for name, value in kwargs.items():
Yuiko Takadab6527002015-12-07 11:49:12 +0900122 if isinstance(value, dict):
123 for ch in get_change(value, path + '%s/' % name):
124 yield ch
125 else:
126 if value is None:
127 yield {'path': path + name,
128 'op': 'remove'}
129 else:
130 yield {'path': path + name,
131 'value': value,
132 'op': 'replace'}
133
134 patch = [ch for ch in get_change(kwargs)
135 if ch['path'].lstrip('/') in allowed_attributes]
136
137 return patch
138
Sam Betts462e9e62016-11-30 18:43:35 +0000139 def _list_request(self, resource, permanent=False, headers=None,
140 extra_headers=False, **kwargs):
Yuiko Takadab6527002015-12-07 11:49:12 +0900141 """Get the list of objects of the specified type.
142
143 :param resource: The name of the REST resource, e.g., 'nodes'.
Sam Betts462e9e62016-11-30 18:43:35 +0000144 :param headers: List of headers to use in request.
145 :param extra_headers: Specify whether to use headers.
Yuiko Takadab6527002015-12-07 11:49:12 +0900146 :param **kwargs: Parameters for the request.
147 :returns: A tuple with the server response and deserialized JSON list
148 of objects
149
150 """
151 uri = self._get_uri(resource, permanent=permanent)
152 if kwargs:
Takashi Kajinami6bddaab2022-05-10 00:58:56 +0900153 uri += "?%s" % urllib_parse.urlencode(kwargs)
Yuiko Takadab6527002015-12-07 11:49:12 +0900154
Sam Betts462e9e62016-11-30 18:43:35 +0000155 resp, body = self.get(uri, headers=headers,
156 extra_headers=extra_headers)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600157 self.expected_success(http_client.OK, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900158
159 return resp, self.deserialize(body)
160
SofiiaAndriichenko569f0a42016-12-08 05:02:17 -0500161 def _show_request(self,
162 resource,
163 uuid=None,
164 permanent=False,
Mark Goddard56399cc2018-02-16 13:37:25 +0000165 headers=None,
166 extra_headers=False,
SofiiaAndriichenko569f0a42016-12-08 05:02:17 -0500167 **kwargs):
Yuiko Takadab6527002015-12-07 11:49:12 +0900168 """Gets a specific object of the specified type.
169
170 :param uuid: Unique identifier of the object in UUID format.
Mark Goddard56399cc2018-02-16 13:37:25 +0000171 :param headers: List of headers to use in request.
172 :param extra_headers: Specify whether to use headers.
Yuiko Takadab6527002015-12-07 11:49:12 +0900173 :returns: Serialized object as a dictionary.
174
175 """
176 if 'uri' in kwargs:
177 uri = kwargs['uri']
178 else:
179 uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
Mark Goddard56399cc2018-02-16 13:37:25 +0000180 resp, body = self.get(uri, headers=headers,
181 extra_headers=extra_headers)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600182 self.expected_success(http_client.OK, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900183
184 return resp, self.deserialize(body)
185
186 def _create_request(self, resource, object_dict):
187 """Create an object of the specified type.
188
189 :param resource: The name of the REST resource, e.g., 'nodes'.
190 :param object_dict: A Python dict that represents an object of the
191 specified type.
192 :returns: A tuple with the server response and the deserialized created
193 object.
194
195 """
196 body = self.serialize(object_dict)
197 uri = self._get_uri(resource)
198
199 resp, body = self.post(uri, body=body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600200 self.expected_success(http_client.CREATED, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900201
202 return resp, self.deserialize(body)
203
Sam Betts462e9e62016-11-30 18:43:35 +0000204 def _create_request_no_response_body(self, resource, object_dict):
205 """Create an object of the specified type.
206
207 Do not expect any body in the response.
208
209 :param resource: The name of the REST resource, e.g., 'nodes'.
210 :param object_dict: A Python dict that represents an object of the
211 specified type.
212 :returns: The server response.
213 """
214
215 body = self.serialize(object_dict)
216 uri = self._get_uri(resource)
217
218 resp, body = self.post(uri, body=body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600219 self.expected_success(http_client.NO_CONTENT, resp.status)
Sam Betts462e9e62016-11-30 18:43:35 +0000220
221 return resp
222
Yolanda Roblaabd90112018-05-15 17:11:54 +0200223 def _delete_request(self, resource, uuid,
224 expected_status=http_client.NO_CONTENT):
Yuiko Takadab6527002015-12-07 11:49:12 +0900225 """Delete specified object.
226
227 :param resource: The name of the REST resource, e.g., 'nodes'.
228 :param uuid: The unique identifier of an object in UUID format.
Yolanda Roblaabd90112018-05-15 17:11:54 +0200229 :param expected_status: Expected response status code. By default is
230 http_client.NO_CONTENT (204)
Yuiko Takadab6527002015-12-07 11:49:12 +0900231 :returns: A tuple with the server response and the response body.
232
233 """
234 uri = self._get_uri(resource, uuid)
235
236 resp, body = self.delete(uri)
Yolanda Roblaabd90112018-05-15 17:11:54 +0200237 self.expected_success(expected_status, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900238 return resp, body
239
Dmitry Tantsure7548052018-07-16 17:48:32 +0200240 def _patch_request(self, resource, uuid, patch_object, params=None):
Yuiko Takadab6527002015-12-07 11:49:12 +0900241 """Update specified object with JSON-patch.
242
243 :param resource: The name of the REST resource, e.g., 'nodes'.
244 :param uuid: The unique identifier of an object in UUID format.
Dmitry Tantsure7548052018-07-16 17:48:32 +0200245 :param params: query parameters to pass.
Yuiko Takadab6527002015-12-07 11:49:12 +0900246 :returns: A tuple with the server response and the serialized patched
247 object.
248
249 """
Dmitry Tantsure7548052018-07-16 17:48:32 +0200250 uri = self._get_uri(resource, uuid, params=params)
Yuiko Takadab6527002015-12-07 11:49:12 +0900251 patch_body = json.dumps(patch_object)
252
253 resp, body = self.patch(uri, body=patch_body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600254 self.expected_success(http_client.OK, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900255 return resp, self.deserialize(body)
256
257 @handle_errors
258 def get_api_description(self):
259 """Retrieves all versions of the Ironic API."""
260
261 return self._list_request('', permanent=True)
262
263 @handle_errors
264 def get_version_description(self, version='v1'):
Kyrylo Romanenko36000542016-09-16 15:07:40 +0300265 """Retrieves the description of the API.
Yuiko Takadab6527002015-12-07 11:49:12 +0900266
267 :param version: The version of the API. Default: 'v1'.
268 :returns: Serialized description of API resources.
269
270 """
271 return self._list_request(version, permanent=True)
272
273 def _put_request(self, resource, put_object):
274 """Update specified object with JSON-patch."""
275 uri = self._get_uri(resource)
276 put_body = json.dumps(put_object)
277
278 resp, body = self.put(uri, body=put_body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600279 self.expected_success([http_client.ACCEPTED, http_client.NO_CONTENT],
280 resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900281 return resp, body