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})
|