| # -*- coding: utf-8 -*- |
| ''' |
| Managing Images in OpenStack Glance |
| =================================== |
| ''' |
| # Import python libs |
| import logging |
| import time |
| |
| |
| # Import OpenStack libs |
| def __virtual__(): |
| return 'glancev2' if 'glancev2.image_list' in __salt__ else False |
| |
| |
| log = logging.getLogger(__name__) |
| |
| |
| def _glancev2_call(fname, *args, **kwargs): |
| return __salt__['glancev2.{}'.format(fname)](*args, **kwargs) |
| |
| |
| def _cinderv3_call(fname, *args, **kwargs): |
| return __salt__['cinderv3.{}'.format(fname)](*args, **kwargs) |
| |
| |
| def image_present(name, cloud_name, location=None, image_properties=None, |
| import_from_format='raw', timeout=30, |
| sleep_time=5, checksum=None, |
| volume_name=None, volume_kwargs=None): |
| """ |
| Creates a task to import an image |
| |
| This state checks if an image is present and, if not, creates a task |
| with import_type that would download an image from a remote location and |
| upload it to Glance. |
| After the task is created, its status is monitored. On success the state |
| would check that an image is present and return its ID. |
| |
| Also, this state can update(add and replace) image properties, |
| but !!It can't delete properties, that are already in state |
| |
| :param name: name of the image |
| :param cloud_name: name of the cloud in cloud.yaml |
| :param location: url link describing where to obtain image |
| :param image_properties: Dict that contains params needed |
| to create or update image. |
| :param container_format: Format of the container |
| :param disk_format: Format of the disk |
| :param protected: If true, image will not be deletable. |
| :param tags: List of strings related to the image |
| :param visibility: Scope of image accessibility. |
| Valid values: public, private, community, shared |
| :param import_from_format: (optional) Format to import the image from |
| :param timeout: (optional) Time for task to download image or wait |
| until it becomes active if it is already present |
| :param sleep_time: (optional) Timer countdown |
| :param checksum: (optional) checksum of the image to verify it |
| :param volume_name: (optional) name of the volume if specified the command |
| works like openstack image create --volume |
| :param volume_kwargs: (optional) if volume_name is specified, this will be |
| used as arguments to image_upload_volume |
| """ |
| try: |
| exact_image = _glancev2_call( |
| 'image_get_details', name=name, cloud_name=cloud_name |
| ) |
| except Exception as e: |
| if 'ResourceNotFound' in repr(e): |
| if volume_name: |
| try: |
| _cinderv3_call( |
| 'volume_get_details', volume_name, |
| cloud_name=cloud_name |
| ) |
| except Exception as e: |
| if 'ResourceNotFound' in repr(e): |
| return _failed('none', volume_name, 'volume') |
| elif 'MultipleResourcesFound' in repr(e): |
| return _failed('find', volume_name, 'volume') |
| else: |
| raise |
| if not volume_kwargs: |
| volume_kwargs = {} |
| try: |
| resp = _cinderv3_call( |
| 'image_upload_volume', volume_name, image_name=name, |
| cloud_name=cloud_name, **volume_kwargs) |
| except Exception as e: |
| log.error( |
| 'Glance create image with ' |
| 'volume failed with {}'.format(e)) |
| return _failed('create', name, 'image') |
| image_status = resp['os-volume_upload_image']['status'] |
| while timeout > 0 and image_status != 'active': |
| try: |
| image = _glancev2_call('image_get_details', name=name, |
| cloud_name=cloud_name) |
| except Exception as e: |
| if 'ResourceNotFound' in repr(e): |
| timeout -= sleep_time |
| time.sleep(sleep_time) |
| continue |
| else: |
| raise |
| image_status = image['status'] |
| if image_status != 'active': |
| log.error( |
| 'Glance upload image to volume failed ' |
| 'for given amount of time' |
| ) |
| return _failed('create', name, 'image') |
| return _succeeded('create', name, 'image', image) |
| |
| else: |
| if not (location and image_properties): |
| return _failed('create', name, 'image') |
| image_properties['name'] = name |
| task_params = {"import_from": location, |
| "import_from_format": import_from_format, |
| "image_properties": image_properties |
| } |
| # Try create task |
| try: |
| task = _glancev2_call( |
| 'task_create', task_type='import', |
| task_input=task_params, cloud_name=cloud_name) |
| except Exception as e: |
| log.error( |
| 'Glance image create failed on ' |
| 'create task with {}'.format( |
| e) |
| ) |
| return _failed('create', name, 'image_task') |
| while timeout > 0: |
| if task['status'] == 'success': |
| break |
| elif task['status'] == 'failure': |
| log.error('Glance task failed to complete') |
| return _failed('create', name, 'image') |
| else: |
| timeout -= sleep_time |
| time.sleep(sleep_time) |
| # Check task status again |
| try: |
| task = _glancev2_call( |
| 'task_show', task_id=task['id'], |
| cloud_name=cloud_name |
| ) |
| except Exception as e: |
| log.error( |
| 'Glance failed to check ' |
| 'task status with {}'.format(e) |
| ) |
| return _failed('create', name, 'image_task') |
| if timeout <= 0 and task['status'] != 'success': |
| log.error( |
| 'Glance task failed to import ' |
| 'image for given amount of time' |
| ) |
| return _failed('create', name, 'image') |
| # Task successfully finished |
| # and now check that is created the image |
| |
| images = _glancev2_call( |
| 'image_list', name=name, cloud_name=cloud_name |
| )['images'] |
| |
| if not len(images): |
| return _failed('create', name, 'image') |
| image = images[0] |
| resp = _succeeded('create', name, 'image', image) |
| |
| if checksum: |
| if image['status'] == 'active': |
| if 'checksum' not in image: |
| log.error( |
| 'Glance image. No checksum for image.' |
| 'Image status is active' |
| ) |
| return _failed('create', name, 'image') |
| if image['checksum'] != checksum: |
| log.error( |
| 'Glance image create failed since ' |
| 'image_checksum should be ' |
| '{} but it is {}'.format(checksum, |
| image['checksum']) |
| ) |
| return _failed('create', name, 'image') |
| |
| elif image['status'] in ['saving', 'queued']: |
| resp['comment'] += (" checksum couldn't be verified, " |
| "since status is not active") |
| return resp |
| elif 'MultipleResourcesFound' in repr(e): |
| return _failed('find', name, 'image') |
| else: |
| raise |
| |
| # NOTE(pas-ha) fail the salt state if existing image is not in active state |
| image_status = exact_image['status'].copy() |
| while timeout > 0 and image_status != 'active': |
| log.warning("Image {name} is not ACTIVE in state, waiting..".format( |
| name=exact_image['name'])) |
| timeout -= sleep_time |
| time.sleep(sleep_time) |
| try: |
| image = _glancev2_call('image_get_details', name=name, |
| cloud_name=cloud_name) |
| except Exception: |
| raise |
| image_status = image['status'] |
| if image_status != 'active': |
| log.error("Image {name} failed to reach ACTIVE in state".format( |
| name=exact_image['name'])) |
| return _failed('present', name, 'image') |
| |
| to_change = [] |
| for prop in image_properties: |
| path = prop.replace('~', '~0').replace('/', '~1') |
| if prop in exact_image: |
| if exact_image[prop] != image_properties[prop]: |
| to_change.append({ |
| 'op': 'replace', |
| 'path': '/{}'.format(path), |
| 'value': image_properties[prop] |
| }) |
| else: |
| to_change.append({ |
| 'op': 'add', |
| 'path': '/{}'.format(path), |
| 'value': image_properties[prop] |
| }) |
| if to_change: |
| try: |
| resp = _glancev2_call( |
| 'image_update', name=name, |
| properties=to_change, cloud_name=cloud_name, |
| ) |
| except Exception as e: |
| log.error('Glance image update failed with {}'.format(e)) |
| return _failed('update', name, 'image') |
| return _succeeded('update', name, 'image', resp) |
| return _succeeded('no_changes', name, 'image') |
| |
| |
| def image_absent(name, cloud_name): |
| try: |
| _glancev2_call('image_get_details', name=name, cloud_name=cloud_name) |
| except Exception as e: |
| if 'ResourceNotFound' in repr(e): |
| return _succeeded('absent', name, 'image') |
| if 'MultipleResourcesFound' in repr(e): |
| return _failed('find', name, 'image') |
| try: |
| _glancev2_call('image_delete', name=name, cloud_name=cloud_name) |
| except Exception as e: |
| log.error('Glance image delete failed with {}'.format(e)) |
| return _failed('delete', name, 'image') |
| return _succeeded('delete', name, 'image') |
| |
| |
| def _succeeded(op, name, resource, changes=None): |
| msg_map = { |
| 'create': '{0} {1} created', |
| 'delete': '{0} {1} removed', |
| 'update': '{0} {1} updated', |
| 'no_changes': '{0} {1} is in desired state', |
| 'absent': '{0} {1} not present', |
| 'resources_moved': '{1} resources were moved from {0}', |
| } |
| changes_dict = { |
| 'name': name, |
| 'result': True, |
| 'comment': msg_map[op].format(resource, name), |
| 'changes': changes or {}, |
| } |
| return changes_dict |
| |
| |
| def _failed(op, name, resource): |
| msg_map = { |
| 'create': '{0} {1} failed to create', |
| 'delete': '{0} {1} failed to delete', |
| 'update': '{0} {1} failed to update', |
| 'find': '{0} {1} found multiple {0}', |
| 'none': '{0} {1} found no {0}', |
| 'resources_moved': 'failed to move {1} from {0}', |
| } |
| changes_dict = { |
| 'name': name, |
| 'result': False, |
| 'comment': msg_map[op].format(resource, name), |
| 'changes': {}, |
| } |
| return changes_dict |