# Copyright 2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # 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. from oslo_config import cfg from oslo_utils import uuidutils from swiftclient import utils as swift_utils from iotronic.common import exception as exc from iotronic.common.glance_service import base_image_service from iotronic.common.glance_service import service from iotronic.common.glance_service import service_utils from iotronic.common.i18n import _ glance_opts = [ cfg.ListOpt('allowed_direct_url_schemes', default=[], help='A list of URL schemes that can be downloaded directly ' 'via the direct_url. Currently supported schemes: ' '[file].'), # To upload this key to Swift: # swift post -m Temp-Url-Key:correcthorsebatterystaple cfg.StrOpt('swift_temp_url_key', help='The secret token given to Swift to allow temporary URL ' 'downloads. Required for temporary URLs.', secret=True), cfg.IntOpt('swift_temp_url_duration', min=0, default=1200, help='The length of time in seconds that the temporary URL ' 'will be valid for. Defaults to 20 minutes. If some ' 'deploys get a 401 response code when trying to download ' 'from the temporary URL, try raising this duration.'), cfg.StrOpt('swift_endpoint_url', help='The "endpoint" (scheme, hostname, optional port) for ' 'the Swift URL of the form ' '"endpoint_url/api_version/account/container/object_id". ' 'Do not include trailing "/". ' 'For example, use "https://swift.example.com". ' 'Required for temporary URLs.'), cfg.StrOpt('swift_api_version', default='v1', help='The Swift API version to create a temporary URL for. ' 'Defaults to "v1". Swift temporary URL format: ' '"endpoint_url/api_version/account/container/object_id"'), cfg.StrOpt('swift_account', help='The account that Glance uses to communicate with ' 'Swift. The format is "AUTH_uuid". "uuid" is the ' 'UUID for the account configured in the glance-api.conf. ' 'Required for temporary URLs. For example: ' '"AUTH_a422b2-91f3-2f46-74b7-d7c9e8958f5d30". ' 'Swift temporary URL format: ' '"endpoint_url/api_version/account/container/object_id"'), cfg.StrOpt('swift_container', default='glance', help='The Swift container Glance is configured to store its ' 'images in. Defaults to "glance", which is the default ' 'in glance-api.conf. ' 'Swift temporary URL format: ' '"endpoint_url/api_version/account/container/object_id"'), cfg.IntOpt('swift_store_multiple_containers_seed', default=0, help='This should match a config by the same name in the ' 'Glance configuration file. When set to 0, a ' 'single-tenant store will only use one ' 'container to store all images. When set to an integer ' 'value between 1 and 32, a single-tenant store will use ' 'multiple containers to store images, and this value ' 'will determine how many containers are created.'), ] CONF = cfg.CONF CONF.register_opts(glance_opts, group='glance') class GlanceImageService(base_image_service.BaseImageService, service.ImageService): def detail(self, **kwargs): return self._detail(method='list', **kwargs) def show(self, image_id): return self._show(image_id, method='get') def download(self, image_id, data=None): return self._download(image_id, method='data', data=data) def create(self, image_meta, data=None): image_id = self._create(image_meta, method='create', data=None)['id'] return self.update(image_id, None, data) def update(self, image_id, image_meta, data=None, purge_props=False): # NOTE(ghe): purge_props not working until bug 1206472 solved return self._update(image_id, image_meta, data, method='update', purge_props=False) def delete(self, image_id): return self._delete(image_id, method='delete') def swift_temp_url(self, image_info): """Generate a no-auth Swift temporary URL. This function will generate the temporary Swift URL using the image id from Glance and the config options: 'swift_endpoint_url', 'swift_api_version', 'swift_account' and 'swift_container'. The temporary URL will be valid for 'swift_temp_url_duration' seconds. This allows Iotronic to download a Glance image without passing around an auth_token. :param image_info: The return from a GET request to Glance for a certain image_id. Should be a dictionary, with keys like 'name' and 'checksum'. See http://docs.openstack.org/developer/glance/glanceapi.html for examples. :returns: A signed Swift URL from which an image can be downloaded, without authentication. :raises: InvalidParameterValue if Swift config options are not set correctly. :raises: MissingParameterValue if a required parameter is not set. :raises: ImageUnacceptable if the image info from Glance does not have a image ID. """ self._validate_temp_url_config() if ('id' not in image_info or not uuidutils.is_uuid_like(image_info['id'])): raise exc.ImageUnacceptable(_( 'The given image info does not have a valid image id: %s') % image_info) url_fragments = { 'endpoint_url': CONF.glance.swift_endpoint_url, 'api_version': CONF.glance.swift_api_version, 'account': CONF.glance.swift_account, 'container': self._get_swift_container(image_info['id']), 'object_id': image_info['id'] } template = '/{api_version}/{account}/{container}/{object_id}' url_path = template.format(**url_fragments) path = swift_utils.generate_temp_url( path=url_path, seconds=CONF.glance.swift_temp_url_duration, key=CONF.glance.swift_temp_url_key, method='GET') return '{endpoint_url}{url_path}'.format( endpoint_url=url_fragments['endpoint_url'], url_path=path) def _validate_temp_url_config(self): """Validate the required settings for a temporary URL.""" if not CONF.glance.swift_temp_url_key: raise exc.MissingParameterValue(_( 'Swift temporary URLs require a shared secret to be created. ' 'You must provide "swift_temp_url_key" as a config option.')) if not CONF.glance.swift_endpoint_url: raise exc.MissingParameterValue(_( 'Swift temporary URLs require a Swift endpoint URL. ' 'You must provide "swift_endpoint_url" as a config option.')) if not CONF.glance.swift_account: raise exc.MissingParameterValue(_( 'Swift temporary URLs require a Swift account string. ' 'You must provide "swift_account" as a config option.')) seed_num_chars = CONF.glance.swift_store_multiple_containers_seed if (seed_num_chars is None or seed_num_chars < 0 or seed_num_chars > 32): raise exc.InvalidParameterValue(_( "An integer value between 0 and 32 is required for" " swift_store_multiple_containers_seed.")) def _get_swift_container(self, image_id): """Get the Swift container the image is stored in. Code based on: https://github.com/openstack/glance_store/blob/3cd690b3 7dc9d935445aca0998e8aec34a3e3530/glance_store/ _drivers/swift/store.py#L725 Returns appropriate container name depending upon value of ``swift_store_multiple_containers_seed``. In single-container mode, which is a seed value of 0, simply returns ``swift_container``. In multiple-container mode, returns ``swift_container`` as the prefix plus a suffix determined by the multiple container seed examples: single-container mode: 'glance' multiple-container mode: 'glance_3a1' for image uuid 3A1xxxxxxx... :param image_id: UUID of image :returns: The name of the swift container the image is stored in """ seed_num_chars = CONF.glance.swift_store_multiple_containers_seed if seed_num_chars > 0: image_id = str(image_id).lower() num_dashes = image_id[:seed_num_chars].count('-') num_chars = seed_num_chars + num_dashes name_suffix = image_id[:num_chars] new_container_name = (CONF.glance.swift_container + '_' + name_suffix) return new_container_name else: return CONF.glance.swift_container def _get_location(self, image_id): """Get storage URL. Returns the direct url representing the backend storage location, or None if this attribute is not shown by Glance. """ image_meta = self.call('get', image_id) if not service_utils.is_image_available(self.context, image_meta): raise exc.ImageNotFound(image_id=image_id) return getattr(image_meta, 'direct_url', None)