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:
@@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add new location API support to image V2 client
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user