Merge "Introduce glance client role"
diff --git a/README.rst b/README.rst
index 420cfa9..0bfdfbf 100644
--- a/README.rst
+++ b/README.rst
@@ -73,6 +73,23 @@
audit:
enabled: false
+Client role
+-----------
+
+Glance images
+
+.. code-block:: yaml
+
+ glance:
+ client:
+ enabled: true
+ server:
+ profile_admin:
+ image:
+ cirros-test:
+ visibility: public
+ protected: false
+ location: http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-i386-disk.img
Client-side RabbitMQ HA setup
diff --git a/glance/_states/glanceng.py b/glance/_states/glanceng.py
new file mode 100644
index 0000000..fb41fb1
--- /dev/null
+++ b/glance/_states/glanceng.py
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+'''
+Managing Images in OpenStack Glance
+===================================
+'''
+# Import python libs
+from __future__ import absolute_import
+import logging
+import time
+
+# Import salt libs
+
+# 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__['glance.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 = 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__['glance.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__['glance.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__['glance.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
\ No newline at end of file
diff --git a/glance/client.sls b/glance/client.sls
new file mode 100644
index 0000000..c3f9213
--- /dev/null
+++ b/glance/client.sls
@@ -0,0 +1,29 @@
+{%- from "glance/map.jinja" import client with context %}
+{%- if client.enabled %}
+
+glance_client_packages:
+ pkg.installed:
+ - names: {{ client.pkgs }}
+
+{%- for identity_name, identity in client.identity.iteritems() %}
+
+{%- for image_name, image in identity.image.iteritems() %}
+
+glance_openstack_image_{{ image_name }}:
+ glanceng.image_present:
+ - name: {{ image_name }}
+ - profile: {{ identity_name }}
+ {%- if image.visibility is defined %}
+ - visibility: {{ image.visibility }}
+ {%- endif %}
+ {%- if image.protected is defined %}
+ - protected: {{ image.protected }}
+ {%- endif %}
+ {%- if image.location is defined %}
+ - location: {{ image.location }}
+ {%- endif %}
+
+{%- endfor %}
+{%- endfor %}
+
+{%- endif %}
\ No newline at end of file
diff --git a/glance/init.sls b/glance/init.sls
index e260848..b325e06 100644
--- a/glance/init.sls
+++ b/glance/init.sls
@@ -3,3 +3,6 @@
{%- if pillar.glance.server.enabled %}
- glance.server
{%- endif %}
+{% if pillar.glance.client is defined %}
+- glance.client
+{% endif %}
\ No newline at end of file
diff --git a/glance/map.jinja b/glance/map.jinja
index a79bade..6e7133c 100644
--- a/glance/map.jinja
+++ b/glance/map.jinja
@@ -17,3 +17,12 @@
}
},
}, merge=pillar.glance.get('server', {})) %}
+
+{% set client = salt['grains.filter_by']({
+ 'Debian': {
+ 'pkgs': ['python-glanceclient']
+ },
+ 'RedHat': {
+ 'pkgs': ['python-glanceclient']
+ },
+}, merge=pillar.glance.get('client', {})) %}
\ No newline at end of file