Test glance hash calculation stops on image deletion

Recently glance has added new location API which also calculates
checksum and hash for the newly added image. This test helps
use to verify that hash calculation process is stopped when
image is deleted from same or remote glance server.

Depends-On: https://review.opendev.org/c/openstack/glance/+/950853
Change-Id: I671f67a99f0ecae00601be02fbf6805b692a366c
This commit is contained in:
Abhishek Kekane
2025-06-02 18:54:48 +00:00
parent 905cb14d8d
commit 102d762ea3
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)