1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5#    http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10# See the License for the specific language governing permissions and
11# limitations under the License.
12
13# import types so that we can reference ListType in sphinx param declarations.
14# We can't just use list, because sphinx gets confused by
15# openstack.resource.Resource.list and openstack.resource2.Resource.list
16import types  # noqa
17
18from openstack.cloud import _normalize
19from openstack.cloud import _utils
20from openstack.cloud import exc
21from openstack import utils
22
23
24def _no_pending_images(images):
25    """If there are any images not in a steady state, don't cache"""
26    for image in images:
27        if image.status not in ('active', 'deleted', 'killed'):
28            return False
29    return True
30
31
32class ImageCloudMixin(_normalize.Normalizer):
33
34    def __init__(self):
35        self.image_api_use_tasks = self.config.config['image_api_use_tasks']
36
37    @property
38    def _raw_image_client(self):
39        if 'raw-image' not in self._raw_clients:
40            image_client = self._get_raw_client('image')
41            self._raw_clients['raw-image'] = image_client
42        return self._raw_clients['raw-image']
43
44    @property
45    def _image_client(self):
46        if 'image' not in self._raw_clients:
47            self._raw_clients['image'] = self._get_versioned_client(
48                'image', min_version=1, max_version='2.latest')
49        return self._raw_clients['image']
50
51    def search_images(self, name_or_id=None, filters=None):
52        images = self.list_images()
53        return _utils._filter_list(images, name_or_id, filters)
54
55    @_utils.cache_on_arguments(should_cache_fn=_no_pending_images)
56    def list_images(self, filter_deleted=True, show_all=False):
57        """Get available images.
58
59        :param filter_deleted: Control whether deleted images are returned.
60        :param show_all: Show all images, including images that are shared
61            but not accepted. (By default in glance v2 shared image that
62            have not been accepted are not shown) show_all will override the
63            value of filter_deleted to False.
64        :returns: A list of glance images.
65        """
66        if show_all:
67            filter_deleted = False
68        # First, try to actually get images from glance, it's more efficient
69        images = []
70        params = {}
71        image_list = []
72        if self._is_client_version('image', 2):
73            if show_all:
74                params['member_status'] = 'all'
75        image_list = list(self.image.images(**params))
76
77        for image in image_list:
78            # The cloud might return DELETED for invalid images.
79            # While that's cute and all, that's an implementation detail.
80            if not filter_deleted:
81                images.append(image)
82            elif image.status.lower() != 'deleted':
83                images.append(image)
84        return self._normalize_images(images)
85
86    def get_image(self, name_or_id, filters=None):
87        """Get an image by name or ID.
88
89        :param name_or_id: Name or ID of the image.
90        :param filters:
91            A dictionary of meta data to use for further filtering. Elements
92            of this dictionary may, themselves, be dictionaries. Example::
93
94                {
95                  'last_name': 'Smith',
96                  'other': {
97                      'gender': 'Female'
98                  }
99                }
100
101            OR
102            A string containing a jmespath expression for further filtering.
103            Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
104
105        :returns: An image ``munch.Munch`` or None if no matching image
106                  is found
107
108        """
109        return _utils._get_entity(self, 'image', name_or_id, filters)
110
111    def get_image_by_id(self, id):
112        """ Get a image by ID
113
114        :param id: ID of the image.
115        :returns: An image ``munch.Munch``.
116        """
117        image = self._normalize_image(
118            self.image.get_image(image={'id': id}))
119
120        return image
121
122    def download_image(
123            self, name_or_id, output_path=None, output_file=None,
124            chunk_size=1024):
125        """Download an image by name or ID
126
127        :param str name_or_id: Name or ID of the image.
128        :param output_path: the output path to write the image to. Either this
129            or output_file must be specified
130        :param output_file: a file object (or file-like object) to write the
131            image data to. Only write() will be called on this object. Either
132            this or output_path must be specified
133        :param int chunk_size: size in bytes to read from the wire and buffer
134            at one time. Defaults to 1024
135
136        :raises: OpenStackCloudException in the event download_image is called
137            without exactly one of either output_path or output_file
138        :raises: OpenStackCloudResourceNotFound if no images are found matching
139            the name or ID provided
140        """
141        if output_path is None and output_file is None:
142            raise exc.OpenStackCloudException(
143                'No output specified, an output path or file object'
144                ' is necessary to write the image data to')
145        elif output_path is not None and output_file is not None:
146            raise exc.OpenStackCloudException(
147                'Both an output path and file object were provided,'
148                ' however only one can be used at once')
149
150        image = self.image.find_image(name_or_id)
151        if not image:
152            raise exc.OpenStackCloudResourceNotFound(
153                "No images with name or ID %s were found" % name_or_id, None)
154
155        return self.image.download_image(
156            image, output=output_file or output_path,
157            chunk_size=chunk_size)
158
159    def get_image_exclude(self, name_or_id, exclude):
160        for image in self.search_images(name_or_id):
161            if exclude:
162                if exclude not in image.name:
163                    return image
164            else:
165                return image
166        return None
167
168    def get_image_name(self, image_id, exclude=None):
169        image = self.get_image_exclude(image_id, exclude)
170        if image:
171            return image.name
172        return None
173
174    def get_image_id(self, image_name, exclude=None):
175        image = self.get_image_exclude(image_name, exclude)
176        if image:
177            return image.id
178        return None
179
180    def wait_for_image(self, image, timeout=3600):
181        image_id = image['id']
182        for count in utils.iterate_timeout(
183                timeout, "Timeout waiting for image to snapshot"):
184            self.list_images.invalidate(self)
185            image = self.get_image(image_id)
186            if not image:
187                continue
188            if image['status'] == 'active':
189                return image
190            elif image['status'] == 'error':
191                raise exc.OpenStackCloudException(
192                    'Image {image} hit error state'.format(image=image_id))
193
194    def delete_image(
195            self, name_or_id, wait=False, timeout=3600, delete_objects=True):
196        """Delete an existing image.
197
198        :param name_or_id: Name of the image to be deleted.
199        :param wait: If True, waits for image to be deleted.
200        :param timeout: Seconds to wait for image deletion. None is forever.
201        :param delete_objects: If True, also deletes uploaded swift objects.
202
203        :returns: True if delete succeeded, False otherwise.
204
205        :raises: OpenStackCloudException if there are problems deleting.
206        """
207        image = self.get_image(name_or_id)
208        if not image:
209            return False
210        self.image.delete_image(image)
211        self.list_images.invalidate(self)
212
213        # Task API means an image was uploaded to swift
214        # TODO(gtema) does it make sense to move this into proxy?
215        if self.image_api_use_tasks and (
216                self.image._IMAGE_OBJECT_KEY in image
217                or self.image._SHADE_IMAGE_OBJECT_KEY in image):
218            (container, objname) = image.get(
219                self.image._IMAGE_OBJECT_KEY, image.get(
220                    self.image._SHADE_IMAGE_OBJECT_KEY)).split('/', 1)
221            self.delete_object(container=container, name=objname)
222
223        if wait:
224            for count in utils.iterate_timeout(
225                    timeout,
226                    "Timeout waiting for the image to be deleted."):
227                self._get_cache(None).invalidate()
228                if self.get_image(image.id) is None:
229                    break
230        return True
231
232    def create_image(
233            self, name, filename=None,
234            container=None,
235            md5=None, sha256=None,
236            disk_format=None, container_format=None,
237            disable_vendor_agent=True,
238            wait=False, timeout=3600, tags=None,
239            allow_duplicates=False, meta=None, volume=None, **kwargs):
240        """Upload an image.
241
242        :param str name: Name of the image to create. If it is a pathname
243                         of an image, the name will be constructed from the
244                         extensionless basename of the path.
245        :param str filename: The path to the file to upload, if needed.
246                             (optional, defaults to None)
247        :param str container: Name of the container in swift where images
248                              should be uploaded for import if the cloud
249                              requires such a thing. (optiona, defaults to
250                              'images')
251        :param str md5: md5 sum of the image file. If not given, an md5 will
252                        be calculated.
253        :param str sha256: sha256 sum of the image file. If not given, an md5
254                           will be calculated.
255        :param str disk_format: The disk format the image is in. (optional,
256                                defaults to the os-client-config config value
257                                for this cloud)
258        :param str container_format: The container format the image is in.
259                                     (optional, defaults to the
260                                     os-client-config config value for this
261                                     cloud)
262        :param list tags: List of tags for this image. Each tag is a string
263                          of at most 255 chars.
264        :param bool disable_vendor_agent: Whether or not to append metadata
265                                          flags to the image to inform the
266                                          cloud in question to not expect a
267                                          vendor agent to be runing.
268                                          (optional, defaults to True)
269        :param bool wait: If true, waits for image to be created. Defaults to
270                          true - however, be aware that one of the upload
271                          methods is always synchronous.
272        :param timeout: Seconds to wait for image creation. None is forever.
273        :param allow_duplicates: If true, skips checks that enforce unique
274                                 image name. (optional, defaults to False)
275        :param meta: A dict of key/value pairs to use for metadata that
276                     bypasses automatic type conversion.
277        :param volume: Name or ID or volume object of a volume to create an
278                       image from. Mutually exclusive with (optional, defaults
279                       to None)
280
281        Additional kwargs will be passed to the image creation as additional
282        metadata for the image and will have all values converted to string
283        except for min_disk, min_ram, size and virtual_size which will be
284        converted to int.
285
286        If you are sure you have all of your data types correct or have an
287        advanced need to be explicit, use meta. If you are just a normal
288        consumer, using kwargs is likely the right choice.
289
290        If a value is in meta and kwargs, meta wins.
291
292        :returns: A ``munch.Munch`` of the Image object
293
294        :raises: OpenStackCloudException if there are problems uploading
295        """
296        if volume:
297            image = self.block_storage.create_image(
298                name=name, volume=volume,
299                allow_duplicates=allow_duplicates,
300                container_format=container_format, disk_format=disk_format,
301                wait=wait, timeout=timeout)
302        else:
303            image = self.image.create_image(
304                name, filename=filename,
305                container=container,
306                md5=md5, sha256=sha256,
307                disk_format=disk_format, container_format=container_format,
308                disable_vendor_agent=disable_vendor_agent,
309                wait=wait, timeout=timeout, tags=tags,
310                allow_duplicates=allow_duplicates, meta=meta, **kwargs)
311
312        self._get_cache(None).invalidate()
313        if not wait:
314            return image
315        try:
316            for count in utils.iterate_timeout(
317                    timeout,
318                    "Timeout waiting for the image to finish."):
319                image_obj = self.get_image(image.id)
320                if image_obj and image_obj.status not in ('queued', 'saving'):
321                    return image_obj
322        except exc.OpenStackCloudTimeout:
323            self.log.debug(
324                "Timeout waiting for image to become ready. Deleting.")
325            self.delete_image(image.id, wait=True)
326            raise
327
328    def update_image_properties(
329            self, image=None, name_or_id=None, meta=None, **properties):
330        image = image or name_or_id
331        return self.image.update_image_properties(
332            image=image, meta=meta, **properties)
333