Richard Felkl | 4143a0e | 2017-02-01 23:24:13 +0100 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | ''' |
| 3 | Managing Images in OpenStack Glance |
| 4 | =================================== |
| 5 | ''' |
| 6 | # Import python libs |
| 7 | from __future__ import absolute_import |
| 8 | import logging |
| 9 | import time |
| 10 | |
Richard Felkl | 4143a0e | 2017-02-01 23:24:13 +0100 | [diff] [blame] | 11 | # Import OpenStack libs |
| 12 | try: |
| 13 | from keystoneclient.exceptions import \ |
| 14 | Unauthorized as kstone_Unauthorized |
| 15 | HAS_KEYSTONE = True |
| 16 | except 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 | |
| 24 | try: |
| 25 | from glanceclient.exc import \ |
| 26 | HTTPUnauthorized as glance_Unauthorized |
| 27 | HAS_GLANCE = True |
| 28 | except ImportError: |
| 29 | HAS_GLANCE = False |
| 30 | |
| 31 | log = logging.getLogger(__name__) |
| 32 | |
| 33 | |
| 34 | def __virtual__(): |
| 35 | ''' |
| 36 | Only load if dependencies are loaded |
| 37 | ''' |
| 38 | return HAS_KEYSTONE and HAS_GLANCE |
| 39 | |
| 40 | |
| 41 | def _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 Hryhorov | 6c21bbb | 2018-03-28 17:06:09 +0300 | [diff] [blame] | 49 | images = __salt__['glanceng.image_list'](name=name, profile=profile) |
Richard Felkl | 4143a0e | 2017-02-01 23:24:13 +0100 | [diff] [blame] | 50 | 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 Drung | 35de764 | 2018-02-14 23:54:30 +0100 | [diff] [blame] | 59 | images_list = list(images.values()) if type(images) is dict else images |
Richard Felkl | 4143a0e | 2017-02-01 23:24:13 +0100 | [diff] [blame] | 60 | |
| 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 | |
| 71 | def 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 Hryhorov | 6c21bbb | 2018-03-28 17:06:09 +0300 | [diff] [blame] | 129 | image = __salt__['glanceng.image_create'](name=name, profile=profile, |
Richard Felkl | 4143a0e | 2017-02-01 23:24:13 +0100 | [diff] [blame] | 130 | 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 Hryhorov | 6c21bbb | 2018-03-28 17:06:09 +0300 | [diff] [blame] | 187 | image = __salt__['glanceng.image_update']( |
Richard Felkl | 4143a0e | 2017-02-01 23:24:13 +0100 | [diff] [blame] | 188 | 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 Hryhorov | 6c21bbb | 2018-03-28 17:06:09 +0300 | [diff] [blame] | 225 | image = __salt__['glanceng.image_show'](image['id']) |
Richard Felkl | 4143a0e | 2017-02-01 23:24:13 +0100 | [diff] [blame] | 226 | 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 Ezhova | 1c2ebae | 2017-07-03 18:29:05 +0400 | [diff] [blame] | 247 | return ret |
| 248 | |
| 249 | |
| 250 | def 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 Hryhorov | 6c21bbb | 2018-03-28 17:06:09 +0300 | [diff] [blame] | 379 | image = __salt__['glanceng.image_show'](image['id']) |
Elena Ezhova | 1c2ebae | 2017-07-03 18:29:05 +0400 | [diff] [blame] | 380 | 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 |