|  | # -*- coding: utf-8 -*- | 
|  | """ | 
|  | Module extending the salt.modules.glance modules. | 
|  |  | 
|  | This module adds functionality for managing Glance V2 tasks by exposing the | 
|  | following functions: | 
|  | - task_create | 
|  | - task_show | 
|  | - task_list | 
|  |  | 
|  | :optdepends:    - glanceclient Python adapter | 
|  | :configuration: This module is not usable until the following are specified | 
|  | either in a pillar or in the minion's config file:: | 
|  |  | 
|  | keystone.user: admin | 
|  | keystone.password: verybadpass | 
|  | keystone.tenant: admin | 
|  | keystone.insecure: False   #(optional) | 
|  | keystone.auth_url: 'http://127.0.0.1:5000/v2.0/' | 
|  |  | 
|  | If configuration for multiple openstack accounts is required, they can be | 
|  | set up as different configuration profiles: | 
|  | For example:: | 
|  |  | 
|  | openstack1: | 
|  | keystone.user: admin | 
|  | keystone.password: verybadpass | 
|  | keystone.tenant: admin | 
|  | keystone.auth_url: 'http://127.0.0.1:5000/v2.0/' | 
|  |  | 
|  | openstack2: | 
|  | keystone.user: admin | 
|  | keystone.password: verybadpass | 
|  | keystone.tenant: admin | 
|  | keystone.auth_url: 'http://127.0.0.2:5000/v2.0/' | 
|  |  | 
|  | With this configuration in place, any of the glance functions can | 
|  | make use of a configuration profile by declaring it explicitly. | 
|  | For example:: | 
|  |  | 
|  | salt '*' glance.image_list profile=openstack1 | 
|  | """ | 
|  |  | 
|  | # Import Python libs | 
|  | from __future__ import absolute_import | 
|  | import logging | 
|  | import pprint | 
|  | import re | 
|  |  | 
|  | # Import salt libs | 
|  | from salt.exceptions import SaltInvocationError | 
|  |  | 
|  | from salt.version import ( | 
|  | __version__, | 
|  | SaltStackVersion | 
|  | ) | 
|  |  | 
|  | from salt.utils import warn_until | 
|  |  | 
|  | # pylint: disable=import-error | 
|  | HAS_GLANCE = False | 
|  | try: | 
|  | from glanceclient import client | 
|  | from glanceclient import exc | 
|  | HAS_GLANCE = True | 
|  | except ImportError: | 
|  | pass | 
|  |  | 
|  |  | 
|  | logging.basicConfig(level=logging.DEBUG) | 
|  | log = logging.getLogger(__name__) | 
|  |  | 
|  |  | 
|  | def __virtual__(): | 
|  | ''' | 
|  | Only load this module if glance | 
|  | is installed on this minion. | 
|  | ''' | 
|  | if not HAS_GLANCE: | 
|  | return False, ("The glance execution module cannot be loaded: " | 
|  | "the glanceclient python library is not available.") | 
|  | return True | 
|  |  | 
|  |  | 
|  | __opts__ = {} | 
|  |  | 
|  |  | 
|  | def _auth(profile=None, api_version=2, **connection_args): | 
|  | ''' | 
|  | Set up glance credentials, returns | 
|  | `glanceclient.client.Client`. Optional parameter | 
|  | "api_version" defaults to 2. | 
|  |  | 
|  | Only intended to be used within glance-enabled modules | 
|  | ''' | 
|  |  | 
|  | kstone = __salt__['keystoneng.auth'](profile, **connection_args) | 
|  | g_endpoint = __salt__['keystoneng.endpoint_get']('glance', profile=profile) | 
|  | glance_client = client.Client(api_version, session=kstone.session, endpoint=g_endpoint.get('url')) | 
|  | return glance_client | 
|  |  | 
|  |  | 
|  | def _validate_image_params(visibility=None, container_format='bare', | 
|  | disk_format='raw', tags=None, **kwargs): | 
|  | # valid options for "visibility": | 
|  | v_list = ['public', 'private', 'shared', 'community'] | 
|  | # valid options for "container_format": | 
|  | cf_list = ['ami', 'ari', 'aki', 'bare', 'ovf'] | 
|  | # valid options for "disk_format": | 
|  | df_list = ['ami', 'ari', 'aki', 'vhd', 'vmdk', | 
|  | 'raw', 'qcow2', 'vdi', 'iso'] | 
|  |  | 
|  | if 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))) | 
|  | if container_format not in cf_list: | 
|  | raise SaltInvocationError('"container_format" needs to be ' + | 
|  | 'one of the following: {0}'.format( | 
|  | ', '.join(cf_list))) | 
|  | if disk_format not in df_list: | 
|  | raise SaltInvocationError('"disk_format" needs to be one ' + | 
|  | 'of the following: {0}'.format( | 
|  | ', '.join(df_list))) | 
|  | if tags: | 
|  | if not isinstance(tags, list): | 
|  | raise SaltInvocationError('Incorrect input type for the {0} ' | 
|  | 'parameter: expected: {1}, ' | 
|  | 'got {2}'.format("tags", list, | 
|  | type(tags))) | 
|  |  | 
|  |  | 
|  | def _validate_task_params(task_type, input_params): | 
|  | # Only import tasks are currently supported | 
|  | # TODO(eezhova): Add support for "export" and "clone" task types | 
|  | valid_task_types = ["import", ] | 
|  |  | 
|  | import_required_params = {"import_from", "import_from_format", | 
|  | "image_properties"} | 
|  |  | 
|  | if task_type not in valid_task_types: | 
|  | raise SaltInvocationError("'task_type' must be one of the following: " | 
|  | "{0}".format(', '.join(valid_task_types))) | 
|  |  | 
|  | if task_type == "import": | 
|  | valid_import_from_formats = ['ami', 'ari', 'aki', 'vhd', 'vmdk', | 
|  | 'raw', 'qcow2', 'vdi', 'iso'] | 
|  | missing_params = import_required_params - set(input_params.keys()) | 
|  | if missing_params: | 
|  | raise SaltInvocationError( | 
|  | "Missing the following task parameters for the 'import' task: " | 
|  | "{0}".format(', '.join(missing_params))) | 
|  |  | 
|  | import_from = input_params['import_from'] | 
|  | import_from_format = input_params['import_from_format'] | 
|  | image_properties = input_params['image_properties'] | 
|  | if not import_from.startswith(('http://', 'https://')): | 
|  | raise SaltInvocationError("Only non-local sources of image data " | 
|  | "are supported.") | 
|  | if import_from_format not in valid_import_from_formats: | 
|  | raise SaltInvocationError( | 
|  | "'import_from_format' needs to be one of the following: " | 
|  | "{0}".format(', '.join(valid_import_from_formats))) | 
|  | _validate_image_params(**image_properties) | 
|  |  | 
|  |  | 
|  | def task_create(task_type, profile=None, input_params=None): | 
|  | """ | 
|  | Create a Glance V2 task of a given type | 
|  |  | 
|  | :param task_type: Task type | 
|  | :param profile: Authentication profile | 
|  | :param input_params: Dictionary with input parameters for a task | 
|  | :return: Dictionary with created task's parameters | 
|  | """ | 
|  | g_client = _auth(profile, api_version=2) | 
|  | log.debug( | 
|  | 'Task type: {}\nInput params: {}'.format(task_type, input_params) | 
|  | ) | 
|  | task = g_client.tasks.create(type=task_type, input=input_params) | 
|  | log.debug("Created task: {}".format(dict(task))) | 
|  | created_task = task_show(task.id, profile=profile) | 
|  | return created_task | 
|  |  | 
|  |  | 
|  | def task_show(task_id, profile=None): | 
|  | """ | 
|  | Show a Glance V2 task | 
|  |  | 
|  | :param task_id: ID of a task to show | 
|  | :param profile: Authentication profile | 
|  | :return: Dictionary with created task's parameters | 
|  | """ | 
|  | g_client = _auth(profile) | 
|  | ret = {} | 
|  | try: | 
|  | task = g_client.tasks.get(task_id) | 
|  | except exc.HTTPNotFound: | 
|  | return { | 
|  | 'result': False, | 
|  | 'comment': 'No task with ID {0}'.format(task_id) | 
|  | } | 
|  | pformat = pprint.PrettyPrinter(indent=4).pformat | 
|  | log.debug('Properties of task {0}:\n{1}'.format( | 
|  | task_id, pformat(task))) | 
|  |  | 
|  | schema = image_schema(schema_type='task', profile=profile) | 
|  | if len(schema.keys()) == 1: | 
|  | schema = schema['task'] | 
|  | for key in schema.keys(): | 
|  | if key in task: | 
|  | ret[key] = task[key] | 
|  | return ret | 
|  |  | 
|  |  | 
|  | def task_list(profile=None): | 
|  | """ | 
|  | List Glance V2 tasks | 
|  |  | 
|  | :param profile: Authentication profile | 
|  | :return: Dictionary with existing tasks | 
|  | """ | 
|  | g_client = _auth(profile) | 
|  | ret = {} | 
|  | tasks = g_client.tasks.list() | 
|  | schema = image_schema(schema_type='task', profile=profile) | 
|  | if len(schema.keys()) == 1: | 
|  | schema = schema['task'] | 
|  | for task in tasks: | 
|  | task_dict = {} | 
|  | for key in schema.keys(): | 
|  | if key in task: | 
|  | task_dict[key] = task[key] | 
|  | ret[task['id']] = task_dict | 
|  | return ret | 
|  |  | 
|  |  | 
|  | def get_image_owner_id(name, profile=None): | 
|  | """ | 
|  | Mine function to get image owner | 
|  |  | 
|  | :param name: Name of the image | 
|  | :param profile: Authentication profile | 
|  | :return: Image owner ID or [] if image is not found | 
|  | """ | 
|  | g_client = _auth(profile) | 
|  | image_id = None | 
|  | for image in g_client.images.list(): | 
|  | if image.name == name: | 
|  | image_id = image.id | 
|  | continue | 
|  | if not image_id: | 
|  | return [] | 
|  | try: | 
|  | image = g_client.images.get(image_id) | 
|  | except exc.HTTPNotFound: | 
|  | return [] | 
|  | return image['owner'] | 
|  |  | 
|  |  | 
|  | def image_schema(schema_type='image', profile=None): | 
|  | ''' | 
|  | Returns names and descriptions of the schema "image"'s | 
|  | properties for this profile's instance of glance | 
|  |  | 
|  | CLI Example: | 
|  |  | 
|  | .. code-block:: bash | 
|  |  | 
|  | salt '*' glance.image_schema | 
|  | ''' | 
|  | return schema_get(schema_type, profile) | 
|  |  | 
|  |  | 
|  | def schema_get(name, profile=None): | 
|  | ''' | 
|  | Known valid names of schemas are: | 
|  | - image | 
|  | - images | 
|  | - member | 
|  | - members | 
|  |  | 
|  | CLI Example: | 
|  |  | 
|  | .. code-block:: bash | 
|  |  | 
|  | salt '*' glance.schema_get name=f16-jeos | 
|  | ''' | 
|  | g_client = _auth(profile) | 
|  | pformat = pprint.PrettyPrinter(indent=4).pformat | 
|  | schema_props = {} | 
|  | for prop in g_client.schemas.get(name).properties: | 
|  | schema_props[prop.name] = prop.description | 
|  | log.debug('Properties of schema {0}:\n{1}'.format( | 
|  | name, pformat(schema_props))) | 
|  | return {name: schema_props} | 
|  |  | 
|  | def image_list(id=None, profile=None, name=None):  # pylint: disable=C0103 | 
|  | ''' | 
|  | Return a list of available images (glance image-list) | 
|  |  | 
|  | CLI Example: | 
|  |  | 
|  | .. code-block:: bash | 
|  |  | 
|  | salt '*' glance.image_list | 
|  | ''' | 
|  |  | 
|  | g_client = _auth(profile) | 
|  | ret = [] | 
|  | for image in g_client.images.list(): | 
|  | 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: | 
|  | # Not really worth an exception | 
|  | return { | 
|  | 'result': False, | 
|  | 'comment': | 
|  | 'More than one image with ' | 
|  | 'name "{0}"'.format(name) | 
|  | } | 
|  | _add_image(ret, image) | 
|  | log.debug('Returning images: {0}'.format(ret)) | 
|  | return ret | 
|  |  | 
|  | 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, 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: | 
|  |  | 
|  | .. code-block:: bash | 
|  |  | 
|  | 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 | 
|  |  | 
|  | 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": | 
|  | cf_list = ['ami', 'ari', 'aki', 'bare', 'ovf'] | 
|  | # valid options for "disk_format": | 
|  | df_list = ['ami', 'ari', 'aki', 'vhd', 'vmdk', | 
|  | 'raw', 'qcow2', 'vdi', 'iso'] | 
|  | # '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 Carbon. | 
|  | if copy_from is not None or is_public is not None: | 
|  | warn_until('Carbon', '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, **kwargs) | 
|  | return image_show(image.id, profile=profile) | 
|  |  | 
|  | def image_delete(id=None, name=None, profile=None):  # pylint: disable=C0103 | 
|  | ''' | 
|  | Delete an image (glance image-delete) | 
|  |  | 
|  | CLI Examples: | 
|  |  | 
|  | .. code-block:: bash | 
|  |  | 
|  | salt '*' glance.image_delete c2eb2eb0-53e1-4a80-b990-8ec887eae7df | 
|  | salt '*' glance.image_delete id=c2eb2eb0-53e1-4a80-b990-8ec887eae7df | 
|  | salt '*' glance.image_delete name=f16-jeos | 
|  | ''' | 
|  | g_client = _auth(profile) | 
|  | image = {'id': False, 'name': None} | 
|  | if name: | 
|  | for image in g_client.images.list(): | 
|  | if image.name == name: | 
|  | id = image.id  # pylint: disable=C0103 | 
|  | continue | 
|  | if not id: | 
|  | return { | 
|  | 'result': False, | 
|  | 'comment': | 
|  | 'Unable to resolve image id ' | 
|  | 'for name {0}'.format(name) | 
|  | } | 
|  | elif not name: | 
|  | name = image['name'] | 
|  | try: | 
|  | g_client.images.delete(id) | 
|  | except exc.HTTPNotFound: | 
|  | return { | 
|  | 'result': False, | 
|  | 'comment': 'No image with ID {0}'.format(id) | 
|  | } | 
|  | except exc.HTTPForbidden as forbidden: | 
|  | log.error(str(forbidden)) | 
|  | return { | 
|  | 'result': False, | 
|  | 'comment': str(forbidden) | 
|  | } | 
|  | return { | 
|  | 'result': True, | 
|  | 'comment': 'Deleted image \'{0}\' ({1}).'.format(name, id), | 
|  | } | 
|  |  | 
|  | def image_show(id=None, name=None, profile=None):  # pylint: disable=C0103 | 
|  | ''' | 
|  | Return details about a specific image (glance image-show) | 
|  |  | 
|  | CLI Example: | 
|  |  | 
|  | .. code-block:: bash | 
|  |  | 
|  | salt '*' glance.image_show | 
|  | ''' | 
|  | g_client = _auth(profile) | 
|  | ret = {} | 
|  | if name: | 
|  | for image in g_client.images.list(): | 
|  | if image.name == name: | 
|  | id = image.id  # pylint: disable=C0103 | 
|  | continue | 
|  | if not id: | 
|  | return { | 
|  | 'result': False, | 
|  | 'comment': | 
|  | 'Unable to resolve image ID ' | 
|  | 'for name \'{0}\''.format(name) | 
|  | } | 
|  | try: | 
|  | image = g_client.images.get(id) | 
|  | except exc.HTTPNotFound: | 
|  | return { | 
|  | 'result': False, | 
|  | 'comment': 'No image with ID {0}'.format(id) | 
|  | } | 
|  | pformat = pprint.PrettyPrinter(indent=4).pformat | 
|  | log.debug('Properties of image {0}:\n{1}'.format( | 
|  | image.name, pformat(image))) | 
|  | schema = image_schema(profile=profile) | 
|  | if len(schema.keys()) == 1: | 
|  | schema = schema['image'] | 
|  | for key in schema.keys(): | 
|  | if key in image: | 
|  | ret[key] = image[key] | 
|  | return ret | 
|  |  | 
|  | 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') | 
|  |  | 
|  | CLI Example: | 
|  |  | 
|  | .. code-block:: bash | 
|  |  | 
|  | salt '*' glance.image_update id=c2eb2eb0-53e1-4a80-b990-8ec887eae7df | 
|  | salt '*' glance.image_update name=f16-jeos | 
|  | ''' | 
|  | if id: | 
|  | image = image_show(id=id, profile=profile) | 
|  | if 'result' in image and not image['result']: | 
|  | return image | 
|  | elif len(image) == 1: | 
|  | image = image.values()[0] | 
|  | elif name: | 
|  | img_list = image_list(name=name, profile=profile) | 
|  | if img_list is dict and 'result' 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: | 
|  | try: | 
|  | image = img_list[0] | 
|  | except KeyError: | 
|  | image = img_list[name] | 
|  | 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) | 
|  | return updated | 
|  |  | 
|  | def _item_list(profile=None): | 
|  | ''' | 
|  | Template for writing list functions | 
|  | Return a list of available items (glance items-list) | 
|  |  | 
|  | CLI Example: | 
|  |  | 
|  | .. code-block:: bash | 
|  |  | 
|  | salt '*' glance.item_list | 
|  | ''' | 
|  | g_client = _auth(profile) | 
|  | ret = [] | 
|  | for item in g_client.items.list(): | 
|  | ret.append(item.__dict__) | 
|  | #ret[item.name] = { | 
|  | #        'name': item.name, | 
|  | #    } | 
|  | return ret |