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 | |
| 11 | # Import salt libs |
| 12 | |
| 13 | # Import OpenStack libs |
| 14 | try: |
| 15 | from keystoneclient.exceptions import \ |
| 16 | Unauthorized as kstone_Unauthorized |
| 17 | HAS_KEYSTONE = True |
| 18 | except ImportError: |
| 19 | try: |
| 20 | from keystoneclient.apiclient.exceptions import \ |
| 21 | Unauthorized as kstone_Unauthorized |
| 22 | HAS_KEYSTONE = True |
| 23 | except ImportError: |
| 24 | HAS_KEYSTONE = False |
| 25 | |
| 26 | try: |
| 27 | from glanceclient.exc import \ |
| 28 | HTTPUnauthorized as glance_Unauthorized |
| 29 | HAS_GLANCE = True |
| 30 | except ImportError: |
| 31 | HAS_GLANCE = False |
| 32 | |
| 33 | log = logging.getLogger(__name__) |
| 34 | |
| 35 | |
| 36 | def __virtual__(): |
| 37 | ''' |
| 38 | Only load if dependencies are loaded |
| 39 | ''' |
| 40 | return HAS_KEYSTONE and HAS_GLANCE |
| 41 | |
| 42 | |
| 43 | def _find_image(name, profile=None): |
| 44 | ''' |
| 45 | Tries to find image with given name, returns |
| 46 | - image, 'Found image <name>' |
| 47 | - None, 'No such image found' |
| 48 | - False, 'Found more than one image with given name' |
| 49 | ''' |
| 50 | try: |
| 51 | images = __salt__['glance.image_list'](name=name, profile=profile) |
| 52 | except kstone_Unauthorized: |
| 53 | return False, 'keystoneclient: Unauthorized' |
| 54 | except glance_Unauthorized: |
| 55 | return False, 'glanceclient: Unauthorized' |
| 56 | log.debug('Got images: {0}'.format(images)) |
| 57 | |
| 58 | if type(images) is dict and len(images) == 1 and 'images' in images: |
| 59 | images = images['images'] |
| 60 | |
| 61 | images_list = images.values() if type(images) is dict else images |
| 62 | |
| 63 | if len(images_list) == 0: |
| 64 | return None, 'No image with name "{0}"'.format(name) |
| 65 | elif len(images_list) == 1: |
| 66 | return images_list[0], 'Found image {0}'.format(name) |
| 67 | elif len(images_list) > 1: |
| 68 | return False, 'Found more than one image with given name' |
| 69 | else: |
| 70 | raise NotImplementedError |
| 71 | |
| 72 | |
| 73 | def image_present(name, profile=None, visibility='public', protected=None, |
| 74 | checksum=None, location=None, disk_format='raw', wait_for=None, |
| 75 | timeout=30): |
| 76 | ''' |
| 77 | Checks if given image is present with properties |
| 78 | set as specified. |
| 79 | An image should got through the stages 'queued', 'saving' |
| 80 | before becoming 'active'. The attribute 'checksum' can |
| 81 | only be checked once the image is active. |
| 82 | If you don't specify 'wait_for' but 'checksum' the function |
| 83 | will wait for the image to become active before comparing |
| 84 | checksums. If you don't specify checksum either the function |
| 85 | will return when the image reached 'saving'. |
| 86 | The default timeout for both is 30 seconds. |
| 87 | Supported properties: |
| 88 | - profile (string) |
| 89 | - visibility ('public' or 'private') |
| 90 | - protected (bool) |
| 91 | - checksum (string, md5sum) |
| 92 | - location (URL, to copy from) |
| 93 | - disk_format ('raw' (default), 'vhd', 'vhdx', 'vmdk', 'vdi', 'iso', |
| 94 | 'qcow2', 'aki', 'ari' or 'ami') |
| 95 | ''' |
| 96 | ret = {'name': name, |
| 97 | 'changes': {}, |
| 98 | 'result': True, |
| 99 | 'comment': '', |
| 100 | } |
| 101 | acceptable = ['queued', 'saving', 'active'] |
| 102 | if wait_for is None and checksum is None: |
| 103 | wait_for = 'saving' |
| 104 | elif wait_for is None and checksum is not None: |
| 105 | wait_for = 'active' |
| 106 | |
| 107 | # Just pop states until we reach the |
| 108 | # first acceptable one: |
| 109 | while len(acceptable) > 1: |
| 110 | if acceptable[0] == wait_for: |
| 111 | break |
| 112 | else: |
| 113 | acceptable.pop(0) |
| 114 | |
| 115 | image, msg = _find_image(name, profile) |
| 116 | if image is False: |
| 117 | if __opts__['test']: |
| 118 | ret['result'] = None |
| 119 | else: |
| 120 | ret['result'] = False |
| 121 | ret['comment'] = msg |
| 122 | return ret |
| 123 | log.debug(msg) |
| 124 | # No image yet and we know where to get one |
| 125 | if image is None and location is not None: |
| 126 | if __opts__['test']: |
| 127 | ret['result'] = None |
| 128 | ret['comment'] = 'glance.image_present would ' \ |
| 129 | 'create an image from {0}'.format(location) |
| 130 | return ret |
| 131 | image = __salt__['glance.image_create'](name=name, profile=profile, |
| 132 | protected=protected, visibility=visibility, |
| 133 | location=location, disk_format=disk_format) |
| 134 | log.debug('Created new image:\n{0}'.format(image)) |
| 135 | ret['changes'] = { |
| 136 | name: |
| 137 | { |
| 138 | 'new': |
| 139 | { |
| 140 | 'id': image['id'] |
| 141 | }, |
| 142 | 'old': None |
| 143 | } |
| 144 | } |
| 145 | timer = timeout |
| 146 | # Kinda busy-loopy but I don't think the Glance |
| 147 | # API has events we can listen for |
| 148 | while timer > 0: |
| 149 | if 'status' in image and \ |
| 150 | image['status'] in acceptable: |
| 151 | log.debug('Image {0} has reached status {1}'.format( |
| 152 | image['name'], image['status'])) |
| 153 | break |
| 154 | else: |
| 155 | timer -= 5 |
| 156 | time.sleep(5) |
| 157 | image, msg = _find_image(name, profile) |
| 158 | if not image: |
| 159 | ret['result'] = False |
| 160 | ret['comment'] += 'Created image {0} '.format( |
| 161 | name) + ' vanished:\n' + msg |
| 162 | return ret |
| 163 | if timer <= 0 and image['status'] not in acceptable: |
| 164 | ret['result'] = False |
| 165 | ret['comment'] += 'Image didn\'t reach an acceptable '+\ |
| 166 | 'state ({0}) before timeout:\n'.format(acceptable)+\ |
| 167 | '\tLast status was "{0}".\n'.format(image['status']) |
| 168 | |
| 169 | # There's no image but where would I get one?? |
| 170 | elif location is None: |
| 171 | if __opts__['test']: |
| 172 | ret['result'] = None |
| 173 | ret['comment'] = 'No location to copy image from specified,\n' +\ |
| 174 | 'glance.image_present would not create one' |
| 175 | else: |
| 176 | ret['result'] = False |
| 177 | ret['comment'] = 'No location to copy image from specified,\n' +\ |
| 178 | 'not creating a new image.' |
| 179 | return ret |
| 180 | |
| 181 | # If we've created a new image also return its last status: |
| 182 | if name in ret['changes']: |
| 183 | ret['changes'][name]['new']['status'] = image['status'] |
| 184 | |
| 185 | if visibility: |
| 186 | if image['visibility'] != visibility: |
| 187 | old_value = image['visibility'] |
| 188 | if not __opts__['test']: |
| 189 | image = __salt__['glance.image_update']( |
| 190 | id=image['id'], visibility=visibility) |
| 191 | # Check if image_update() worked: |
| 192 | if image['visibility'] != visibility: |
| 193 | if not __opts__['test']: |
| 194 | ret['result'] = False |
| 195 | elif __opts__['test']: |
| 196 | ret['result'] = None |
| 197 | ret['comment'] += '"visibility" is {0}, '\ |
| 198 | 'should be {1}.\n'.format(image['visibility'], |
| 199 | visibility) |
| 200 | else: |
| 201 | if 'new' in ret['changes']: |
| 202 | ret['changes']['new']['visibility'] = visibility |
| 203 | else: |
| 204 | ret['changes']['new'] = {'visibility': visibility} |
| 205 | if 'old' in ret['changes']: |
| 206 | ret['changes']['old']['visibility'] = old_value |
| 207 | else: |
| 208 | ret['changes']['old'] = {'visibility': old_value} |
| 209 | else: |
| 210 | ret['comment'] += '"visibility" is correct ({0}).\n'.format( |
| 211 | visibility) |
| 212 | if protected is not None: |
| 213 | if not isinstance(protected, bool) or image['protected'] ^ protected: |
| 214 | if not __opts__['test']: |
| 215 | ret['result'] = False |
| 216 | else: |
| 217 | ret['result'] = None |
| 218 | ret['comment'] += '"protected" is {0}, should be {1}.\n'.format( |
| 219 | image['protected'], protected) |
| 220 | else: |
| 221 | ret['comment'] += '"protected" is correct ({0}).\n'.format( |
| 222 | protected) |
| 223 | if 'status' in image and checksum: |
| 224 | if image['status'] == 'active': |
| 225 | if 'checksum' not in image: |
| 226 | # Refresh our info about the image |
| 227 | image = __salt__['glance.image_show'](image['id']) |
| 228 | if 'checksum' not in image: |
| 229 | if not __opts__['test']: |
| 230 | ret['result'] = False |
| 231 | else: |
| 232 | ret['result'] = None |
| 233 | ret['comment'] += 'No checksum available for this image:\n' +\ |
| 234 | '\tImage has status "{0}".'.format(image['status']) |
| 235 | elif image['checksum'] != checksum: |
| 236 | if not __opts__['test']: |
| 237 | ret['result'] = False |
| 238 | else: |
| 239 | ret['result'] = None |
| 240 | ret['comment'] += '"checksum" is {0}, should be {1}.\n'.format( |
| 241 | image['checksum'], checksum) |
| 242 | else: |
| 243 | ret['comment'] += '"checksum" is correct ({0}).\n'.format( |
| 244 | checksum) |
| 245 | elif image['status'] in ['saving', 'queued']: |
| 246 | ret['comment'] += 'Checksum won\'t be verified as image ' +\ |
| 247 | 'hasn\'t reached\n\t "status=active" yet.\n' |
| 248 | log.debug('glance.image_present will return: {0}'.format(ret)) |
| 249 | return ret |