Merge "Test glance hash calculation stops on image deletion"

This commit is contained in:
Zuul
2025-07-23 23:55:54 +00:00
committed by Gerrit Code Review
5 changed files with 173 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
---
features:
- |
Add new location API support to image V2 client

View File

@@ -19,6 +19,7 @@ import random
from oslo_log import log as logging
from tempest.api.image import base
from tempest.common import image as image_utils
from tempest.common import waiters
from tempest import config
from tempest.lib.common.utils import data_utils
@@ -980,3 +981,87 @@ class ImageLocationsTest(base.BaseV2ImageTest):
self.assertEqual(orig_image['os_hash_value'], image['os_hash_value'])
self.assertEqual(orig_image['os_hash_algo'], image['os_hash_algo'])
self.assertNotIn('validation_data', image['locations'][0])
class HashCalculationRemoteDeletionTest(base.BaseV2ImageTest):
"""Test calculation of image hash with new location API when the image is
deleted from a remote Glance service.
"""
@classmethod
def resource_setup(cls):
super(HashCalculationRemoteDeletionTest,
cls).resource_setup()
if not cls.versions_client.has_version('2.17'):
# API is not new enough to support add location API
skip_msg = (
'%s skipped as Glance does not support v2.17')
raise cls.skipException(skip_msg)
@classmethod
def skip_checks(cls):
super(HashCalculationRemoteDeletionTest,
cls).skip_checks()
if not CONF.image_feature_enabled.do_secure_hash:
skip_msg = (
"%s skipped as do_secure_hash is disabled" %
cls.__name__)
raise cls.skipException(skip_msg)
if not CONF.image_feature_enabled.http_store_enabled:
skip_msg = (
"%s skipped as http store is disabled" %
cls.__name__)
raise cls.skipException(skip_msg)
@decorators.idempotent_id('123e4567-e89b-12d3-a456-426614174000')
def test_hash_calculation_cancelled(self):
"""Test that image hash calculation is cancelled when the image
is deleted from a remote Glance service.
This test creates an image using new location API, verifies that
the hash calculation is initiated, and then deletes the image from a
remote Glance service, and verifies that the hash calculation process
is properly cancelled and image deleted successfully.
"""
# Create an image with a location
image_name = data_utils.rand_name('image')
container_format = CONF.image.container_formats[0]
disk_format = CONF.image.disk_formats[0]
image = self.create_image(name=image_name,
container_format=container_format,
disk_format=disk_format,
visibility='private')
self.assertEqual(image_name, image['name'])
self.assertEqual('queued', image['status'])
# Start http server at random port to simulate the image location
# and to provide random data for the image with slow transfer
server = image_utils.RandomDataServer()
server.start()
self.addCleanup(server.stop)
# Add a location to the image
location = 'http://localhost:%d' % server.port
self.client.add_image_location(image['id'], location)
waiters.wait_for_image_status(self.client, image['id'], 'active')
# Verify that the hash calculation is initiated
image_info = self.client.show_image(image['id'])
self.assertEqual(CONF.image.hashing_algorithm,
image_info['os_hash_algo'])
self.assertEqual('active', image_info['status'])
if CONF.image.alternate_image_endpoint:
# If alternate image endpoint is configured, we will delete the
# image from the alternate worker
self.os_primary.image_client_remote.delete_image(image['id'])
else:
# delete image from backend
self.client.delete_image(image['id'])
# If image is deleted successfully, the hash calculation is cancelled
self.client.wait_for_resource_deletion(image['id'])
# Stop the server to release the port
server.stop()

View File

@@ -14,6 +14,10 @@
# under the License.
import copy
from http import server
import random
import threading
import time
def get_image_meta_from_headers(resp):
@@ -63,3 +67,57 @@ def image_meta_to_headers(**metadata):
headers['x-image-meta-%s' % key] = str(value)
return headers
class RandomDataHandler(server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.end_headers()
start_time = time.time()
chunk_size = 64 * 1024 # 64 KiB per chunk
while time.time() - start_time < 60:
data = bytes(random.getrandbits(8) for _ in range(chunk_size))
try:
self.wfile.write(data)
self.wfile.flush()
# simulate slow transfer
time.sleep(0.2)
except BrokenPipeError:
# Client disconnected; stop sending data
break
def do_HEAD(self):
# same size as in do_GET (19,660,800 bytes (about 18.75 MiB)
size = 300 * 65536
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Content-Length', str(size))
self.end_headers()
class RandomDataServer(object):
def __init__(self, handler_class=RandomDataHandler):
self.handler_class = handler_class
self.server = None
self.thread = None
self.port = None
def start(self):
# Bind to port 0 for an unused port
self.server = server.HTTPServer(('localhost', 0), self.handler_class)
self.port = self.server.server_address[1]
# Run server in background thread
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.daemon = True
self.thread.start()
def stop(self):
if self.server:
self.server.shutdown()
self.server.server_close()
self.thread.join()
self.server = None
self.thread = None

View File

@@ -688,6 +688,11 @@ ImageGroup = [
'vdi', 'iso', 'vhdx'],
help="A list of image's disk formats "
"users can specify."),
cfg.StrOpt('hashing_algorithm',
default='sha512',
help=('Hashing algorithm used by glance to calculate image '
'hashes. This configuration value should be same as '
'glance-api.conf: hashing_algorithm config option.')),
cfg.StrOpt('images_manifest_file',
default=None,
help="A path to a manifest.yml generated using the "
@@ -732,6 +737,17 @@ ImageFeaturesGroup = [
help=('Indicates that image format is enforced by glance, '
'such that we should not expect to be able to upload '
'bad images for testing other services.')),
cfg.BoolOpt('do_secure_hash',
default=True,
help=('Is do_secure_hash enabled in glance. '
'This configuration value should be same as '
'glance-api.conf: do_secure_hash config option.')),
cfg.BoolOpt('http_store_enabled',
default=False,
help=('Is http store is enabled in glance. '
'http store needs to be mentioned either in '
'glance-api.conf: stores or in enabled_backends '
'configuration option.')),
]
network_group = cfg.OptGroup(name='network',

View File

@@ -304,3 +304,13 @@ class ImagesClient(rest_client.RestClient):
resp, _ = self.delete(url)
self.expected_success(204, resp.status)
return rest_client.ResponseBody(resp)
def add_image_location(self, image_id, url, validation_data=None):
"""Add location for specific Image."""
if not validation_data:
validation_data = {}
data = json.dumps({'url': url, 'validation_data': validation_data})
resp, _ = self.post('images/%s/locations' % (image_id),
data)
self.expected_success(202, resp.status)
return rest_client.ResponseBody(resp)