update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b second try
[osm/SO.git] / rwlaunchpad / plugins / rwimagemgr / rift / tasklets / rwimagemgr / glance_client.py
1
2 #
3 # Copyright 2016 RIFT.IO Inc
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17
18 import itertools
19 import logging
20 import os
21 import glanceclient
22 import keystoneclient.v3.client as keystone_client
23 from keystoneauth1 import (
24 identity as keystone_identity,
25 session as keystone_session
26 )
27
28 from gi.repository import RwcalYang
29
30 logger = logging.getLogger(name=__name__)
31
32
33 class OpenstackImageError(Exception):
34 pass
35
36
37 class OpenstackNonUniqueImageError(OpenstackImageError):
38 pass
39
40
41 class OpenstackImageCreateError(Exception):
42 pass
43
44
45 class OpenstackImageDeleteError(Exception):
46 pass
47
48
49 class InvalidImageError(Exception):
50 pass
51
52
53 class OpenstackAccount(object):
54 def __init__(self, auth_url, tenant, username, password):
55 self.auth_url = auth_url
56 self.tenant = tenant
57 self.username = username
58 self.password = password
59
60
61 class OpenstackImage(object):
62 """ This value class encapsultes the RIFT-relevent glance image fields """
63
64 FIELDS = ["id", "name", "checksum", "disk_format",
65 "container_format", "size", "properties", "status"]
66 OPTIONAL_FIELDS = ["id", "checksum", "location"]
67
68 def __init__(self, name, disk_format, container_format, size,
69 properties=None, id=None, checksum=None, status="saving",
70 location=None):
71 self.name = name
72 self.disk_format = disk_format
73 self.container_format = container_format
74 self.size = size
75 self.properties = properties if properties is not None else {}
76 self.status = status
77
78 self.id = id
79 self.checksum = checksum
80
81 @classmethod
82 def from_image_response(cls, image):
83 """ Convert a image response from glance into a OpenstackImage
84
85 Arguments:
86 image - A glance image object (from glance_client.images.list() for example)
87
88 Returns:
89 An instance of OpenstackImage
90
91 Raises:
92 OpenstackImageError - Could not convert the response into a OpenstackImage object
93 """
94 missing_fields = [field for field in cls.FIELDS
95 if field not in cls.OPTIONAL_FIELDS and not hasattr(image, field)]
96 if missing_fields:
97 raise OpenstackImageError(
98 "Openstack image is missing required fields: %s" % missing_fields
99 )
100
101 kwargs = {field: getattr(image, field) for field in cls.FIELDS}
102
103 return cls(**kwargs)
104
105
106 class OpenstackKeystoneClient(object):
107 """ This class wraps the Keystone Client """
108 def __init__(self, ks_client):
109 self._ks_client = ks_client
110
111 @property
112 def auth_token(self):
113 return self._ks_client.auth_token
114
115 @classmethod
116 def from_openstack_account(cls, os_account):
117 ks_client = keystone_client.Client(
118 insecure=True,
119 auth_url=os_account.auth_url,
120 username=os_account.username,
121 password=os_account.password,
122 tenant_name=os_account.tenant
123 )
124
125 return cls(ks_client)
126
127 @property
128 def glance_endpoint(self):
129 """ Return the glance endpoint from the keystone service """
130 glance_ep = self._ks_client.service_catalog.url_for(
131 service_type='image',
132 endpoint_type='publicURL'
133 )
134
135 return glance_ep
136
137
138 class OpenstackGlanceClient(object):
139 def __init__(self, log, glance_client):
140 self._log = log
141 self._client = glance_client
142
143 @classmethod
144 def from_ks_client(cls, log, ks_client):
145 """ Create a OpenstackGlanceClient from a keystone client instance
146
147 Arguments:
148 log - logger instance
149 ks_client - A keystone client instance
150 """
151
152 glance_ep = ks_client.glance_endpoint
153 glance_client = glanceclient.Client(
154 '1',
155 glance_ep,
156 token=ks_client.auth_token,
157 )
158
159 return cls(log, glance_client)
160
161 @classmethod
162 def from_token(cls, log, host, port, token):
163 """ Create a OpenstackGlanceClient instance using a keystone auth token
164
165 Arguments:
166 log - logger instance
167 host - the glance host
168 port - the glance port
169 token - the keystone token
170
171 Returns:
172 A OpenstackGlanceClient instance
173 """
174 endpoint = "http://{}:{}".format(host, port)
175 glance_client = glanceclient.Client("1", endpoint, token=token)
176 return cls(log, glance_client)
177
178 def get_image_list(self):
179 """ Return the list of images from the Glance server
180
181 Returns:
182 A list of OpenstackImage instances
183 """
184 images = []
185 for image in itertools.chain(
186 self._client.images.list(is_public=False),
187 self._client.images.list(is_public=True)
188 ):
189 images.append(OpenstackImage.from_image_response(image))
190
191 return images
192
193 def get_image_data(self, image_id):
194 """ Return a image bytes generator from a image id
195
196 Arguments:
197 image_id - An image id that exists on the glance server
198
199 Returns:
200 An generator which produces the image data bytestrings
201
202 Raises:
203 OpenstackImageError - Could not find the image id
204 """
205
206 try:
207 self._client.images.get(image_id)
208 except Exception as e:
209 msg = "Failed to find image from image: %s" % image_id
210 self._log.exception(msg)
211 raise OpenstackImageError(msg) from e
212
213 img_data = self._client.images.data(image_id)
214 return img_data
215
216 def find_active_image(self, id=None, name=None, checksum=None):
217 """ Find an active images on the glance server
218
219 Arguments:
220 id - the image id to match
221 name - the image name to match
222 checksum - the image checksum to match
223
224 Returns:
225 A OpenstackImage instance
226
227 Raises:
228 OpenstackImageError - could not find a matching image
229 with matching image name and checksum
230 """
231 if id is None and name is None:
232 raise ValueError("image id or image name must be provided")
233
234 self._log.debug("attempting to find active image with id %s name %s and checksum %s",
235 id, name, checksum)
236
237 found_image = None
238
239 image_list = self.get_image_list()
240 self._log.debug("got image list from openstack: %s", image_list)
241 for image in self.get_image_list():
242 self._log.debug(image)
243 if image.status != "active":
244 continue
245
246 if id is not None:
247 if image.id != id:
248 continue
249
250 if name is not None:
251 if image.name != name:
252 continue
253
254 if checksum is not None:
255 if image.checksum != checksum:
256 continue
257
258 if found_image is not None:
259 raise OpenstackNonUniqueImageError(
260 "Found multiple images that matched the criteria. Use image id to disambiguate."
261 )
262
263 found_image = image
264
265 if found_image is None:
266 raise OpenstackImageError(
267 "could not find an active image with id %s name %s and checksum %s" %
268 (id, name, checksum))
269
270 return OpenstackImage.from_image_response(found_image)
271
272 def create_image_from_hdl(self, image, file_hdl):
273 """ Create an image on the glance server a file handle
274
275 Arguments:
276 image - An OpenstackImage instance
277 file_hdl - An open image file handle
278
279 Raises:
280 OpenstackImageCreateError - Could not upload the image
281 """
282 try:
283 self._client.images.create(
284 name=image.name,
285 is_public="False",
286 disk_format=image.disk_format,
287 container_format=image.container_format,
288 data=file_hdl
289 )
290 except Exception as e:
291 msg = "Failed to Openstack upload image"
292 self._log.exception(msg)
293 raise OpenstackImageCreateError(msg) from e
294
295 def create_image_from_url(self, image_url, image_name, image_checksum=None,
296 disk_format=None, container_format=None):
297 """ Create an image on the glance server from a image url
298
299 Arguments:
300 image_url - An HTTP image url
301 image_name - An openstack image name (filename with proper extension)
302 image checksum - The image md5 checksum
303
304 Raises:
305 OpenstackImageCreateError - Could not create the image
306 """
307 def disk_format_from_image_name(image_name):
308 _, image_ext = os.path.splitext(image_name)
309 if not image_ext:
310 raise InvalidImageError("image name must have an extension")
311
312 # Strip off the .
313 image_ext = image_ext[1:]
314
315 if not hasattr(RwcalYang.DiskFormat, image_ext.upper()):
316 raise InvalidImageError("unknown image extension for disk format: %s", image_ext)
317
318 disk_format = image_ext.lower()
319 return disk_format
320
321 # If the disk format was not provided, attempt to match via the file
322 # extension.
323 if disk_format is None:
324 disk_format = disk_format_from_image_name(image_name)
325
326 if container_format is None:
327 container_format = "bare"
328
329 create_args = dict(
330 location=image_url,
331 name=image_name,
332 is_public="False",
333 disk_format=disk_format,
334 container_format=container_format,
335 )
336
337 if image_checksum is not None:
338 create_args["checksum"] = image_checksum
339
340 try:
341 self._log.debug("creating an image from url: %s", create_args)
342 image = self._client.images.create(**create_args)
343 except Exception as e:
344 msg = "Failed to create image from url in openstack"
345 self._log.exception(msg)
346 raise OpenstackImageCreateError(msg) from e
347
348 return OpenstackImage.from_image_response(image)
349
350 def delete_image_from_id(self, image_id):
351 self._log.info("Deleting image from catalog: %s", image_id)
352 try:
353 image = self._client.images.delete(image_id)
354 except Exception as e:
355 msg = "Failed to delete image %s in openstack" % image_id
356 self._log.exception(msg)
357 raise OpenstackImageDeleteError(msg)