232177e68c
As noted in previous discussions, glance should clean its staging directory on startup. This is important for scenarios where we started an import operation, but failed in the middle. If, when we recover, the image has already been deleted from the database, then we will never remove the (potentially very large) residue from disk in our staging directory. This is currently a problem with web-download, but will also occur with glance-direct once we have the non-shared distributed import functionality merged. Closes-Bug: #1913625 Change-Id: Ib80e9cfb58680f9e8ead5993dc206f4da882dd09
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
# Copyright 2021 Red Hat, Inc.
|
|
# 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.
|
|
|
|
import os
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import uuidutils
|
|
|
|
from glance.common import exception
|
|
from glance.common import store_utils
|
|
from glance import context
|
|
from glance.i18n import _LE
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
|
|
|
|
def staging_store_path():
|
|
"""Return the local path to the staging store.
|
|
|
|
:raises: GlanceException if staging store is not configured to be
|
|
a file:// URI
|
|
"""
|
|
if CONF.enabled_backends:
|
|
separator, staging_dir = store_utils.get_dir_separator()
|
|
else:
|
|
staging_dir = CONF.node_staging_uri
|
|
expected_prefix = 'file://'
|
|
if not staging_dir.startswith(expected_prefix):
|
|
raise exception.GlanceException(
|
|
'Unexpected scheme in staging store; '
|
|
'unable to scan for residue')
|
|
return staging_dir[len(expected_prefix):]
|
|
|
|
|
|
class StagingStoreCleaner:
|
|
def __init__(self, db):
|
|
self.db = db
|
|
self.context = context.get_admin_context()
|
|
|
|
@staticmethod
|
|
def get_image_id(filename):
|
|
if '.' in filename:
|
|
filename, ext = filename.split('.', 1)
|
|
if uuidutils.is_uuid_like(filename):
|
|
return filename
|
|
|
|
def is_valid_image(self, image_id):
|
|
try:
|
|
image = self.db.image_get(self.context, image_id)
|
|
# FIXME(danms): Maybe check that it's not deleted or
|
|
# something else like state, size, etc
|
|
return not image['deleted']
|
|
except exception.ImageNotFound:
|
|
return False
|
|
|
|
@staticmethod
|
|
def delete_file(path):
|
|
try:
|
|
os.remove(path)
|
|
except FileNotFoundError:
|
|
# NOTE(danms): We must have raced with something else, so this
|
|
# is not a problem
|
|
pass
|
|
except Exception as e:
|
|
LOG.error(_LE('Failed to delete stale staging '
|
|
'path %(path)r: %(err)s'),
|
|
{'path': path, 'err': str(e)})
|
|
return False
|
|
return True
|
|
|
|
def clean_orphaned_staging_residue(self):
|
|
try:
|
|
files = os.listdir(staging_store_path())
|
|
except FileNotFoundError:
|
|
# NOTE(danms): If we cannot list the staging dir, there is
|
|
# clearly nothing left from a previous run, so nothing to
|
|
# clean up.
|
|
files = []
|
|
if not files:
|
|
return
|
|
|
|
LOG.debug('Found %i files in staging directory for potential cleanup',
|
|
len(files))
|
|
cleaned = ignored = error = 0
|
|
for filename in files:
|
|
image_id = self.get_image_id(filename)
|
|
if not image_id:
|
|
# NOTE(danms): We should probably either have a config option
|
|
# that decides what to do here (i.e. reap or ignore), or decide
|
|
# that this is not okay and just nuke anything we find.
|
|
LOG.debug('Staging directory contains unexpected non-image '
|
|
'file %r; ignoring',
|
|
filename)
|
|
ignored += 1
|
|
continue
|
|
if self.is_valid_image(image_id):
|
|
# NOTE(danms): We found a non-deleted image for this
|
|
# file, so leave it in place.
|
|
ignored += 1
|
|
continue
|
|
path = os.path.join(staging_store_path(), filename)
|
|
LOG.debug('Stale staging residue found for image '
|
|
'%(uuid)s: %(file)r; deleting now.',
|
|
{'uuid': image_id, 'file': path})
|
|
if self.delete_file(path):
|
|
cleaned += 1
|
|
else:
|
|
error += 1
|
|
|
|
LOG.debug('Cleaned %(cleaned)i stale staging files, '
|
|
'%(ignored)i ignored (%(error)i errors)',
|
|
{'cleaned': cleaned, 'ignored': ignored, 'error': error})
|