From 177e305693f19725f80c4563b5b7f893ad4bffce Mon Sep 17 00:00:00 2001 From: Jerome Pansanel Date: Thu, 29 Jun 2017 08:05:22 +0200 Subject: [PATCH 1/5] Error management This commit contains several enhancement in the management of errors and exceptions. --- cloudkeeper_os/imagemanager.py | 57 +++++++++++++++++++++++++--------- cloudkeeper_os/server.py | 10 ++++-- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/cloudkeeper_os/imagemanager.py b/cloudkeeper_os/imagemanager.py index a6dd8f2..95926a1 100644 --- a/cloudkeeper_os/imagemanager.py +++ b/cloudkeeper_os/imagemanager.py @@ -23,6 +23,7 @@ import glanceclient.v2.client as glanceclient from oslo_config import cfg from oslo_log import log +import webob.exc from cloudkeeper_os import cloudkeeper_pb2 from cloudkeeper_os import keystone_client @@ -44,15 +45,21 @@ def __init__(self): self.mapping = mapping.Mapping() def add_appliance(self, appliance): - """Add an appliance + """Add an appliance to glance """ project_name = self.mapping.get_project_from_vo(appliance.vo) if not project_name: - # TODO Add a project exception LOG.error('No such VO: ' + appliance.vo) - LOG.debug("Get session for projet: %s" % project_name) - sess = keystone_client.get_session(project_name=project_name) - glance = glanceclient.Client(session=sess) + return None + LOG.debug("Get session for project: %s" % project_name) + try: + sess = keystone_client.get_session(project_name=project_name) + glance = glanceclient.Client(session=sess) + # TODO add a glance session test + except webob.exc.HTTPForbidden as err: + LOG.error("Connection to Glance failed.") + LOG.exception(err) + return None LOG.info('Adding appliance: ' + appliance.title) LOG.debug("Image access mode: " "%s" % appliance.image.Mode.Name(appliance.image.mode)) @@ -64,13 +71,22 @@ def add_appliance(self, appliance): kwargs['filename'] = filename kwargs['username'] = appliance.image.username kwargs['password'] = appliance.image.password - if not utils.retrieve_image(**kwargs): + try: + utils.retrieve_image(**kwargs) + except Exception as err: + LOG.error("Failed to download image from Cloudkeeper.") + LOG.exception(err) return None else: filename = appliance.image.location image_format = appliance.image.Format.Name(appliance.image.format) appliance.ClearField('image') - image_data = open(filename, 'rb') + try: + image_data = open(filename, 'rb') + except IOError as err: + LOG.error("Can not open image file: %s" % filename) + LOG.exception(err) + return None properties = {} for (descriptor, value) in appliance.ListFields(): if descriptor.name == 'identifier': @@ -100,7 +116,7 @@ def add_appliance(self, appliance): return glance_image.id def update_appliance(self, appliance): - """Update properties of an appliance + """Update an appliance stored in glance """ project_name = self.mapping.get_project_from_vo(appliance.vo) sess = keystone_client.get_session(project_name=project_name) @@ -145,9 +161,10 @@ def update_appliance(self, appliance): LOG.debug("Appliance properties updated with new " "values: %s" % (properties)) glance.images.update(image_list[0]['id'], **properties) + return image_list[0]['id'] def remove_appliance(self, appliance): - """Remove an appliance + """Remove an appliance in glance """ project_name = self.mapping.get_project_from_vo(appliance.vo) sess = keystone_client.get_session(project_name=project_name) @@ -157,21 +174,24 @@ def remove_appliance(self, appliance): kwargs = {'filters': filters} img_generator = glance.images.list(**kwargs) image_list = list(img_generator) - if len(image_list) == 1: - LOG.info('Deleting image: %s' % image_list[0]['id']) - glance.images.delete(image_list[0]['id']) - elif len(image_list) > 1: + if len(image_list) > 1: LOG.error("Multiple images found with the same properties " "(%s: %s, %s: %s)" % (IMAGE_ID_TAG, appliance.identifier, IMAGE_LIST_ID_TAG, appliance.image_list_identifier)) - else: + return None + elif len(image_list) == 0: LOG.error("No image found with the following properties " "(%s: %s, %s: %s)" % (IMAGE_ID_TAG, appliance.identifier, IMAGE_LIST_ID_TAG, appliance.image_list_identifier)) + return None + else: + LOG.info('Deleting image: %s' % image_list[0]['id']) + glance.images.delete(image_list[0]['id']) + return image_list[0]['id'] class ImageListManager(object): @@ -241,14 +261,20 @@ def remove_image_list(self, image_list_identifier): self.update_image_list_identifiers() if image_list_identifier not in self.appliances: # raise NotIdentifierFound exception - LOG.error("No image with the image_list_identifier: %s" % image_list_identifier) + LOG.error("No image with the image_list_identifier:" + "%s" % image_list_identifier) + return None vo_name = self.appliances[image_list_identifier][0]['APPLIANCE_VO'] project_name = self.mapping.get_project_from_vo(vo_name) sess = keystone_client.get_session(project_name) glance = glanceclient.Client(session=sess) + LOG.debug("Delete all images with the Image List Identifier: " + "%s" % image_list_identifier) for image in self.appliances[image_list_identifier]: + LOG.info("Deleting image %s" % image['id']) glance.images.delete(image['id']) self.appliances.pop(image_list_identifier) + return image_list_identifier def get_image_list_identifiers(self): """Return a list of identifiers @@ -256,6 +282,7 @@ def get_image_list_identifiers(self): self.update_image_list_identifiers() image_list_identifiers = [] for identifier in self.appliances: + LOG.debug("Append new image list identifier: %s" % identifier) image_list_identifiers.append( cloudkeeper_pb2.ImageListIdentifier( image_list_identifier=identifier diff --git a/cloudkeeper_os/server.py b/cloudkeeper_os/server.py index 49d9636..ee0047f 100644 --- a/cloudkeeper_os/server.py +++ b/cloudkeeper_os/server.py @@ -83,7 +83,10 @@ def UpdateAppliance(self, request, context): ) LOG.info("updating appliance: %s" % request.identifier) manager = imagemanager.ApplianceManager() - manager.update_appliance(request) + if not manager.update_appliance(request): + metadata = ( + ('status', 'ERROR'), + ) LOG.debug("Sending metadata information ('%s': '%s')" % metadata[0]) context.set_trailing_metadata(metadata) return cloudkeeper_pb2.Empty() @@ -97,7 +100,10 @@ def RemoveAppliance(self, request, context): ) LOG.info("Removing appliance: %s" % request.identifier) manager = imagemanager.ApplianceManager() - manager.remove_appliance(request) + if not manager.remove_appliance(request): + metadata = ( + ('status', 'ERROR'), + ) LOG.debug("Sending metadata information ('%s': '%s')" % metadata[0]) context.set_trailing_metadata(metadata) return cloudkeeper_pb2.Empty() From 264cc134a2fbefee7a722eb5b78dae1b5d14f319 Mon Sep 17 00:00:00 2001 From: Jerome Pansanel Date: Thu, 29 Jun 2017 09:10:46 +0200 Subject: [PATCH 2/5] Add rule to disable pylint check against dynamically generated members --- pylintrc | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 pylintrc diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..60aed2a --- /dev/null +++ b/pylintrc @@ -0,0 +1,4 @@ +[Typecheck] +# Disable warnings on the cloudkeeper_pb2 classes because pylint doesn't +# support importing from cloudkeeper_pb2 yet. +ignored-modules = cloudkeeper_os.cloudkeeper_pb2 From 9118ea5290601a67afd1adaad6a240121194b495 Mon Sep 17 00:00:00 2001 From: Jerome Pansanel Date: Tue, 18 Jul 2017 07:25:50 +0200 Subject: [PATCH 3/5] Add missing LOG string when error occurs --- cloudkeeper_os/mapping.py | 2 ++ cloudkeeper_os/{keystone_client.py => openstack_client.py} | 0 2 files changed, 2 insertions(+) rename cloudkeeper_os/{keystone_client.py => openstack_client.py} (100%) diff --git a/cloudkeeper_os/mapping.py b/cloudkeeper_os/mapping.py index eeb0f55..db61a17 100644 --- a/cloudkeeper_os/mapping.py +++ b/cloudkeeper_os/mapping.py @@ -46,6 +46,7 @@ def get_project_from_vo(self, vo_name): if vo_name in self.vo_mapping: return self.vo_mapping[vo_name] else: + LOG.error("No such VO '%s' in the mapping file." % (vo_name)) return None def get_vo_from_project(self, project): @@ -54,6 +55,7 @@ def get_vo_from_project(self, project): if project in self.project_mapping: return self.project_mapping[project] else: + LOG.error("No such project '%s' in the mapping file." % (project)) return None def get_vos(self): diff --git a/cloudkeeper_os/keystone_client.py b/cloudkeeper_os/openstack_client.py similarity index 100% rename from cloudkeeper_os/keystone_client.py rename to cloudkeeper_os/openstack_client.py From 975be6b70a40828dadbdda1a92526ca2415009d1 Mon Sep 17 00:00:00 2001 From: Jerome Pansanel Date: Tue, 18 Jul 2017 07:26:20 +0200 Subject: [PATCH 4/5] Add constant definitions --- cloudkeeper_os/constants.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 cloudkeeper_os/constants.py diff --git a/cloudkeeper_os/constants.py b/cloudkeeper_os/constants.py new file mode 100644 index 0000000..c42ba41 --- /dev/null +++ b/cloudkeeper_os/constants.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +# Copyright 2017 CNRS and University of Strasbourg +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Constants +""" + +APPLIANCE_INT_VALUES = ['ram', 'core', 'expiration_date'] +IMAGE_ID_TAG = 'APPLIANCE_ID' +IMAGE_LIST_ID_TAG = 'APPLIANCE_IMAGE_LIST_ID' From fa4af68b0221eeecd70ce7d8dd179a8a24026982 Mon Sep 17 00:00:00 2001 From: Jerome Pansanel Date: Tue, 18 Jul 2017 11:30:01 +0200 Subject: [PATCH 5/5] Improvement of the robustness of the code This commit contains the following changes: * All OpenStack client are now managed by a dedicated module * Several functions have been moved to the utils module * Additional log messages have been added The following issues have been fixed: * Image data is now correctly updated * Manage correctly some errors --- cloudkeeper_os/imagemanager.py | 226 +++++++++++++---------------- cloudkeeper_os/openstack_client.py | 17 +++ cloudkeeper_os/utils.py | 68 ++++++++- 3 files changed, 182 insertions(+), 129 deletions(-) diff --git a/cloudkeeper_os/imagemanager.py b/cloudkeeper_os/imagemanager.py index 95926a1..5cfc21d 100644 --- a/cloudkeeper_os/imagemanager.py +++ b/cloudkeeper_os/imagemanager.py @@ -18,24 +18,21 @@ """ import json -import uuid -import glanceclient.v2.client as glanceclient from oslo_config import cfg from oslo_log import log -import webob.exc from cloudkeeper_os import cloudkeeper_pb2 -from cloudkeeper_os import keystone_client +from cloudkeeper_os import constants +from cloudkeeper_os import openstack_client from cloudkeeper_os import mapping from cloudkeeper_os import utils CONF = cfg.CONF LOG = log.getLogger(__name__) - -APPLIANCE_INT_VALUES = ['ram', 'core', 'expiration_date'] -IMAGE_ID_TAG = 'APPLIANCE_ID' -IMAGE_LIST_ID_TAG = 'APPLIANCE_IMAGE_LIST_ID' +IMAGE_ID_TAG = constants.IMAGE_ID_TAG +IMAGE_LIST_ID_TAG = constants.IMAGE_LIST_ID_TAG +APPLIANCE_INT_VALUES = constants.APPLIANCE_INT_VALUES class ApplianceManager(object): @@ -44,69 +41,48 @@ class ApplianceManager(object): def __init__(self): self.mapping = mapping.Mapping() + def add_appliance(self, appliance): """Add an appliance to glance """ project_name = self.mapping.get_project_from_vo(appliance.vo) if not project_name: - LOG.error('No such VO: ' + appliance.vo) + LOG.debug("Cannot get project name from vo %s" % appliance.vo) return None - LOG.debug("Get session for project: %s" % project_name) - try: - sess = keystone_client.get_session(project_name=project_name) - glance = glanceclient.Client(session=sess) - # TODO add a glance session test - except webob.exc.HTTPForbidden as err: - LOG.error("Connection to Glance failed.") - LOG.exception(err) + + glance = openstack_client.get_glance_client(project_name) + if not glance: + LOG.error("Cannot get glance client for project %s" % project_name) return None + LOG.info('Adding appliance: ' + appliance.title) + LOG.debug("Image access mode: " "%s" % appliance.image.Mode.Name(appliance.image.mode)) if appliance.image.Mode.Name(appliance.image.mode) == 'REMOTE': - LOG.debug("Downloading image from Cloudkeeper") - filename = CONF.tempdir + '/' + str(uuid.uuid4()) - kwargs = {} - kwargs['uri'] = appliance.image.location - kwargs['filename'] = filename - kwargs['username'] = appliance.image.username - kwargs['password'] = appliance.image.password - try: - utils.retrieve_image(**kwargs) - except Exception as err: - LOG.error("Failed to download image from Cloudkeeper.") - LOG.exception(err) - return None + filename = utils.retrieve_image(appliance) else: filename = appliance.image.location + if not filename: + LOG.error("Image filename is not set.") + return None image_format = appliance.image.Format.Name(appliance.image.format) - appliance.ClearField('image') try: image_data = open(filename, 'rb') except IOError as err: LOG.error("Can not open image file: %s" % filename) LOG.exception(err) return None - properties = {} - for (descriptor, value) in appliance.ListFields(): - if descriptor.name == 'identifier': - key = IMAGE_ID_TAG - elif descriptor.name == 'image_list_identifier': - key = IMAGE_LIST_ID_TAG - else: - if descriptor.name == 'attributes': - data = dict(value) - value = json.dumps(data) - LOG.debug("attribute value: %s" % value) - key = 'APPLIANCE_' + str.upper(descriptor.name) - properties[key] = str(value) - # Add property for cloud-info-provider compatibility - properties['vmcatcher_event_ad_mpuri'] = appliance.mpuri + appliance.ClearField('image') + + properties = utils.extract_appliance_properties(appliance) + LOG.debug("Create image %s (format: %s, " "properties %s)" % (appliance.title, str.lower(image_format), properties) ) + glance_image = glance.images.create(name=appliance.title, disk_format=str.lower(image_format), container_format="bare" @@ -119,79 +95,71 @@ def update_appliance(self, appliance): """Update an appliance stored in glance """ project_name = self.mapping.get_project_from_vo(appliance.vo) - sess = keystone_client.get_session(project_name=project_name) - glance = glanceclient.Client(session=sess) - filters = {IMAGE_ID_TAG: appliance.identifier, - IMAGE_LIST_ID_TAG: appliance.image_list_identifier} - kwargs = {'filters': filters} - img_generator = glance.images.list(**kwargs) - if appliance.HasField('image'): - appliance.ClearField('image') - image_list = list(img_generator) - # TODO Deal the case where a property has been removed - if len(image_list) > 1: - LOG.error("Multiple images found with the same properties " - "(%s: %s, %s: %s)" % (IMAGE_ID_TAG, - appliance.identifier, - IMAGE_LIST_ID_TAG, - appliance.image_list_identifier)) + if not project_name: + LOG.debug("Cannot get project name from vo %s" % appliance.vo) + return None + + glance = openstack_client.get_glance_client(project_name) + if not glance: + LOG.error("Cannot get glance client for project %s" % project_name) + return None + + glance_image = utils.find_image(glance, appliance.identifier, + appliance.image_list_identifier) + if not glance_image: + LOG.info('Cannot delete image: image not found') + return None + + LOG.debug("Image access mode: " + "%s" % appliance.image.Mode.Name(appliance.image.mode)) + if appliance.image.Mode.Name(appliance.image.mode) == 'REMOTE': + filename = utils.retrieve_image(appliance) + else: + filename = appliance.image.location + if not filename: + LOG.error("Image filename is not set.") return None - elif len(image_list) == 0: - LOG.error("No image found with the following properties " - "(%s: %s, %s: %s)" % (IMAGE_ID_TAG, - appliance.identifier, - IMAGE_LIST_ID_TAG, - appliance.image_list_identifier)) + image_format = appliance.image.Format.Name(appliance.image.format) + try: + image_data = open(filename, 'rb') + except IOError as err: + LOG.error("Can not open image file: %s" % filename) + LOG.exception(err) return None - properties = {} - LOG.info('Updating image: %s' % image_list[0]['id']) - for (descriptor, value) in appliance.ListFields(): - if descriptor.name == 'identifier': - key = IMAGE_ID_TAG - elif descriptor.name == 'image_list_identifier': - key = IMAGE_LIST_ID_TAG - else: - if descriptor.name == 'attributes': - data = dict(value) - value = json.dumps(data) - key = 'APPLIANCE_' + str.upper(descriptor.name) - properties[key] = str(value) - # Add property for cloud-info-provider compatibility - properties['vmcatcher_event_ad_mpuri'] = appliance.mpuri + appliance.ClearField('image') + + properties = utils.extract_appliance_properties(appliance) + properties['disk_format'] = str.lower(image_format) + + LOG.info('Updating image: %s' % glance_image.id) LOG.debug("Appliance properties updated with new " "values: %s" % (properties)) - glance.images.update(image_list[0]['id'], **properties) - return image_list[0]['id'] + glance.images.upload(glance_image.id, image_data) + glance.images.update(glance_image.id, **properties) + return glance_image.id + def remove_appliance(self, appliance): """Remove an appliance in glance """ project_name = self.mapping.get_project_from_vo(appliance.vo) - sess = keystone_client.get_session(project_name=project_name) - glance = glanceclient.Client(session=sess) - filters = {IMAGE_ID_TAG: appliance.identifier, - IMAGE_LIST_ID_TAG: appliance.image_list_identifier} - kwargs = {'filters': filters} - img_generator = glance.images.list(**kwargs) - image_list = list(img_generator) - if len(image_list) > 1: - LOG.error("Multiple images found with the same properties " - "(%s: %s, %s: %s)" % (IMAGE_ID_TAG, - appliance.identifier, - IMAGE_LIST_ID_TAG, - appliance.image_list_identifier)) + if not project_name: + LOG.debug("Cannot get project name from vo %s" % appliance.vo) return None - elif len(image_list) == 0: - LOG.error("No image found with the following properties " - "(%s: %s, %s: %s)" % (IMAGE_ID_TAG, - appliance.identifier, - IMAGE_LIST_ID_TAG, - appliance.image_list_identifier)) + + glance = openstack_client.get_glance_client(project_name) + if not glance: + LOG.error("Cannot get glance client for project %s" % project_name) return None - else: - LOG.info('Deleting image: %s' % image_list[0]['id']) - glance.images.delete(image_list[0]['id']) - return image_list[0]['id'] + + glance_image = utils.find_image(glance, appliance.identifier, + appliance.image_list_identifier) + if not glance_image: + LOG.info('Cannot delete image: image not found') + return None + + LOG.info('Deleting image: %s' % glance_image.id) + return glance_image.id class ImageListManager(object): @@ -203,28 +171,31 @@ def __init__(self): self.appliances = {} self.mapping = mapping.Mapping() - def update_image_list_identifiers(self, project_name=None): + def update_image_list_identifiers(self): """Update the identifier list """ appliances = {} - if project_name: - project_list = [project_name] - else: - project_list = self.mapping.get_projects() - for project in project_list: + + for project_name in self.mapping.get_projects(): + glance = openstack_client.get_glance_client(project_name) + if not glance: + LOG.error("Not authorized to manage images from the " + "project: %s" % project_name) + continue try: - sess = keystone_client.get_session(project) - glance = glanceclient.Client(session=sess) img_generator = glance.images.list() image_list = list(img_generator) - for image in image_list: - if IMAGE_LIST_ID_TAG in image: - if image[IMAGE_LIST_ID_TAG] not in appliances: - appliances[image[IMAGE_LIST_ID_TAG]] = [] - appliances[image[IMAGE_LIST_ID_TAG]].append(image) except Exception: LOG.error("Not authorized to manage images from the " - "project: %s" % project) + "project: %s" % project_name) + continue + + for image in image_list: + if IMAGE_LIST_ID_TAG in image: + if image[IMAGE_LIST_ID_TAG] not in appliances: + appliances[image[IMAGE_LIST_ID_TAG]] = [] + appliances[image[IMAGE_LIST_ID_TAG]].append(image) + self.appliances = appliances def get_appliances(self, image_list_identifier): @@ -266,8 +237,15 @@ def remove_image_list(self, image_list_identifier): return None vo_name = self.appliances[image_list_identifier][0]['APPLIANCE_VO'] project_name = self.mapping.get_project_from_vo(vo_name) - sess = keystone_client.get_session(project_name) - glance = glanceclient.Client(session=sess) + if not project_name: + LOG.debug("Cannot get project name from vo %s" % vo_name) + return None + + glance = openstack_client.get_glance_client(project_name) + if not glance: + LOG.error("Cannot get glance client for project %s" % project_name) + return None + LOG.debug("Delete all images with the Image List Identifier: " "%s" % image_list_identifier) for image in self.appliances[image_list_identifier]: diff --git a/cloudkeeper_os/openstack_client.py b/cloudkeeper_os/openstack_client.py index 9addd09..869d1f8 100644 --- a/cloudkeeper_os/openstack_client.py +++ b/cloudkeeper_os/openstack_client.py @@ -17,11 +17,15 @@ """Keystone helper """ +import glanceclient.v2.client as glanceclient from keystoneauth1.identity import v3 from keystoneauth1 import session from oslo_config import cfg +from oslo_log import log +import webob.exc CONF = cfg.CONF +LOG = log.getLogger(__name__) CFG_GROUP = "keystone_authtoken" @@ -32,3 +36,16 @@ def get_session(project_name): auth_params['project_name'] = project_name auth = v3.Password(**auth_params) return session.Session(auth=auth, verify=False) + +def get_glance_client(project_name): + """Get a glance client + """ + LOG.debug("Get glance client for project: %s" % project_name) + try: + sess = get_session(project_name=project_name) + glance_client = glanceclient.Client(session=sess) + except webob.exc.HTTPForbidden as err: + LOG.error("Connection to Glance failed.") + LOG.exception(err) + return None + return glance_client diff --git a/cloudkeeper_os/utils.py b/cloudkeeper_os/utils.py index 448dc0e..87a1863 100644 --- a/cloudkeeper_os/utils.py +++ b/cloudkeeper_os/utils.py @@ -17,19 +17,33 @@ """A set of utils for Cloudkeeper-OS """ +import json import shutil +import uuid import requests from requests.auth import HTTPBasicAuth +from oslo_config import cfg from oslo_log import log +from cloudkeeper_os import constants + +CONF = cfg.CONF LOG = log.getLogger(__name__) +IMAGE_ID_TAG = constants.IMAGE_ID_TAG +IMAGE_LIST_ID_TAG = constants.IMAGE_LIST_ID_TAG + -def retrieve_image(uri, filename, username='', password='', capath=None): - """Retrieve an image +def retrieve_image(appliance): + """Retrieve an image from Cloudkeeper NGINX """ # TODO manage SSL case + filename = CONF.tempdir + '/' + str(uuid.uuid4()) + uri = appliance.image.location + username = appliance.image.username + password = appliance.image.password + LOG.info("Download image from %s" % uri) auth = HTTPBasicAuth(username, password) response = requests.get(uri, auth=auth, stream=True) @@ -39,8 +53,52 @@ def retrieve_image(uri, filename, username='', password='', capath=None): shutil.copyfileobj(response.raw, output) output.close() LOG.debug("Image data successfully saved to %s" % filename) - return True else: - # TODO raise an exception LOG.error("Failed to download image data due to HTTP error") - return False + return None + return filename + + +def find_image(glance_client, identifier, image_list_identifier): + """Search for a glance image given a appliance and image list identifiers + """ + filters = {IMAGE_ID_TAG: identifier, + IMAGE_LIST_ID_TAG: image_list_identifier} + kwargs = {'filters': filters} + img_generator = glance_client.images.list(**kwargs) + image_list = list(img_generator) + + if len(image_list) > 1: + LOG.error("Multiple images found with the same properties " + "(%s: %s, %s: %s)" % (IMAGE_ID_TAG, identifier, + IMAGE_LIST_ID_TAG, + image_list_identifier)) + return None + elif len(image_list) == 0: + LOG.error("No image found with the following properties " + "(%s: %s, %s: %s)" % (IMAGE_ID_TAG, identifier, + IMAGE_LIST_ID_TAG, + image_list_identifier)) + return None + else: + return image_list[0] + +def extract_appliance_properties(appliance): + """Extract properties from an appliance + """ + properties = {} + for (descriptor, value) in appliance.ListFields(): + if descriptor.name == 'identifier': + key = IMAGE_ID_TAG + elif descriptor.name == 'image_list_identifier': + key = IMAGE_LIST_ID_TAG + else: + if descriptor.name == 'attributes': + data = dict(value) + value = json.dumps(data) + LOG.debug("attribute value: %s" % value) + key = 'APPLIANCE_' + str.upper(descriptor.name) + properties[key] = str(value) + # Add property for cloud-info-provider compatibility + properties['vmcatcher_event_ad_mpuri'] = appliance.mpuri + return properties