blob: 3c52fcd155c5dd6b466848ef2c8023a7deecac73 [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
14from oslo_serialization import jsonutils as json
John L. Villalovosd88fe9f2018-02-01 16:24:43 -080015import six
Solio Sarabiaf78122e2017-02-23 16:17:34 -060016from six.moves import http_client
Yuiko Takadab6527002015-12-07 11:49:12 +090017from six.moves.urllib import parse as urllib
Yuiko Takadaff785002015-12-17 15:56:42 +090018from tempest.lib.common import api_version_utils
Lenny Verkhovsky88625042016-03-08 17:44:00 +020019from tempest.lib.common import rest_client
Yuiko Takadab6527002015-12-07 11:49:12 +090020
Vasyl Saienko4ddbeec2017-01-20 16:26:04 +000021# NOTE(vsaienko): concurrent tests work because they are launched in
22# separate processes so global variables are not shared among them.
Yuiko Takadaff785002015-12-17 15:56:42 +090023BAREMETAL_MICROVERSION = None
24
Yuiko Takadab6527002015-12-07 11:49:12 +090025
Vasyl Saienko4ddbeec2017-01-20 16:26:04 +000026def set_baremetal_api_microversion(baremetal_microversion):
27 global BAREMETAL_MICROVERSION
28 BAREMETAL_MICROVERSION = baremetal_microversion
29
30
31def reset_baremetal_api_microversion():
32 global BAREMETAL_MICROVERSION
33 BAREMETAL_MICROVERSION = None
34
35
Yuiko Takadab6527002015-12-07 11:49:12 +090036def handle_errors(f):
37 """A decorator that allows to ignore certain types of errors."""
38
John L. Villalovosd88fe9f2018-02-01 16:24:43 -080039 @six.wraps(f)
Yuiko Takadab6527002015-12-07 11:49:12 +090040 def wrapper(*args, **kwargs):
41 param_name = 'ignore_errors'
42 ignored_errors = kwargs.get(param_name, tuple())
43
44 if param_name in kwargs:
45 del kwargs[param_name]
46
47 try:
48 return f(*args, **kwargs)
49 except ignored_errors:
50 # Silently ignore errors
51 pass
52
53 return wrapper
54
55
56class BaremetalClient(rest_client.RestClient):
57 """Base Tempest REST client for Ironic API."""
58
Yuiko Takadaff785002015-12-17 15:56:42 +090059 api_microversion_header_name = 'X-OpenStack-Ironic-API-Version'
Yuiko Takadab6527002015-12-07 11:49:12 +090060 uri_prefix = ''
61
Yuiko Takadaff785002015-12-17 15:56:42 +090062 def get_headers(self):
63 headers = super(BaremetalClient, self).get_headers()
64 if BAREMETAL_MICROVERSION:
65 headers[self.api_microversion_header_name] = BAREMETAL_MICROVERSION
66 return headers
67
Vasyl Saienkof20979c2016-05-27 11:25:01 +030068 def request(self, *args, **kwargs):
69 resp, resp_body = super(BaremetalClient, self).request(*args, **kwargs)
Riccardo Pittau441c5062020-03-30 15:06:28 +020070 latest_microversion = api_version_utils.LATEST_MICROVERSION
71 if (BAREMETAL_MICROVERSION
72 and BAREMETAL_MICROVERSION != latest_microversion):
Yuiko Takadaff785002015-12-17 15:56:42 +090073 api_version_utils.assert_version_header_matches_request(
74 self.api_microversion_header_name,
75 BAREMETAL_MICROVERSION,
76 resp)
77 return resp, resp_body
78
Yuiko Takadab6527002015-12-07 11:49:12 +090079 def serialize(self, object_dict):
80 """Serialize an Ironic object."""
81
82 return json.dumps(object_dict)
83
84 def deserialize(self, object_str):
85 """Deserialize an Ironic object."""
86
87 return json.loads(object_str)
88
Dmitry Tantsure7548052018-07-16 17:48:32 +020089 def _get_uri(self, resource_name, uuid=None, permanent=False,
90 params=None):
Yuiko Takadab6527002015-12-07 11:49:12 +090091 """Get URI for a specific resource or object.
92
93 :param resource_name: The name of the REST resource, e.g., 'nodes'.
94 :param uuid: The unique identifier of an object in UUID format.
95 :returns: Relative URI for the resource or object.
96
97 """
98 prefix = self.uri_prefix if not permanent else ''
Dmitry Tantsure7548052018-07-16 17:48:32 +020099 if params:
100 params = '?' + '&'.join('%s=%s' % tpl for tpl in params.items())
101 else:
102 params = ''
Yuiko Takadab6527002015-12-07 11:49:12 +0900103
Dmitry Tantsure7548052018-07-16 17:48:32 +0200104 return '{pref}/{res}{uuid}{params}'.format(
105 pref=prefix, res=resource_name,
106 uuid='/%s' % uuid if uuid else '',
107 params=params)
Yuiko Takadab6527002015-12-07 11:49:12 +0900108
109 def _make_patch(self, allowed_attributes, **kwargs):
110 """Create a JSON patch according to RFC 6902.
111
112 :param allowed_attributes: An iterable object that contains a set of
113 allowed attributes for an object.
114 :param **kwargs: Attributes and new values for them.
115 :returns: A JSON path that sets values of the specified attributes to
116 the new ones.
117
118 """
119 def get_change(kwargs, path='/'):
Luong Anh Tuane22fbe12016-09-12 16:32:14 +0700120 for name, value in kwargs.items():
Yuiko Takadab6527002015-12-07 11:49:12 +0900121 if isinstance(value, dict):
122 for ch in get_change(value, path + '%s/' % name):
123 yield ch
124 else:
125 if value is None:
126 yield {'path': path + name,
127 'op': 'remove'}
128 else:
129 yield {'path': path + name,
130 'value': value,
131 'op': 'replace'}
132
133 patch = [ch for ch in get_change(kwargs)
134 if ch['path'].lstrip('/') in allowed_attributes]
135
136 return patch
137
Sam Betts462e9e62016-11-30 18:43:35 +0000138 def _list_request(self, resource, permanent=False, headers=None,
139 extra_headers=False, **kwargs):
Yuiko Takadab6527002015-12-07 11:49:12 +0900140 """Get the list of objects of the specified type.
141
142 :param resource: The name of the REST resource, e.g., 'nodes'.
Sam Betts462e9e62016-11-30 18:43:35 +0000143 :param headers: List of headers to use in request.
144 :param extra_headers: Specify whether to use headers.
Yuiko Takadab6527002015-12-07 11:49:12 +0900145 :param **kwargs: Parameters for the request.
146 :returns: A tuple with the server response and deserialized JSON list
147 of objects
148
149 """
150 uri = self._get_uri(resource, permanent=permanent)
151 if kwargs:
152 uri += "?%s" % urllib.urlencode(kwargs)
153
Sam Betts462e9e62016-11-30 18:43:35 +0000154 resp, body = self.get(uri, headers=headers,
155 extra_headers=extra_headers)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600156 self.expected_success(http_client.OK, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900157
158 return resp, self.deserialize(body)
159
SofiiaAndriichenko569f0a42016-12-08 05:02:17 -0500160 def _show_request(self,
161 resource,
162 uuid=None,
163 permanent=False,
Mark Goddard56399cc2018-02-16 13:37:25 +0000164 headers=None,
165 extra_headers=False,
SofiiaAndriichenko569f0a42016-12-08 05:02:17 -0500166 **kwargs):
Yuiko Takadab6527002015-12-07 11:49:12 +0900167 """Gets a specific object of the specified type.
168
169 :param uuid: Unique identifier of the object in UUID format.
Mark Goddard56399cc2018-02-16 13:37:25 +0000170 :param headers: List of headers to use in request.
171 :param extra_headers: Specify whether to use headers.
Yuiko Takadab6527002015-12-07 11:49:12 +0900172 :returns: Serialized object as a dictionary.
173
174 """
175 if 'uri' in kwargs:
176 uri = kwargs['uri']
177 else:
178 uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
Mark Goddard56399cc2018-02-16 13:37:25 +0000179 resp, body = self.get(uri, headers=headers,
180 extra_headers=extra_headers)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600181 self.expected_success(http_client.OK, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900182
183 return resp, self.deserialize(body)
184
185 def _create_request(self, resource, object_dict):
186 """Create an object of the specified type.
187
188 :param resource: The name of the REST resource, e.g., 'nodes'.
189 :param object_dict: A Python dict that represents an object of the
190 specified type.
191 :returns: A tuple with the server response and the deserialized created
192 object.
193
194 """
195 body = self.serialize(object_dict)
196 uri = self._get_uri(resource)
197
198 resp, body = self.post(uri, body=body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600199 self.expected_success(http_client.CREATED, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900200
201 return resp, self.deserialize(body)
202
Sam Betts462e9e62016-11-30 18:43:35 +0000203 def _create_request_no_response_body(self, resource, object_dict):
204 """Create an object of the specified type.
205
206 Do not expect any body in the response.
207
208 :param resource: The name of the REST resource, e.g., 'nodes'.
209 :param object_dict: A Python dict that represents an object of the
210 specified type.
211 :returns: The server response.
212 """
213
214 body = self.serialize(object_dict)
215 uri = self._get_uri(resource)
216
217 resp, body = self.post(uri, body=body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600218 self.expected_success(http_client.NO_CONTENT, resp.status)
Sam Betts462e9e62016-11-30 18:43:35 +0000219
220 return resp
221
Yolanda Roblaabd90112018-05-15 17:11:54 +0200222 def _delete_request(self, resource, uuid,
223 expected_status=http_client.NO_CONTENT):
Yuiko Takadab6527002015-12-07 11:49:12 +0900224 """Delete specified object.
225
226 :param resource: The name of the REST resource, e.g., 'nodes'.
227 :param uuid: The unique identifier of an object in UUID format.
Yolanda Roblaabd90112018-05-15 17:11:54 +0200228 :param expected_status: Expected response status code. By default is
229 http_client.NO_CONTENT (204)
Yuiko Takadab6527002015-12-07 11:49:12 +0900230 :returns: A tuple with the server response and the response body.
231
232 """
233 uri = self._get_uri(resource, uuid)
234
235 resp, body = self.delete(uri)
Yolanda Roblaabd90112018-05-15 17:11:54 +0200236 self.expected_success(expected_status, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900237 return resp, body
238
Dmitry Tantsure7548052018-07-16 17:48:32 +0200239 def _patch_request(self, resource, uuid, patch_object, params=None):
Yuiko Takadab6527002015-12-07 11:49:12 +0900240 """Update specified object with JSON-patch.
241
242 :param resource: The name of the REST resource, e.g., 'nodes'.
243 :param uuid: The unique identifier of an object in UUID format.
Dmitry Tantsure7548052018-07-16 17:48:32 +0200244 :param params: query parameters to pass.
Yuiko Takadab6527002015-12-07 11:49:12 +0900245 :returns: A tuple with the server response and the serialized patched
246 object.
247
248 """
Dmitry Tantsure7548052018-07-16 17:48:32 +0200249 uri = self._get_uri(resource, uuid, params=params)
Yuiko Takadab6527002015-12-07 11:49:12 +0900250 patch_body = json.dumps(patch_object)
251
252 resp, body = self.patch(uri, body=patch_body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600253 self.expected_success(http_client.OK, resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900254 return resp, self.deserialize(body)
255
256 @handle_errors
257 def get_api_description(self):
258 """Retrieves all versions of the Ironic API."""
259
260 return self._list_request('', permanent=True)
261
262 @handle_errors
263 def get_version_description(self, version='v1'):
Kyrylo Romanenko36000542016-09-16 15:07:40 +0300264 """Retrieves the description of the API.
Yuiko Takadab6527002015-12-07 11:49:12 +0900265
266 :param version: The version of the API. Default: 'v1'.
267 :returns: Serialized description of API resources.
268
269 """
270 return self._list_request(version, permanent=True)
271
272 def _put_request(self, resource, put_object):
273 """Update specified object with JSON-patch."""
274 uri = self._get_uri(resource)
275 put_body = json.dumps(put_object)
276
277 resp, body = self.put(uri, body=put_body)
Solio Sarabiaf78122e2017-02-23 16:17:34 -0600278 self.expected_success([http_client.ACCEPTED, http_client.NO_CONTENT],
279 resp.status)
Yuiko Takadab6527002015-12-07 11:49:12 +0900280 return resp, body