Merge "Test glance hash calculation stops on image deletion"
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