186 lines
8.1 KiB
Python
186 lines
8.1 KiB
Python
# 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 effectivly 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 operiation. 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 nova.openstack.common.gettextutils import _
|
|
from nova.openstack.common import lockutils
|
|
from nova.openstack.common import log as logging
|
|
from nova.openstack.common import timeutils
|
|
from nova.virt import imagecache
|
|
from nova.virt.vmwareapi import ds_util
|
|
from nova.virt.vmwareapi import error_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, path, dc_ref):
|
|
try:
|
|
ds_util.file_delete(self._session, path, dc_ref)
|
|
except (error_util.CannotDeleteFileException,
|
|
error_util.FileFaultException,
|
|
error_util.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': path, 'ex': e})
|
|
except error_util.FileNotFoundException:
|
|
LOG.debug(_("File not found: %s"), path)
|
|
|
|
def timestamp_folder_get(self, ds_path, image_id):
|
|
"""Returns the timestamp folder."""
|
|
return '%s/%s' % (ds_path, image_id)
|
|
|
|
def timestamp_cleanup(self, dc_ref, ds_browser,
|
|
ds_ref, ds_name, ds_path):
|
|
ts = self._get_timestamp(ds_browser, ds_path)
|
|
if ts:
|
|
ts_path = '%s/%s' % (ds_path, 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)
|
|
if not ds_browser:
|
|
ds_browser = vim_util.get_dynamic_property(
|
|
self._session._get_vim(), ds_ref,
|
|
"Datastore", "browser")
|
|
self._ds_browser[ds_ref] = 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(path, lock_file_prefix='nova-vmware-ts',
|
|
external=True):
|
|
ts = self._get_timestamp(ds_browser, path)
|
|
if not ts:
|
|
ts_path = '%s/%s' % (path,
|
|
self._get_timestamp_filename())
|
|
try:
|
|
ds_util.mkdir(self._session, ts_path, dc_info.ref)
|
|
except error_util.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(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(path, lock_file_prefix='nova-vmware-ts',
|
|
external=True):
|
|
self.timestamp_cleanup(dc_info.ref, ds_browser,
|
|
datastore['ref'], datastore['name'],
|
|
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 = ds_util.build_datastore_path(datastore['name'],
|
|
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)
|