blob: d607fef41acfc96bb9fdece50f52f4356c3d7bca [file] [log] [blame]
# -*- 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
'''
endpoint_type = str(connection_args.get('connection_endpoint_type',
'internal'))
kstone = __salt__['keystoneng.auth'](profile, **connection_args)
g_endpoint = __salt__['keystoneng.endpoint_get']('glance',
profile=profile,
interface=endpoint_type)
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