summaryrefslogtreecommitdiff
path: root/nova/virt/vmwareapi/imagecache.py
blob: 946da3aef4a7e366f9fc6627986224f617873ebb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# Copyright (c) 2014 VMware, Inc.
#
#    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.

"""
Image cache class

Images that are stored in the cache folder will be stored in a folder whose
name is the image ID. In the event that an image is discovered to be no longer
used then a timestamp will be added to the image folder.
The timestamp will be a folder - this is due to the fact that we can use the
VMware API's for creating and deleting of folders (it really simplifies
things). The timestamp will contain the time, on the compute node, when the
image was first seen to be unused.
At each aging iteration we check if the image can be aged.
This is done by comparing the current nova compute time to the time embedded
in the timestamp. If the time exceeds the configured aging time then
the parent folder, that is the image ID folder, will be deleted.
That effectively ages the cached image.
If an image is used then the timestamps will be deleted.

When accessing a timestamp we make use of locking. This ensure that aging
will not delete an image during the spawn operation. When spawning
the timestamp folder will be locked  and the timestamps will be purged.
This will ensure that a image is not deleted during the spawn.
"""

from oslo.config import cfg
from oslo.utils import timeutils
from oslo.vmware import exceptions as vexc

from nova.i18n import _
from nova.openstack.common import lockutils
from nova.openstack.common import log as logging
from nova.virt import imagecache
from nova.virt.vmwareapi import ds_util
from nova.virt.vmwareapi import vim_util

LOG = logging.getLogger(__name__)

CONF = cfg.CONF
CONF.import_opt('remove_unused_original_minimum_age_seconds',
                'nova.virt.imagecache')

TIMESTAMP_PREFIX = 'ts-'
TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M-%S'


class ImageCacheManager(imagecache.ImageCacheManager):
    def __init__(self, session, base_folder):
        super(ImageCacheManager, self).__init__()
        self._session = session
        self._base_folder = base_folder
        self._ds_browser = {}

    def _folder_delete(self, ds_path, dc_ref):
        try:
            ds_util.file_delete(self._session, ds_path, dc_ref)
        except (vexc.CannotDeleteFileException,
                vexc.FileFaultException,
                vexc.FileLockedException) as e:
            # There may be more than one process or thread that tries
            # to delete the file.
            LOG.warning(_("Unable to delete %(file)s. Exception: %(ex)s"),
                        {'file': ds_path, 'ex': e})
        except vexc.FileNotFoundException:
            LOG.debug("File not found: %s", ds_path)

    def enlist_image(self, image_id, datastore, dc_ref):
        ds_browser = self._get_ds_browser(datastore.ref)
        cache_root_folder = datastore.build_path(self._base_folder)

        # Check if the timestamp file exists - if so then delete it. This
        # will ensure that the aging will not delete a cache image if it
        # is going to be used now.
        path = self.timestamp_folder_get(cache_root_folder, image_id)

        # Lock to ensure that the spawn will not try and access a image
        # that is currently being deleted on the datastore.
        with lockutils.lock(str(path), lock_file_prefix='nova-vmware-ts',
                            external=True):
            self.timestamp_cleanup(dc_ref, ds_browser, path)

    def timestamp_folder_get(self, ds_path, image_id):
        """Returns the timestamp folder."""
        return ds_path.join(image_id)

    def timestamp_cleanup(self, dc_ref, ds_browser, ds_path):
        ts = self._get_timestamp(ds_browser, ds_path)
        if ts:
            ts_path = ds_path.join(ts)
            LOG.debug("Timestamp path %s exists. Deleting!", ts_path)
            # Image is used - no longer need timestamp folder
            self._folder_delete(ts_path, dc_ref)

    def _get_timestamp(self, ds_browser, ds_path):
        files = ds_util.get_sub_folders(self._session, ds_browser, ds_path)
        if files:
            for file in files:
                if file.startswith(TIMESTAMP_PREFIX):
                    return file

    def _get_timestamp_filename(self):
        return '%s%s' % (TIMESTAMP_PREFIX,
                         timeutils.strtime(fmt=TIMESTAMP_FORMAT))

    def _get_datetime_from_filename(self, timestamp_filename):
        ts = timestamp_filename.lstrip(TIMESTAMP_PREFIX)
        return timeutils.parse_strtime(ts, fmt=TIMESTAMP_FORMAT)

    def _get_ds_browser(self, ds_ref):
        ds_browser = self._ds_browser.get(ds_ref.value)
        if not ds_browser:
            ds_browser = vim_util.get_dynamic_property(
                    self._session.vim, ds_ref,
                    "Datastore", "browser")
            self._ds_browser[ds_ref.value] = ds_browser
        return ds_browser

    def _list_datastore_images(self, ds_path, datastore):
        """Return a list of the images present in _base.

        This method returns a dictionary with the following keys:
            - unexplained_images
            - originals
        """
        ds_browser = self._get_ds_browser(datastore.ref)
        originals = ds_util.get_sub_folders(self._session, ds_browser,
                                            ds_path)
        return {'unexplained_images': [],
                'originals': originals}

    def _age_cached_images(self, context, datastore, dc_info,
                           ds_path):
        """Ages cached images."""
        age_seconds = CONF.remove_unused_original_minimum_age_seconds
        unused_images = self.originals - self.used_images
        ds_browser = self._get_ds_browser(datastore.ref)
        for image in unused_images:
            path = self.timestamp_folder_get(ds_path, image)
            # Lock to ensure that the spawn will not try and access a image
            # that is currently being deleted on the datastore.
            with lockutils.lock(str(path), lock_file_prefix='nova-vmware-ts',
                                external=True):
                ts = self._get_timestamp(ds_browser, path)
                if not ts:
                    ts_path = path.join(self._get_timestamp_filename())
                    try:
                        ds_util.mkdir(self._session, ts_path, dc_info.ref)
                    except vexc.FileAlreadyExistsException:
                        LOG.debug("Timestamp already exists.")
                    LOG.info(_("Image %s is no longer used by this node. "
                               "Pending deletion!"), image)
                else:
                    dt = self._get_datetime_from_filename(str(ts))
                    if timeutils.is_older_than(dt, age_seconds):
                        LOG.info(_("Image %s is no longer used. "
                                   "Deleting!"), path)
                        # Image has aged - delete the image ID folder
                        self._folder_delete(path, dc_info.ref)

        # If the image is used and the timestamp file exists then we delete
        # the timestamp.
        for image in self.used_images:
            path = self.timestamp_folder_get(ds_path, image)
            with lockutils.lock(str(path), lock_file_prefix='nova-vmware-ts',
                                external=True):
                self.timestamp_cleanup(dc_info.ref, ds_browser,
                                       path)

    def update(self, context, instances, datastores_info):
        """The cache manager entry point.

        This will invoke the cache manager. This will update the cache
        according to the defined cache management scheme. The information
        populated in the cached stats will be used for the cache management.
        """
        # read running instances data
        running = self._list_running_instances(context, instances)
        self.used_images = set(running['used_images'].keys())
        # perform the aging and image verification per datastore
        for (datastore, dc_info) in datastores_info:
            ds_path = datastore.build_path(self._base_folder)
            images = self._list_datastore_images(ds_path, datastore)
            self.originals = images['originals']
            self._age_cached_images(context, datastore, dc_info, ds_path)

    def get_image_cache_folder(self, datastore, image_id):
        """Returns datastore path of folder containing the image."""
        return datastore.build_path(self._base_folder, image_id)