blob: 3f256ec3f911441ad7414b11d6d9de4b9e39e45b [file] [log] [blame]
Kurt Taylor6a6f5be2013-04-02 18:53:47 -04001# Copyright 2013 IBM Corp.
Matthew Treinish72ea4422013-02-07 14:42:49 -05002# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import copy
Attila Fazekas5c068812013-02-14 15:29:44 +010017import errno
Matthew Treinish72ea4422013-02-07 14:42:49 -050018import os
Attila Fazekase72b7cd2013-03-26 18:34:21 +010019import time
Matthew Treinish72ea4422013-02-07 14:42:49 -050020
Doug Hellmann583ce2c2015-03-11 14:55:46 +000021from oslo_log import log as logging
Matthew Treinish21905512015-07-13 10:33:35 -040022from oslo_serialization import jsonutils as json
Matthew Treinish71426682015-04-23 11:19:38 -040023import six
Matthew Treinish89128142015-04-23 10:44:30 -040024from six.moves.urllib import parse as urllib
Masayuki Igawabfa07602015-01-20 18:47:17 +090025
Matthew Treinish72ea4422013-02-07 14:42:49 -050026from tempest.common import glance_http
Matthew Treinish72ea4422013-02-07 14:42:49 -050027from tempest import exceptions
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -080028from tempest.lib.common import rest_client
Andrea Frittoli (andreaf)db9672e2016-02-23 14:07:24 -050029from tempest.lib.common.utils import misc as misc_utils
30from tempest.lib import exceptions as lib_exc
Matthew Treinish72ea4422013-02-07 14:42:49 -050031
Attila Fazekase72b7cd2013-03-26 18:34:21 +010032LOG = logging.getLogger(__name__)
33
Matthew Treinish72ea4422013-02-07 14:42:49 -050034
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -080035class ImagesClient(rest_client.RestClient):
Matthew Treinish72ea4422013-02-07 14:42:49 -050036
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -080037 def __init__(self, auth_provider, catalog_type, region, **kwargs):
Ken'ichi Ohmichi69dcf442015-11-30 11:48:01 +000038 super(ImagesClient, self).__init__(
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -080039 auth_provider, catalog_type, region, **kwargs)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000040 self._http = None
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -080041 self.dscv = kwargs.get("disable_ssl_certificate_validation")
42 self.ca_certs = kwargs.get("ca_certs")
Matthew Treinish72ea4422013-02-07 14:42:49 -050043
44 def _image_meta_from_headers(self, headers):
45 meta = {'properties': {}}
Matthew Treinish71426682015-04-23 11:19:38 -040046 for key, value in six.iteritems(headers):
Matthew Treinish72ea4422013-02-07 14:42:49 -050047 if key.startswith('x-image-meta-property-'):
48 _key = key[22:]
49 meta['properties'][_key] = value
50 elif key.startswith('x-image-meta-'):
51 _key = key[13:]
52 meta[_key] = value
53
54 for key in ['is_public', 'protected', 'deleted']:
55 if key in meta:
56 meta[key] = meta[key].strip().lower() in ('t', 'true', 'yes',
57 '1')
58 for key in ['size', 'min_ram', 'min_disk']:
59 if key in meta:
60 try:
61 meta[key] = int(meta[key])
62 except ValueError:
63 pass
64 return meta
65
66 def _image_meta_to_headers(self, fields):
67 headers = {}
68 fields_copy = copy.deepcopy(fields)
Attila Fazekase72b7cd2013-03-26 18:34:21 +010069 copy_from = fields_copy.pop('copy_from', None)
70 if copy_from is not None:
71 headers['x-glance-api-copy-from'] = copy_from
Matthew Treinish71426682015-04-23 11:19:38 -040072 for key, value in six.iteritems(fields_copy.pop('properties', {})):
Matthew Treinish72ea4422013-02-07 14:42:49 -050073 headers['x-image-meta-property-%s' % key] = str(value)
Matthew Treinish71426682015-04-23 11:19:38 -040074 for key, value in six.iteritems(fields_copy.pop('api', {})):
Attila Fazekase72b7cd2013-03-26 18:34:21 +010075 headers['x-glance-api-property-%s' % key] = str(value)
Matthew Treinish71426682015-04-23 11:19:38 -040076 for key, value in six.iteritems(fields_copy):
Matthew Treinish72ea4422013-02-07 14:42:49 -050077 headers['x-image-meta-%s' % key] = str(value)
78 return headers
79
80 def _get_file_size(self, obj):
81 """Analyze file-like object and attempt to determine its size.
82
83 :param obj: file-like object, typically redirected from stdin.
84 :retval The file's size or None if it cannot be determined.
85 """
86 # For large images, we need to supply the size of the
87 # image file. See LP Bugs #827660 and #845788.
88 if hasattr(obj, 'seek') and hasattr(obj, 'tell'):
89 try:
90 obj.seek(0, os.SEEK_END)
91 obj_size = obj.tell()
92 obj.seek(0)
93 return obj_size
Dirk Mueller1db5db22013-06-23 20:21:32 +020094 except IOError as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -050095 if e.errno == errno.ESPIPE:
96 # Illegal seek. This means the user is trying
97 # to pipe image data to the client, e.g.
98 # echo testdata | bin/glance add blah..., or
99 # that stdin is empty, or that a file-like
100 # object which doesn't support 'seek/tell' has
101 # been supplied.
102 return None
103 else:
104 raise
105 else:
106 # Cannot determine size of input image
107 return None
108
109 def _get_http(self):
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000110 return glance_http.HTTPClient(auth_provider=self.auth_provider,
111 filters=self.filters,
Masayuki Igawabc7e1892015-03-03 11:46:48 +0900112 insecure=self.dscv,
113 ca_certs=self.ca_certs)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500114
115 def _create_with_data(self, headers, data):
116 resp, body_iter = self.http.raw_request('POST', '/v1/images',
117 headers=headers, body=data)
118 self._error_checker('POST', '/v1/images', headers, data, resp,
119 body_iter)
120 body = json.loads(''.join([c for c in body_iter]))
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800121 return rest_client.ResponseBody(resp, body)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500122
123 def _update_with_data(self, image_id, headers, data):
124 url = '/v1/images/%s' % image_id
125 resp, body_iter = self.http.raw_request('PUT', url, headers=headers,
126 body=data)
127 self._error_checker('PUT', url, headers, data,
128 resp, body_iter)
129 body = json.loads(''.join([c for c in body_iter]))
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800130 return rest_client.ResponseBody(resp, body)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500131
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000132 @property
133 def http(self):
134 if self._http is None:
Masayuki Igawabc7e1892015-03-03 11:46:48 +0900135 self._http = self._get_http()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000136 return self._http
137
Ghanshyam86a31e12015-12-16 15:42:47 +0900138 def create_image(self, **kwargs):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500139 headers = {}
Ghanshyam86a31e12015-12-16 15:42:47 +0900140 data = kwargs.pop('data', None)
141 headers.update(self._image_meta_to_headers(kwargs))
Matthew Treinish72ea4422013-02-07 14:42:49 -0500142
Ghanshyam86a31e12015-12-16 15:42:47 +0900143 if data is not None:
144 return self._create_with_data(headers, data)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500145
Matthew Treinishce3ef922013-03-11 14:02:46 -0400146 resp, body = self.post('v1/images', None, headers)
David Kranz9c3b3b62014-06-19 16:05:53 -0400147 self.expected_success(201, resp.status)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500148 body = json.loads(body)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800149 return rest_client.ResponseBody(resp, body)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500150
Ghanshyam86a31e12015-12-16 15:42:47 +0900151 def update_image(self, image_id, **kwargs):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500152 headers = {}
Ghanshyam86a31e12015-12-16 15:42:47 +0900153 data = kwargs.pop('data', None)
154 headers.update(self._image_meta_to_headers(kwargs))
Matthew Treinish72ea4422013-02-07 14:42:49 -0500155
156 if data is not None:
157 return self._update_with_data(image_id, headers, data)
158
159 url = 'v1/images/%s' % image_id
Ghanshyam86a31e12015-12-16 15:42:47 +0900160 resp, body = self.put(url, None, headers)
David Kranz9c3b3b62014-06-19 16:05:53 -0400161 self.expected_success(200, resp.status)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500162 body = json.loads(body)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800163 return rest_client.ResponseBody(resp, body)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500164
165 def delete_image(self, image_id):
166 url = 'v1/images/%s' % image_id
David Kranz9c3b3b62014-06-19 16:05:53 -0400167 resp, body = self.delete(url)
168 self.expected_success(200, resp.status)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800169 return rest_client.ResponseBody(resp, body)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500170
Ghanshyame60db482016-01-13 09:50:12 +0900171 def list_images(self, detail=False, **kwargs):
172 """Return a list of all images filtered by input parameters.
173
174 Available params: see http://developer.openstack.org/
175 api-ref-image-v1.html#listImage-v1
176
177 Most parameters except the following are passed to the API without
178 any changes.
179 :param changes_since: The name is changed to changes-since
180 """
Matthew Treinish72ea4422013-02-07 14:42:49 -0500181 url = 'v1/images'
182
Ken'ichi Ohmichibcad2a22015-05-22 09:37:06 +0000183 if detail:
184 url += '/detail'
Attila Fazekas11795b52013-02-24 15:49:08 +0100185
Ghanshyame60db482016-01-13 09:50:12 +0900186 properties = kwargs.pop('properties', {})
187 for key, value in six.iteritems(properties):
188 kwargs['property-%s' % key] = value
ivan-zhuccc89462013-10-31 16:53:12 +0800189
Ghanshyame60db482016-01-13 09:50:12 +0900190 if kwargs.get('changes_since'):
191 kwargs['changes-since'] = kwargs.pop('changes_since')
ivan-zhud1bbe5d2013-12-29 18:32:46 +0800192
Attila Fazekas11795b52013-02-24 15:49:08 +0100193 if len(kwargs) > 0:
194 url += '?%s' % urllib.urlencode(kwargs)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500195
196 resp, body = self.get(url)
David Kranz9c3b3b62014-06-19 16:05:53 -0400197 self.expected_success(200, resp.status)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500198 body = json.loads(body)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800199 return rest_client.ResponseBody(resp, body)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500200
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100201 def get_image_meta(self, image_id):
202 url = 'v1/images/%s' % image_id
203 resp, __ = self.head(url)
David Kranz9c3b3b62014-06-19 16:05:53 -0400204 self.expected_success(200, resp.status)
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100205 body = self._image_meta_from_headers(resp)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800206 return rest_client.ResponseBody(resp, body)
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100207
Ken'ichi Ohmichi5d410762015-05-22 01:10:03 +0000208 def show_image(self, image_id):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500209 url = 'v1/images/%s' % image_id
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100210 resp, body = self.get(url)
David Kranz9c3b3b62014-06-19 16:05:53 -0400211 self.expected_success(200, resp.status)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800212 return rest_client.ResponseBodyData(resp, body)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500213
214 def is_resource_deleted(self, id):
215 try:
Attila Fazekas9fa29472014-08-18 09:48:00 +0200216 self.get_image_meta(id)
Masayuki Igawabfa07602015-01-20 18:47:17 +0900217 except lib_exc.NotFound:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500218 return True
219 return False
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500220
Matt Riedemannd2b96512014-10-13 10:18:16 -0700221 @property
222 def resource_type(self):
223 """Returns the primary type of resource this client works with."""
224 return 'image_meta'
225
Ken'ichi Ohmichif0df53c2015-05-22 01:16:50 +0000226 def list_image_members(self, image_id):
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500227 url = 'v1/images/%s/members' % image_id
228 resp, body = self.get(url)
David Kranz9c3b3b62014-06-19 16:05:53 -0400229 self.expected_success(200, resp.status)
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500230 body = json.loads(body)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800231 return rest_client.ResponseBody(resp, body)
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500232
Ken'ichi Ohmichibcad2a22015-05-22 09:37:06 +0000233 def list_shared_images(self, tenant_id):
234 """List shared images with the specified tenant"""
235 url = 'v1/shared-images/%s' % tenant_id
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500236 resp, body = self.get(url)
David Kranz9c3b3b62014-06-19 16:05:53 -0400237 self.expected_success(200, resp.status)
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500238 body = json.loads(body)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800239 return rest_client.ResponseBody(resp, body)
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500240
Ghanshyam8b1603b2015-12-02 16:04:53 +0900241 def add_member(self, member_id, image_id, **kwargs):
242 """Add a member to an image.
243
244 Available params: see http://developer.openstack.org/
245 api-ref-image-v1.html#addMember-v1
246 """
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500247 url = 'v1/images/%s/members/%s' % (image_id, member_id)
Ghanshyam8b1603b2015-12-02 16:04:53 +0900248 body = json.dumps({'member': kwargs})
Valeriy Ponomaryov88686d82014-02-16 12:24:51 +0200249 resp, __ = self.put(url, body)
David Kranz9c3b3b62014-06-19 16:05:53 -0400250 self.expected_success(204, resp.status)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800251 return rest_client.ResponseBody(resp)
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500252
253 def delete_member(self, member_id, image_id):
254 url = 'v1/images/%s/members/%s' % (image_id, member_id)
255 resp, __ = self.delete(url)
David Kranz9c3b3b62014-06-19 16:05:53 -0400256 self.expected_success(204, resp.status)
Ken'ichi Ohmichie76510d2016-03-02 10:33:48 -0800257 return rest_client.ResponseBody(resp)
Matthew Treinishfa23cf82013-03-06 14:23:02 -0500258
Attila Fazekasa8b5fe72013-08-01 16:59:06 +0200259 # NOTE(afazekas): just for the wait function
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100260 def _get_image_status(self, image_id):
David Kranz34f18782015-01-06 13:43:55 -0500261 meta = self.get_image_meta(image_id)
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100262 status = meta['status']
263 return status
264
Attila Fazekasa8b5fe72013-08-01 16:59:06 +0200265 # NOTE(afazkas): Wait reinvented again. It is not in the correct layer
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100266 def wait_for_image_status(self, image_id, status):
267 """Waits for a Image to reach a given status."""
268 start_time = time.time()
269 old_value = value = self._get_image_status(image_id)
270 while True:
271 dtime = time.time() - start_time
272 time.sleep(self.build_interval)
273 if value != old_value:
274 LOG.info('Value transition from "%s" to "%s"'
275 'in %d second(s).', old_value,
276 value, dtime)
Attila Fazekas195f1d42013-10-24 10:33:16 +0200277 if value == status:
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100278 return value
279
Attila Fazekas195f1d42013-10-24 10:33:16 +0200280 if value == 'killed':
281 raise exceptions.ImageKilledException(image_id=image_id,
Attila Fazekas9dfb9072013-11-13 16:45:36 +0100282 status=status)
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100283 if dtime > self.build_timeout:
284 message = ('Time Limit Exceeded! (%ds)'
285 'while waiting for %s, '
286 'but we got %s.' %
287 (self.build_timeout, status, value))
Matt Riedemann7cec3462014-06-25 12:48:43 -0700288 caller = misc_utils.find_test_caller()
289 if caller:
290 message = '(%s) %s' % (caller, message)
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100291 raise exceptions.TimeoutException(message)
292 time.sleep(self.build_interval)
293 old_value = value
294 value = self._get_image_status(image_id)