Merge pull request #25505 from 0xf10e/glance_state_module_boron

Glance state module for 2015.8 "Beryllium"
This commit is contained in:
Thomas S Hatch 2015-07-29 14:55:56 -06:00
commit f06dd05a31
2 changed files with 443 additions and 40 deletions

View File

@ -40,10 +40,23 @@ from __future__ import absolute_import
# Import third party libs
#import salt.ext.six as six
# Import salt libs
from salt.exceptions import (
#CommandExecutionError,
SaltInvocationError
)
from salt.utils import warn_until
from salt.version import (
__version__,
SaltStackVersion
)
# is there not SaltStackVersion.current() to get
# the version of the salt running this code??
CUR_VER = SaltStackVersion(__version__[0], __version__[1])
BORON = SaltStackVersion.from_name('Boron')
# pylint: disable=import-error
HAS_GLANCE = False
try:
@ -77,6 +90,7 @@ def __virtual__():
return 'glance'
return False
__opts__ = {}
@ -143,12 +157,14 @@ def _auth(profile=None, api_version=2, **connection_args):
'get a new one using username and password.')
if HAS_KEYSTONE:
# TODO: redact kwargs['password']
log.debug('Calling keystoneclient.v2_0.client.Client(' +
'{0}, **{1})'.format(endpoint, kwargs))
keystone = kstone.Client(**kwargs)
log.debug(help(keystone.get_token))
kwargs['token'] = keystone.get_token(keystone.session)
# This doesn't realy prevent the password to show up
# in the minion log as keystoneclient.session is
# logging it anyway when in debug-mode
kwargs.pop('password')
log.debug('Calling glanceclient.client.Client(' +
'{0}, {1}, **{2})'.format(api_version, endpoint, kwargs))
@ -158,22 +174,65 @@ def _auth(profile=None, api_version=2, **connection_args):
"Can't retrieve a auth_token without keystone")
def image_create(name, location, profile=None, visibility='public',
container_format='bare', disk_format='raw'):
def _add_image(collection, image):
'''
Add image to given dictionary
'''
image_prep = {
'id': image.id,
'name': image.name,
'created_at': image.created_at,
'file': image.file,
'min_disk': image.min_disk,
'min_ram': image.min_ram,
'owner': image.owner,
'protected': image.protected,
'status': image.status,
'tags': image.tags,
'updated_at': image.updated_at,
'visibility': image.visibility,
}
# Those cause AttributeErrors in Icehouse' glanceclient
for attr in ['container_format', 'disk_format', 'size']:
if attr in image:
image_prep[attr] = image[attr]
if type(collection) is dict:
collection[image.name] = image_prep
elif type(collection) is list:
collection.append(image_prep)
else:
msg = '"collection" is {0}'.format(type(collection)) +\
'instead of dict or list.'
log.error(msg)
raise TypeError(msg)
return collection
def image_create(name, location=None, profile=None, visibility=None,
container_format='bare', disk_format='raw', protected=None,
copy_from=None, is_public=None):
'''
Create an image (glance image-create)
CLI Example:
CLI Example, old format:
.. code-block:: bash
salt '*' glance.image_create name=f16-jeos is_public=true \\
disk_format=qcow2 container_format=ovf \\
copy_from=http://berrange.fedorapeople.org/\
images/2012-02-29/f16-x86_64-openstack-sda.qcow2
CLI Example, new format resembling Glance API v2:
salt '*' glance.image_create name=f16-jeos visibility=public \\
disk_format=qcow2 container_format=ovf \\
copy_from=http://berrange.fedorapeople.org/\
images/2012-02-29/f16-x86_64-openstack-sda.qcow2
For all possible values, run ``glance help image-create`` on the minion.
The parameter 'visibility' defaults to 'public' if neither
'visibility' nor 'is_public' is specified.
'''
kwargs = {}
# valid options for "visibility":
v_list = ['public', 'private']
# valid options for "container_format":
@ -181,20 +240,52 @@ def image_create(name, location, profile=None, visibility='public',
# valid options for "disk_format":
df_list = ['ami', 'ari', 'aki', 'vhd', 'vmdk',
'raw', 'qcow2', 'vdi', 'iso']
if visibility not in v_list:
raise SaltInvocationError('"visibility" needs to be one ' +
'of the following: {0}'.format(', '.join(v_list)))
# 'location' and 'visibility' are the parameters used in
# Glance API v2. For now we have to use v1 for now (see below)
# but this modules interface will change in Boron.
if copy_from is not None or is_public is not None:
warn_until('Boron', 'The parameters \'copy_from\' and '
'\'is_public\' are deprecated and will be removed. '
'Use \'location\' and \'visibility\' instead.')
if is_public is not None and visibility is not None:
raise SaltInvocationError('Must only specify one of '
'\'is_public\' and \'visibility\'')
if copy_from is not None and location is not None:
raise SaltInvocationError('Must only specify one of '
'\'copy_from\' and \'location\'')
if copy_from is not None:
kwargs['copy_from'] = copy_from
else:
kwargs['copy_from'] = location
if is_public is not None:
kwargs['is_public'] = is_public
elif visibility is not None:
if visibility not in v_list:
raise SaltInvocationError('"visibility" needs to be one ' +
'of the following: {0}'.format(', '.join(v_list)))
elif visibility == 'public':
kwargs['is_public'] = True
else:
kwargs['is_public'] = False
else:
kwargs['is_public'] = True
if container_format not in cf_list:
raise SaltInvocationError('"container_format" needs to be ' +
'one of the following: {0}'.format(', '.join(cf_list)))
else:
kwargs['container_format'] = container_format
if disk_format not in df_list:
raise SaltInvocationError('"disk_format" needs to be one ' +
'of the following: {0}'.format(', '.join(df_list)))
else:
kwargs['disk_format'] = disk_format
if protected is not None:
kwargs['protected'] = protected
# Icehouse's glanceclient doesn't have add_location() and
# glanceclient.v2 doesn't implement Client.images.create()
# in a usable fashion. Thus we have to use v1 for now.
g_client = _auth(profile, api_version=1)
image = g_client.images.create(name=name, copy_from=location)
image = g_client.images.create(name=name, **kwargs)
return image_show(image.id)
@ -217,8 +308,15 @@ def image_delete(id=None, name=None, profile=None): # pylint: disable=C0103
id = image.id # pylint: disable=C0103
continue
if not id:
return {'Error': 'Unable to resolve image id'}
g_client.images.delete(id)
return {'Error': 'Unable to resolve '
'image id for name {0}'.format(name)}
try:
g_client.images.delete(id)
except exc.HTTPNotFound:
return {'Error': 'No image with ID {0}'.format(id)}
except exc.HTTPForbidden as forbidden:
log.error(str(forbidden))
return {'Error': str(forbidden)}
ret = 'Deleted image with ID {0}'.format(id)
if name:
ret += ' ({0})'.format(name)
@ -248,18 +346,27 @@ def image_show(id=None, name=None, profile=None): # pylint: disable=C0103
pformat = pprint.PrettyPrinter(indent=4).pformat
log.debug('Properties of image {0}:\n{1}'.format(
image.name, pformat(image)))
# TODO: Get rid of the wrapping dict, see #24568
ret[image.name] = {}
ret_details = {}
# I may want to use this code on Beryllium
# until we got Boron packages for Ubuntu
# so please keep this code until Carbon!
warn_until('Carbon', 'Starting with \'Boron\' image_show() '
'will stop wrapping the returned image in another '
'dictionary.')
if CUR_VER < BORON:
ret[image.name] = ret_details
else:
ret = ret_details
schema = image_schema(profile=profile)
if len(schema.keys()) == 1:
schema = schema['image']
for key in schema.keys():
if key in image:
ret[image.name][key] = image[key]
ret_details[key] = image[key]
return ret
def image_list(id=None, profile=None): # pylint: disable=C0103
def image_list(id=None, profile=None, name=None): # pylint: disable=C0103
'''
Return a list of available images (glance image-list)
@ -270,29 +377,30 @@ def image_list(id=None, profile=None): # pylint: disable=C0103
salt '*' glance.image_list
'''
g_client = _auth(profile)
ret = {}
# TODO: Get rid of the wrapping dict, see #24568
# I may want to use this code on Beryllium
# until we got Boron packages for Ubuntu
# so please keep this code until Carbon!
warn_until('Carbon', 'Starting in \'Boron\' image_list() '
'will return a list of images instead of a dictionary '
'keyed with the images\' names.')
if CUR_VER < BORON:
ret = {}
else:
ret = []
for image in g_client.images.list():
ret[image.name] = {
'id': image.id,
'name': image.name,
'created_at': image.created_at,
'file': image.file,
'min_disk': image.min_disk,
'min_ram': image.min_ram,
'owner': image.owner,
'protected': image.protected,
'status': image.status,
'tags': image.tags,
'updated_at': image.updated_at,
'visibility': image.visibility,
}
# Those cause AttributeErrors in Icehouse' glanceclient
for attr in ['container_format', 'disk_format', 'size']:
if attr in image:
ret[image.name][attr] = image[attr]
if id == image.id:
return ret[image.name]
if id is None and name is None:
_add_image(ret, image)
else:
if id is not None and id == image.id:
_add_image(ret, image)
return ret
if name == image.name:
if name in ret and CUR_VER < BORON:
# Not really worth an exception
return {'Error': 'More than one image '
'with name "{0}"'.format(name)}
_add_image(ret, image)
log.debug('Returning images: {0}'.format(ret))
return ret
@ -304,6 +412,52 @@ def image_schema(profile=None):
return schema_get('image', profile)
def image_update(id=None, name=None, profile=None, **kwargs): # pylint: disable=C0103
'''
Update properties of given image.
Known to work for:
- min_ram (in MB)
- protected (bool)
- visibility ('public' or 'private')
'''
if id:
image = image_show(id=id)
# TODO: This unwrapping should get a warn_until
if len(image) == 1:
image = image.values()[0]
elif name:
img_list = image_list(name=name)
if img_list is not list and 'Error' in img_list:
return img_list
elif len(img_list) == 0:
return {'result': False,
'comment': 'No image with name \'{0}\' '
'found.'.format(name)}
elif len(img_list) == 1:
image = img_list[0]
else:
raise SaltInvocationError
log.debug('Found image:\n{0}'.format(image))
to_update = {}
for key, value in kwargs.items():
if key.startswith('_'):
continue
if key not in image or image[key] != value:
log.debug('add <{0}={1}> to to_update'.format(key, value))
to_update[key] = value
g_client = _auth(profile)
updated = g_client.images.update(image['id'], **to_update)
# I may want to use this code on Beryllium
# until we got Boron packages for Ubuntu
# so please keep this code until Carbon!
warn_until('Carbon', 'Starting with \'Boron\' image_update() '
'will stop wrapping the returned, updated image in '
'another dictionary.')
if CUR_VER < BORON:
updated = {updated.name: updated}
return updated
def schema_get(name, profile=None):
'''
Known valid names of schemas are:
@ -343,11 +497,10 @@ def _item_list(profile=None):
return ret
#The following is a list of functions that need to be incorporated in the
#glance module. This list should be updated as functions are added.
# The following is a list of functions that need to be incorporated in the
# glance module. This list should be updated as functions are added.
# image-download Download a specific image.
# image-update Update a specific image.
# member-create Share a specific image with a tenant.
# member-delete Remove a shared image from a tenant.
# member-list Describe sharing permissions by image or tenant.

250
salt/states/glance.py Normal file
View File

@ -0,0 +1,250 @@
# -*- coding: utf-8 -*-
'''
Managing Images in OpenStack Glance
===================================
'''
# Import python libs
from __future__ import absolute_import
import logging
import time
# Import salt libs
from salt.utils import warn_until
log = logging.getLogger(__name__)
def _find_image(name):
'''
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'
'''
images_dict = __salt__['glance.image_list'](name=name)
log.debug('Got images_dict: {0}'.format(images_dict))
warn_until('Boron', 'Starting with Boron '
'\'glance.image_list\' is not supposed to return '
'the images wrapped in a separate dict anymore.')
if len(images_dict) == 1 and 'images' in images_dict:
images_dict = images_dict['images']
# I /think/ this will still work when glance.image_list
# starts returning a list instead of a dictionary...
if len(images_dict) == 0:
return None, 'No image with name "{0}"'.format(name)
elif len(images_dict) == 1:
return images_dict.values()[0], 'Found image {0}'.format(name)
elif len(images_dict) > 1:
return False, 'Found more than one image with given name'
else:
raise NotImplementedError
def image_present(name, visibility='public', protected=None,
checksum=None, location=None, 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:
- visibility ('public' or 'private')
- protected (bool)
- checksum (string, md5sum)
- location (URL, to copy from)
'''
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)
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__['glance.image_create'](name=name,
protected=protected, visibility=visibility,
location=location)
# See Salt issue #24568
warn_until('Boron', 'Starting with Boron '
'\'glance.image_create\' is not supposed to return '
'the image wrapped in a dict anymore.')
if len(image.keys()) == 1:
image = image.values()[0]
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)
if not image:
ret['result'] = False
ret['comment'] += 'Created image {0} '.format(
name) + ' vanished:\n' + msg
return ret
elif len(image.keys()) == 1:
# See Salt issue #24568
warn_until('Boron', 'Starting with Boron '
'\'_find_image()\' is not supposed to return '
'the image wrapped in a dict anymore.')
image = image.values()[0]
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'])
# See Salt issue #24568
warn_until('Boron', 'Starting with Boron '
'\'_find_image()\' is not supposed to return '
'the image wrapped in a dict anymore.')
if len(image.keys()) == 1:
image = image.values()[0]
# ret[comment] +=
# 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__['glance.image_update'](
id=image['id'], visibility=visibility)
# See Salt issue #24568
warn_until('Boron', 'Starting with Boron '
'\'glance.image_update\' is not supposed to return '
'the image wrapped in a dict anymore.')
if len(image.keys()) == 1:
image = image.values()[0]
# 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__['glance.image_show'](image['id'])
warn_until('Boron', 'Starting with Boron '
'\'glance.image_show\' is not supposed to return '
'the image wrapped in a dict anymore.')
if len(image.keys()) == 1:
image = image.values()[0]
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