blob: e59dcbd8c931430cc506f305ac31ae5f7cbcd5e2 [file] [log] [blame]
Richard Felkl4143a0e2017-02-01 23:24:13 +01001# -*- coding: utf-8 -*-
2'''
3Managing Images in OpenStack Glance
4===================================
5'''
6# Import python libs
7from __future__ import absolute_import
8import logging
9import time
10
Richard Felkl4143a0e2017-02-01 23:24:13 +010011# Import OpenStack libs
12try:
13 from keystoneclient.exceptions import \
14 Unauthorized as kstone_Unauthorized
15 HAS_KEYSTONE = True
16except ImportError:
17 try:
18 from keystoneclient.apiclient.exceptions import \
19 Unauthorized as kstone_Unauthorized
20 HAS_KEYSTONE = True
21 except ImportError:
22 HAS_KEYSTONE = False
23
24try:
25 from glanceclient.exc import \
26 HTTPUnauthorized as glance_Unauthorized
27 HAS_GLANCE = True
28except ImportError:
29 HAS_GLANCE = False
30
31log = logging.getLogger(__name__)
32
33
34def __virtual__():
35 '''
36 Only load if dependencies are loaded
37 '''
38 return HAS_KEYSTONE and HAS_GLANCE
39
40
41def _find_image(name, profile=None):
42 '''
43 Tries to find image with given name, returns
44 - image, 'Found image <name>'
45 - None, 'No such image found'
46 - False, 'Found more than one image with given name'
47 '''
48 try:
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +030049 images = __salt__['glanceng.image_list'](name=name, profile=profile)
Richard Felkl4143a0e2017-02-01 23:24:13 +010050 except kstone_Unauthorized:
51 return False, 'keystoneclient: Unauthorized'
52 except glance_Unauthorized:
53 return False, 'glanceclient: Unauthorized'
54 log.debug('Got images: {0}'.format(images))
55
56 if type(images) is dict and len(images) == 1 and 'images' in images:
57 images = images['images']
58
Benjamin Drung35de7642018-02-14 23:54:30 +010059 images_list = list(images.values()) if type(images) is dict else images
Richard Felkl4143a0e2017-02-01 23:24:13 +010060
61 if len(images_list) == 0:
62 return None, 'No image with name "{0}"'.format(name)
63 elif len(images_list) == 1:
64 return images_list[0], 'Found image {0}'.format(name)
65 elif len(images_list) > 1:
66 return False, 'Found more than one image with given name'
67 else:
68 raise NotImplementedError
69
70
71def image_present(name, profile=None, visibility='public', protected=None,
72 checksum=None, location=None, disk_format='raw', wait_for=None,
73 timeout=30):
74 '''
75 Checks if given image is present with properties
76 set as specified.
77 An image should got through the stages 'queued', 'saving'
78 before becoming 'active'. The attribute 'checksum' can
79 only be checked once the image is active.
80 If you don't specify 'wait_for' but 'checksum' the function
81 will wait for the image to become active before comparing
82 checksums. If you don't specify checksum either the function
83 will return when the image reached 'saving'.
84 The default timeout for both is 30 seconds.
85 Supported properties:
86 - profile (string)
87 - visibility ('public' or 'private')
88 - protected (bool)
89 - checksum (string, md5sum)
90 - location (URL, to copy from)
91 - disk_format ('raw' (default), 'vhd', 'vhdx', 'vmdk', 'vdi', 'iso',
92 'qcow2', 'aki', 'ari' or 'ami')
93 '''
94 ret = {'name': name,
95 'changes': {},
96 'result': True,
97 'comment': '',
98 }
99 acceptable = ['queued', 'saving', 'active']
100 if wait_for is None and checksum is None:
101 wait_for = 'saving'
102 elif wait_for is None and checksum is not None:
103 wait_for = 'active'
104
105 # Just pop states until we reach the
106 # first acceptable one:
107 while len(acceptable) > 1:
108 if acceptable[0] == wait_for:
109 break
110 else:
111 acceptable.pop(0)
112
113 image, msg = _find_image(name, profile)
114 if image is False:
115 if __opts__['test']:
116 ret['result'] = None
117 else:
118 ret['result'] = False
119 ret['comment'] = msg
120 return ret
121 log.debug(msg)
122 # No image yet and we know where to get one
123 if image is None and location is not None:
124 if __opts__['test']:
125 ret['result'] = None
126 ret['comment'] = 'glance.image_present would ' \
127 'create an image from {0}'.format(location)
128 return ret
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300129 image = __salt__['glanceng.image_create'](name=name, profile=profile,
Richard Felkl4143a0e2017-02-01 23:24:13 +0100130 protected=protected, visibility=visibility,
131 location=location, disk_format=disk_format)
132 log.debug('Created new image:\n{0}'.format(image))
133 ret['changes'] = {
134 name:
135 {
136 'new':
137 {
138 'id': image['id']
139 },
140 'old': None
141 }
142 }
143 timer = timeout
144 # Kinda busy-loopy but I don't think the Glance
145 # API has events we can listen for
146 while timer > 0:
147 if 'status' in image and \
148 image['status'] in acceptable:
149 log.debug('Image {0} has reached status {1}'.format(
150 image['name'], image['status']))
151 break
152 else:
153 timer -= 5
154 time.sleep(5)
155 image, msg = _find_image(name, profile)
156 if not image:
157 ret['result'] = False
158 ret['comment'] += 'Created image {0} '.format(
159 name) + ' vanished:\n' + msg
160 return ret
161 if timer <= 0 and image['status'] not in acceptable:
162 ret['result'] = False
163 ret['comment'] += 'Image didn\'t reach an acceptable '+\
164 'state ({0}) before timeout:\n'.format(acceptable)+\
165 '\tLast status was "{0}".\n'.format(image['status'])
166
167 # There's no image but where would I get one??
168 elif location is None:
169 if __opts__['test']:
170 ret['result'] = None
171 ret['comment'] = 'No location to copy image from specified,\n' +\
172 'glance.image_present would not create one'
173 else:
174 ret['result'] = False
175 ret['comment'] = 'No location to copy image from specified,\n' +\
176 'not creating a new image.'
177 return ret
178
179 # If we've created a new image also return its last status:
180 if name in ret['changes']:
181 ret['changes'][name]['new']['status'] = image['status']
182
183 if visibility:
184 if image['visibility'] != visibility:
185 old_value = image['visibility']
186 if not __opts__['test']:
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300187 image = __salt__['glanceng.image_update'](
Richard Felkl4143a0e2017-02-01 23:24:13 +0100188 id=image['id'], visibility=visibility)
189 # Check if image_update() worked:
190 if image['visibility'] != visibility:
191 if not __opts__['test']:
192 ret['result'] = False
193 elif __opts__['test']:
194 ret['result'] = None
195 ret['comment'] += '"visibility" is {0}, '\
196 'should be {1}.\n'.format(image['visibility'],
197 visibility)
198 else:
199 if 'new' in ret['changes']:
200 ret['changes']['new']['visibility'] = visibility
201 else:
202 ret['changes']['new'] = {'visibility': visibility}
203 if 'old' in ret['changes']:
204 ret['changes']['old']['visibility'] = old_value
205 else:
206 ret['changes']['old'] = {'visibility': old_value}
207 else:
208 ret['comment'] += '"visibility" is correct ({0}).\n'.format(
209 visibility)
210 if protected is not None:
211 if not isinstance(protected, bool) or image['protected'] ^ protected:
212 if not __opts__['test']:
213 ret['result'] = False
214 else:
215 ret['result'] = None
216 ret['comment'] += '"protected" is {0}, should be {1}.\n'.format(
217 image['protected'], protected)
218 else:
219 ret['comment'] += '"protected" is correct ({0}).\n'.format(
220 protected)
221 if 'status' in image and checksum:
222 if image['status'] == 'active':
223 if 'checksum' not in image:
224 # Refresh our info about the image
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300225 image = __salt__['glanceng.image_show'](image['id'])
Richard Felkl4143a0e2017-02-01 23:24:13 +0100226 if 'checksum' not in image:
227 if not __opts__['test']:
228 ret['result'] = False
229 else:
230 ret['result'] = None
231 ret['comment'] += 'No checksum available for this image:\n' +\
232 '\tImage has status "{0}".'.format(image['status'])
233 elif image['checksum'] != checksum:
234 if not __opts__['test']:
235 ret['result'] = False
236 else:
237 ret['result'] = None
238 ret['comment'] += '"checksum" is {0}, should be {1}.\n'.format(
239 image['checksum'], checksum)
240 else:
241 ret['comment'] += '"checksum" is correct ({0}).\n'.format(
242 checksum)
243 elif image['status'] in ['saving', 'queued']:
244 ret['comment'] += 'Checksum won\'t be verified as image ' +\
245 'hasn\'t reached\n\t "status=active" yet.\n'
246 log.debug('glance.image_present will return: {0}'.format(ret))
Elena Ezhova1c2ebae2017-07-03 18:29:05 +0400247 return ret
248
249
250def image_import(name, profile=None, visibility='public', protected=False,
251 location=None, import_from_format='raw', disk_format='raw',
252 container_format='bare', tags=None,
253 checksum=None, timeout=30):
254 """
255 Creates a task to import an image
256
257 This state checks if an image is present and, if not, creates a task
258 with import_type that would download an image from a remote location and
259 upload it to Glance.
260 After the task is created, its status is monitored. On success the state
261 would check that an image is present and return its ID.
262
263 *Important*: This state is supposed to work only with Glance V2 API as
264 opposed to the image_present state that is compatible only
265 with Glance V1.
266
267 :param name: Name of an image
268 :param profile: Authentication profile
269 :param visibility: Scope of image accessibility.
270 Valid values: public, private, community, shared
271 :param protected: If true, image will not be deletable.
272 :param location: a URL where Glance can get the image data
273 :param import_from_format: Format to import the image from
274 :param disk_format: Format of the disk
275 :param container_format: Format of the container
276 :param tags: List of strings related to the image
277 :param checksum: Checksum of the image to import, it would be used to
278 validate the checksum of a newly created image
279 :param timeout: Time to wait for an import task to succeed
280 """
281
282 ret = {'name': name,
283 'changes': {},
284 'result': True,
285 'comment': 'Image "{0}" already exists'.format(name)}
286 tags = tags or []
287
288 image, msg = _find_image(name, profile)
289 log.debug(msg)
290 if image:
291 return ret
292 elif image is False:
293 if __opts__['test']:
294 ret['result'] = None
295 else:
296 ret['result'] = False
297 ret['comment'] = msg
298 return ret
299 else:
300 if __opts__['test']:
301 ret['result'] = None
302 ret['comment'] = ("glanceng.image_import would create an image "
303 "from {0}".format(location))
304 return ret
305
306 image_properties = {"container_format": container_format,
307 "disk_format": disk_format,
308 "name": name,
309 "protected": protected,
310 "tags": tags,
311 "visibility": visibility
312 }
313 task_params = {"import_from": location,
314 "import_from_format": import_from_format,
315 "image_properties": image_properties
316 }
317
318 task = __salt__['glanceng.task_create'](
319 task_type='import', profile=profile,
320 input_params=task_params)
321 task_id = task['id']
322 log.debug('Created new task:\n{0}'.format(task))
323 ret['changes'] = {
324 name:
325 {
326 'new':
327 {
328 'task_id': task_id
329 },
330 'old': None
331 }
332 }
333
334 # Wait for the task to complete
335 timer = timeout
336 while timer > 0:
337 if 'status' in task and task['status'] == 'success':
338 log.debug('Task {0} has successfully completed'.format(
339 task_id))
340 break
341 elif 'status' in task and task['status'] == 'failure':
342 msg = "Task {0} has failed".format(task_id)
343 ret['result'] = False
344 ret['comment'] = msg
345 return ret
346 else:
347 timer -= 5
348 time.sleep(5)
349 existing_tasks = __salt__['glanceng.task_list'](profile)
350 if task_id not in existing_tasks:
351 ret['result'] = False
352 ret['comment'] += 'Created task {0} '.format(
353 task_id) + ' vanished:\n' + msg
354 return ret
355 else:
356 task = existing_tasks[task_id]
357 if timer <= 0 and task['status'] != 'success':
358 ret['result'] = False
359 ret['comment'] = ('Task {0} did not reach state success before '
360 'the timeout:\nLast status was '
361 '"{1}".\n'.format(task_id, task['status']))
362 return ret
363
364 # The import task has successfully completed. Now, let's check that it
365 # created the image.
366 image, msg = _find_image(name, profile)
367 if not image:
368 ret['result'] = False
369 ret['comment'] = msg
370 else:
371 ret['changes'][name]['new']['image_id'] = image['id']
372 ret['changes'][name]['new']['image_status'] = image['status']
373 ret['comment'] = ("Image {0} was successfully created by task "
374 "{1}".format(image['id'], task_id))
375 if checksum:
376 if image['status'] == 'active':
377 if 'checksum' not in image:
378 # Refresh our info about the image
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300379 image = __salt__['glanceng.image_show'](image['id'])
Elena Ezhova1c2ebae2017-07-03 18:29:05 +0400380 if 'checksum' not in image:
381 if not __opts__['test']:
382 ret['result'] = False
383 else:
384 ret['result'] = None
385 ret['comment'] += (
386 "No checksum available for this image:\n"
387 "Image has status '{0}'.".format(image['status']))
388 elif image['checksum'] != checksum:
389 if not __opts__['test']:
390 ret['result'] = False
391 else:
392 ret['result'] = None
393 ret['comment'] += ("'checksum' is {0}, should be "
394 "{1}.\n".format(image['checksum'],
395 checksum))
396 else:
397 ret['comment'] += (
398 "'checksum' is correct ({0}).\n".format(checksum))
399 elif image['status'] in ['saving', 'queued']:
400 ret['comment'] += (
401 "Checksum will not be verified as image has not "
402 "reached 'status=active' yet.\n")
403 log.debug('glance.image_present will return: {0}'.format(ret))
404 return ret