|  | # -*- coding: utf-8 -*- | 
|  | ''' | 
|  | Managing Images in OpenStack Glance | 
|  | =================================== | 
|  | ''' | 
|  | # Import python libs | 
|  | from __future__ import absolute_import | 
|  | import logging | 
|  | import time | 
|  |  | 
|  | # Import OpenStack libs | 
|  | try: | 
|  | from keystoneclient.exceptions import \ | 
|  | Unauthorized as kstone_Unauthorized | 
|  | HAS_KEYSTONE = True | 
|  | except ImportError: | 
|  | try: | 
|  | from keystoneclient.apiclient.exceptions import \ | 
|  | Unauthorized as kstone_Unauthorized | 
|  | HAS_KEYSTONE = True | 
|  | except ImportError: | 
|  | HAS_KEYSTONE = False | 
|  |  | 
|  | try: | 
|  | from glanceclient.exc import \ | 
|  | HTTPUnauthorized as glance_Unauthorized | 
|  | HAS_GLANCE = True | 
|  | except ImportError: | 
|  | HAS_GLANCE = False | 
|  |  | 
|  | log = logging.getLogger(__name__) | 
|  |  | 
|  |  | 
|  | def __virtual__(): | 
|  | ''' | 
|  | Only load if dependencies are loaded | 
|  | ''' | 
|  | return HAS_KEYSTONE and HAS_GLANCE | 
|  |  | 
|  |  | 
|  | def _find_image(name, profile=None): | 
|  | ''' | 
|  | Tries to find image with given name, returns | 
|  | - image, 'Found image <name>' | 
|  | - None, 'No such image found' | 
|  | - False, 'Found more than one image with given name' | 
|  | ''' | 
|  | try: | 
|  | images = __salt__['glanceng.image_list'](name=name, profile=profile) | 
|  | except kstone_Unauthorized: | 
|  | return False, 'keystoneclient: Unauthorized' | 
|  | except glance_Unauthorized: | 
|  | return False, 'glanceclient: Unauthorized' | 
|  | log.debug('Got images: {0}'.format(images)) | 
|  |  | 
|  | if type(images) is dict and len(images) == 1 and 'images' in images: | 
|  | images = images['images'] | 
|  |  | 
|  | images_list = list(images.values()) if type(images) is dict else images | 
|  |  | 
|  | if len(images_list) == 0: | 
|  | return None, 'No image with name "{0}"'.format(name) | 
|  | elif len(images_list) == 1: | 
|  | return images_list[0], 'Found image {0}'.format(name) | 
|  | elif len(images_list) > 1: | 
|  | return False, 'Found more than one image with given name' | 
|  | else: | 
|  | raise NotImplementedError | 
|  |  | 
|  |  | 
|  | def image_present(name, profile=None, visibility='public', protected=None, | 
|  | checksum=None, location=None, disk_format='raw', wait_for=None, | 
|  | timeout=30): | 
|  | ''' | 
|  | Checks if given image is present with properties | 
|  | set as specified. | 
|  | An image should got through the stages 'queued', 'saving' | 
|  | before becoming 'active'. The attribute 'checksum' can | 
|  | only be checked once the image is active. | 
|  | If you don't specify 'wait_for' but 'checksum' the function | 
|  | will wait for the image to become active before comparing | 
|  | checksums. If you don't specify checksum either the function | 
|  | will return when the image reached 'saving'. | 
|  | The default timeout for both is 30 seconds. | 
|  | Supported properties: | 
|  | - profile (string) | 
|  | - visibility ('public' or 'private') | 
|  | - protected (bool) | 
|  | - checksum (string, md5sum) | 
|  | - location (URL, to copy from) | 
|  | - disk_format ('raw' (default), 'vhd', 'vhdx', 'vmdk', 'vdi', 'iso', | 
|  | 'qcow2', 'aki', 'ari' or 'ami') | 
|  | ''' | 
|  | ret = {'name': name, | 
|  | 'changes': {}, | 
|  | 'result': True, | 
|  | 'comment': '', | 
|  | } | 
|  | acceptable = ['queued', 'saving', 'active'] | 
|  | if wait_for is None and checksum is None: | 
|  | wait_for = 'saving' | 
|  | elif wait_for is None and checksum is not None: | 
|  | wait_for = 'active' | 
|  |  | 
|  | # Just pop states until we reach the | 
|  | # first acceptable one: | 
|  | while len(acceptable) > 1: | 
|  | if acceptable[0] == wait_for: | 
|  | break | 
|  | else: | 
|  | acceptable.pop(0) | 
|  |  | 
|  | image, msg = _find_image(name, profile) | 
|  | if image is False: | 
|  | if __opts__['test']: | 
|  | ret['result'] = None | 
|  | else: | 
|  | ret['result'] = False | 
|  | ret['comment'] = msg | 
|  | return ret | 
|  | log.debug(msg) | 
|  | # No image yet and we know where to get one | 
|  | if image is None and location is not None: | 
|  | if __opts__['test']: | 
|  | ret['result'] = None | 
|  | ret['comment'] = 'glance.image_present would ' \ | 
|  | 'create an image from {0}'.format(location) | 
|  | return ret | 
|  | image = __salt__['glanceng.image_create'](name=name, profile=profile, | 
|  | protected=protected, visibility=visibility, | 
|  | location=location, disk_format=disk_format) | 
|  | log.debug('Created new image:\n{0}'.format(image)) | 
|  | ret['changes'] = { | 
|  | name: | 
|  | { | 
|  | 'new': | 
|  | { | 
|  | 'id': image['id'] | 
|  | }, | 
|  | 'old': None | 
|  | } | 
|  | } | 
|  | timer = timeout | 
|  | # Kinda busy-loopy but I don't think the Glance | 
|  | # API has events we can listen for | 
|  | while timer > 0: | 
|  | if 'status' in image and \ | 
|  | image['status'] in acceptable: | 
|  | log.debug('Image {0} has reached status {1}'.format( | 
|  | image['name'], image['status'])) | 
|  | break | 
|  | else: | 
|  | timer -= 5 | 
|  | time.sleep(5) | 
|  | image, msg = _find_image(name, profile) | 
|  | if not image: | 
|  | ret['result'] = False | 
|  | ret['comment'] += 'Created image {0} '.format( | 
|  | name) + ' vanished:\n' + msg | 
|  | return ret | 
|  | if timer <= 0 and image['status'] not in acceptable: | 
|  | ret['result'] = False | 
|  | ret['comment'] += 'Image didn\'t reach an acceptable '+\ | 
|  | 'state ({0}) before timeout:\n'.format(acceptable)+\ | 
|  | '\tLast status was "{0}".\n'.format(image['status']) | 
|  |  | 
|  | # There's no image but where would I get one?? | 
|  | elif location is None: | 
|  | if __opts__['test']: | 
|  | ret['result'] = None | 
|  | ret['comment'] = 'No location to copy image from specified,\n' +\ | 
|  | 'glance.image_present would not create one' | 
|  | else: | 
|  | ret['result'] = False | 
|  | ret['comment'] = 'No location to copy image from specified,\n' +\ | 
|  | 'not creating a new image.' | 
|  | return ret | 
|  |  | 
|  | # If we've created a new image also return its last status: | 
|  | if name in ret['changes']: | 
|  | ret['changes'][name]['new']['status'] = image['status'] | 
|  |  | 
|  | if visibility: | 
|  | if image['visibility'] != visibility: | 
|  | old_value = image['visibility'] | 
|  | if not __opts__['test']: | 
|  | image = __salt__['glanceng.image_update']( | 
|  | id=image['id'], visibility=visibility) | 
|  | # Check if image_update() worked: | 
|  | if image['visibility'] != visibility: | 
|  | if not __opts__['test']: | 
|  | ret['result'] = False | 
|  | elif __opts__['test']: | 
|  | ret['result'] = None | 
|  | ret['comment'] += '"visibility" is {0}, '\ | 
|  | 'should be {1}.\n'.format(image['visibility'], | 
|  | visibility) | 
|  | else: | 
|  | if 'new' in ret['changes']: | 
|  | ret['changes']['new']['visibility'] = visibility | 
|  | else: | 
|  | ret['changes']['new'] = {'visibility': visibility} | 
|  | if 'old' in ret['changes']: | 
|  | ret['changes']['old']['visibility'] = old_value | 
|  | else: | 
|  | ret['changes']['old'] = {'visibility': old_value} | 
|  | else: | 
|  | ret['comment'] += '"visibility" is correct ({0}).\n'.format( | 
|  | visibility) | 
|  | if protected is not None: | 
|  | if not isinstance(protected, bool) or image['protected'] ^ protected: | 
|  | if not __opts__['test']: | 
|  | ret['result'] = False | 
|  | else: | 
|  | ret['result'] = None | 
|  | ret['comment'] += '"protected" is {0}, should be {1}.\n'.format( | 
|  | image['protected'], protected) | 
|  | else: | 
|  | ret['comment'] += '"protected" is correct ({0}).\n'.format( | 
|  | protected) | 
|  | if 'status' in image and checksum: | 
|  | if image['status'] == 'active': | 
|  | if 'checksum' not in image: | 
|  | # Refresh our info about the image | 
|  | image = __salt__['glanceng.image_show'](image['id']) | 
|  | if 'checksum' not in image: | 
|  | if not __opts__['test']: | 
|  | ret['result'] = False | 
|  | else: | 
|  | ret['result'] = None | 
|  | ret['comment'] += 'No checksum available for this image:\n' +\ | 
|  | '\tImage has status "{0}".'.format(image['status']) | 
|  | elif image['checksum'] != checksum: | 
|  | if not __opts__['test']: | 
|  | ret['result'] = False | 
|  | else: | 
|  | ret['result'] = None | 
|  | ret['comment'] += '"checksum" is {0}, should be {1}.\n'.format( | 
|  | image['checksum'], checksum) | 
|  | else: | 
|  | ret['comment'] += '"checksum" is correct ({0}).\n'.format( | 
|  | checksum) | 
|  | elif image['status'] in ['saving', 'queued']: | 
|  | ret['comment'] += 'Checksum won\'t be verified as image ' +\ | 
|  | 'hasn\'t reached\n\t "status=active" yet.\n' | 
|  | log.debug('glance.image_present will return: {0}'.format(ret)) | 
|  | return ret | 
|  |  | 
|  |  | 
|  | def image_import(name, profile=None, visibility='public', protected=False, | 
|  | location=None, import_from_format='raw', disk_format='raw', | 
|  | container_format='bare', tags=None, | 
|  | checksum=None, timeout=30): | 
|  | """ | 
|  | 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. | 
|  |  | 
|  | *Important*: This state is supposed to work only with Glance V2 API as | 
|  | opposed to the image_present state that is compatible only | 
|  | with Glance V1. | 
|  |  | 
|  | :param name: Name of an image | 
|  | :param profile: Authentication profile | 
|  | :param visibility: Scope of image accessibility. | 
|  | Valid values: public, private, community, shared | 
|  | :param protected: If true, image will not be deletable. | 
|  | :param location: a URL where Glance can get the image data | 
|  | :param import_from_format: Format to import the image from | 
|  | :param disk_format: Format of the disk | 
|  | :param container_format: Format of the container | 
|  | :param tags: List of strings related to the image | 
|  | :param checksum: Checksum of the image to import, it would be used to | 
|  | validate the checksum of a newly created image | 
|  | :param timeout: Time to wait for an import task to succeed | 
|  | """ | 
|  |  | 
|  | ret = {'name': name, | 
|  | 'changes': {}, | 
|  | 'result': True, | 
|  | 'comment': 'Image "{0}" already exists'.format(name)} | 
|  | tags = tags or [] | 
|  |  | 
|  | image, msg = _find_image(name, profile) | 
|  | log.debug(msg) | 
|  | if image: | 
|  | return ret | 
|  | elif image is False: | 
|  | if __opts__['test']: | 
|  | ret['result'] = None | 
|  | else: | 
|  | ret['result'] = False | 
|  | ret['comment'] = msg | 
|  | return ret | 
|  | else: | 
|  | if __opts__['test']: | 
|  | ret['result'] = None | 
|  | ret['comment'] = ("glanceng.image_import would create an image " | 
|  | "from {0}".format(location)) | 
|  | return ret | 
|  |  | 
|  | image_properties = {"container_format": container_format, | 
|  | "disk_format": disk_format, | 
|  | "name": name, | 
|  | "protected": protected, | 
|  | "tags": tags, | 
|  | "visibility": visibility | 
|  | } | 
|  | task_params = {"import_from": location, | 
|  | "import_from_format": import_from_format, | 
|  | "image_properties": image_properties | 
|  | } | 
|  |  | 
|  | task = __salt__['glanceng.task_create']( | 
|  | task_type='import', profile=profile, | 
|  | input_params=task_params) | 
|  | task_id = task['id'] | 
|  | log.debug('Created new task:\n{0}'.format(task)) | 
|  | ret['changes'] = { | 
|  | name: | 
|  | { | 
|  | 'new': | 
|  | { | 
|  | 'task_id': task_id | 
|  | }, | 
|  | 'old': None | 
|  | } | 
|  | } | 
|  |  | 
|  | # Wait for the task to complete | 
|  | timer = timeout | 
|  | while timer > 0: | 
|  | if 'status' in task and task['status'] == 'success': | 
|  | log.debug('Task {0} has successfully completed'.format( | 
|  | task_id)) | 
|  | break | 
|  | elif 'status' in task and task['status'] == 'failure': | 
|  | msg = "Task {0} has failed".format(task_id) | 
|  | ret['result'] = False | 
|  | ret['comment'] = msg | 
|  | return ret | 
|  | else: | 
|  | timer -= 5 | 
|  | time.sleep(5) | 
|  | existing_tasks = __salt__['glanceng.task_list'](profile) | 
|  | if task_id not in existing_tasks: | 
|  | ret['result'] = False | 
|  | ret['comment'] += 'Created task {0} '.format( | 
|  | task_id) + ' vanished:\n' + msg | 
|  | return ret | 
|  | else: | 
|  | task = existing_tasks[task_id] | 
|  | if timer <= 0 and task['status'] != 'success': | 
|  | ret['result'] = False | 
|  | ret['comment'] = ('Task {0} did not reach state success before ' | 
|  | 'the timeout:\nLast status was ' | 
|  | '"{1}".\n'.format(task_id, task['status'])) | 
|  | return ret | 
|  |  | 
|  | # The import task has successfully completed. Now, let's check that it | 
|  | # created the image. | 
|  | image, msg = _find_image(name, profile) | 
|  | if not image: | 
|  | ret['result'] = False | 
|  | ret['comment'] = msg | 
|  | else: | 
|  | ret['changes'][name]['new']['image_id'] = image['id'] | 
|  | ret['changes'][name]['new']['image_status'] = image['status'] | 
|  | ret['comment'] = ("Image {0} was successfully created by task " | 
|  | "{1}".format(image['id'], task_id)) | 
|  | if checksum: | 
|  | if image['status'] == 'active': | 
|  | if 'checksum' not in image: | 
|  | # Refresh our info about the image | 
|  | image = __salt__['glanceng.image_show'](image['id']) | 
|  | if 'checksum' not in image: | 
|  | if not __opts__['test']: | 
|  | ret['result'] = False | 
|  | else: | 
|  | ret['result'] = None | 
|  | ret['comment'] += ( | 
|  | "No checksum available for this image:\n" | 
|  | "Image has status '{0}'.".format(image['status'])) | 
|  | elif image['checksum'] != checksum: | 
|  | if not __opts__['test']: | 
|  | ret['result'] = False | 
|  | else: | 
|  | ret['result'] = None | 
|  | ret['comment'] += ("'checksum' is {0}, should be " | 
|  | "{1}.\n".format(image['checksum'], | 
|  | checksum)) | 
|  | else: | 
|  | ret['comment'] += ( | 
|  | "'checksum' is correct ({0}).\n".format(checksum)) | 
|  | elif image['status'] in ['saving', 'queued']: | 
|  | ret['comment'] += ( | 
|  | "Checksum will not be verified as image has not " | 
|  | "reached 'status=active' yet.\n") | 
|  | log.debug('glance.image_present will return: {0}'.format(ret)) | 
|  | return ret |