Support image signature verification
Add image signature verification support when creating from image. Change-Id: I37b7a795da18e3ddb18e9f293a9c795e207e7b7e Partial-Implements: bp cinder-support-image-signing
This commit is contained in:
parent
7d6df90ee3
commit
e8c24577b8
@ -439,6 +439,15 @@ class InvalidImageRef(Invalid):
|
|||||||
message = _("Invalid image href %(image_href)s.")
|
message = _("Invalid image href %(image_href)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSignatureImage(Invalid):
|
||||||
|
message = _("Signature metadata is incomplete for image: "
|
||||||
|
"%(image_id)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class ImageSignatureVerificationException(CinderException):
|
||||||
|
message = _("Failed to verify image signature, reason: %(reason)s.")
|
||||||
|
|
||||||
|
|
||||||
class ImageNotFound(NotFound):
|
class ImageNotFound(NotFound):
|
||||||
message = _("Image %(image_id)s could not be found.")
|
message = _("Image %(image_id)s could not be found.")
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import itertools
|
|||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import glanceclient.exc
|
import glanceclient.exc
|
||||||
@ -47,6 +48,27 @@ glance_opts = [
|
|||||||
help='A list of url schemes that can be downloaded directly '
|
help='A list of url schemes that can be downloaded directly '
|
||||||
'via the direct_url. Currently supported schemes: '
|
'via the direct_url. Currently supported schemes: '
|
||||||
'[file, cinder].'),
|
'[file, cinder].'),
|
||||||
|
cfg.StrOpt('verify_glance_signatures',
|
||||||
|
choices=['disabled', 'enabled'],
|
||||||
|
default='enabled',
|
||||||
|
help=textwrap.dedent(
|
||||||
|
"""
|
||||||
|
Enable image signature verification.
|
||||||
|
|
||||||
|
Cinder uses the image signature metadata from Glance and
|
||||||
|
verifies the signature of a signed image while downloading
|
||||||
|
that image. There are two options here.
|
||||||
|
|
||||||
|
1. ``enabled``: verify when image has signature metadata.
|
||||||
|
2. ``disabled``: verification is turned off.
|
||||||
|
|
||||||
|
If the image signature cannot be verified or if the image
|
||||||
|
signature metadata is incomplete when required, then Cinder
|
||||||
|
will not create the volume and update it into an error
|
||||||
|
state. This provides end users with stronger assurances
|
||||||
|
of the integrity of the image data they are using to
|
||||||
|
create volumes.
|
||||||
|
""")),
|
||||||
cfg.StrOpt('glance_catalog_info',
|
cfg.StrOpt('glance_catalog_info',
|
||||||
default='image:glance:publicURL',
|
default='image:glance:publicURL',
|
||||||
help='Info to match when looking for glance in the service '
|
help='Info to match when looking for glance in the service '
|
||||||
|
@ -31,6 +31,9 @@ import os
|
|||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
import cryptography
|
||||||
|
from cursive import exception as cursive_exception
|
||||||
|
from cursive import signature_utils
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
@ -39,6 +42,7 @@ from oslo_utils import imageutils
|
|||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from oslo_utils import units
|
from oslo_utils import units
|
||||||
import psutil
|
import psutil
|
||||||
|
import six
|
||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
@ -284,6 +288,74 @@ def resize_image(source, size, run_as_root=False):
|
|||||||
utils.execute(*cmd, run_as_root=run_as_root)
|
utils.execute(*cmd, run_as_root=run_as_root)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_glance_image_signature(context, image_service, image_id, path):
|
||||||
|
verifier = None
|
||||||
|
image_meta = image_service.show(context, image_id)
|
||||||
|
image_properties = image_meta.get('properties', {})
|
||||||
|
img_signature = image_properties.get('img_signature')
|
||||||
|
img_sig_hash_method = image_properties.get('img_signature_hash_method')
|
||||||
|
img_sig_cert_uuid = image_properties.get('img_signature_certificate_uuid')
|
||||||
|
img_sig_key_type = image_properties.get('img_signature_key_type')
|
||||||
|
if all(m is None for m in [img_signature,
|
||||||
|
img_sig_cert_uuid,
|
||||||
|
img_sig_hash_method,
|
||||||
|
img_sig_key_type]):
|
||||||
|
# NOTE(tommylikehu): We won't verify the image signature
|
||||||
|
# if none of the signature metadata presents.
|
||||||
|
return False
|
||||||
|
if any(m is None for m in [img_signature,
|
||||||
|
img_sig_cert_uuid,
|
||||||
|
img_sig_hash_method,
|
||||||
|
img_sig_key_type]):
|
||||||
|
LOG.error('Image signature metadata for image %s is '
|
||||||
|
'incomplete.', image_id)
|
||||||
|
raise exception.InvalidSignatureImage(image_id=image_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
verifier = signature_utils.get_verifier(
|
||||||
|
context=context,
|
||||||
|
img_signature_certificate_uuid=img_sig_cert_uuid,
|
||||||
|
img_signature_hash_method=img_sig_hash_method,
|
||||||
|
img_signature=img_signature,
|
||||||
|
img_signature_key_type=img_sig_key_type,
|
||||||
|
)
|
||||||
|
except cursive_exception.SignatureVerificationError:
|
||||||
|
message = _('Failed to get verifier for image: %s') % image_id
|
||||||
|
LOG.error(message)
|
||||||
|
raise exception.ImageSignatureVerificationException(
|
||||||
|
reason=message)
|
||||||
|
if verifier:
|
||||||
|
with fileutils.remove_path_on_error(path):
|
||||||
|
with open(path, "rb") as tem_file:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
chunk = tem_file.read(1024)
|
||||||
|
if chunk:
|
||||||
|
verifier.update(chunk)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
verifier.verify()
|
||||||
|
LOG.info('Image signature verification succeeded '
|
||||||
|
'for image: %s', image_id)
|
||||||
|
return True
|
||||||
|
except cryptography.exceptions.InvalidSignature:
|
||||||
|
message = _('Image signature verification '
|
||||||
|
'failed for image: %s') % image_id
|
||||||
|
LOG.error(message)
|
||||||
|
raise exception.ImageSignatureVerificationException(
|
||||||
|
reason=message)
|
||||||
|
except Exception as ex:
|
||||||
|
message = _('Failed to verify signature for '
|
||||||
|
'image: %(image)s due to '
|
||||||
|
'error: %(error)s ') % {'image': image_id,
|
||||||
|
'error':
|
||||||
|
six.text_type(ex)}
|
||||||
|
LOG.error(message)
|
||||||
|
raise exception.ImageSignatureVerificationException(
|
||||||
|
reason=message)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def fetch(context, image_service, image_id, path, _user_id, _project_id):
|
def fetch(context, image_service, image_id, path, _user_id, _project_id):
|
||||||
# TODO(vish): Improve context handling and add owner and auth data
|
# TODO(vish): Improve context handling and add owner and auth data
|
||||||
# when it is added to glance. Right now there is no
|
# when it is added to glance. Right now there is no
|
||||||
|
@ -76,6 +76,9 @@ class Detail(object):
|
|||||||
DRIVER_FAILED_EXTEND = (
|
DRIVER_FAILED_EXTEND = (
|
||||||
'010',
|
'010',
|
||||||
_("Volume Driver failed to extend volume."))
|
_("Volume Driver failed to extend volume."))
|
||||||
|
SIGNATURE_VERIFICATION_FAILED = (
|
||||||
|
'011',
|
||||||
|
_("Image signature verification failed."))
|
||||||
|
|
||||||
ALL = (UNKNOWN_ERROR,
|
ALL = (UNKNOWN_ERROR,
|
||||||
DRIVER_NOT_INITIALIZED,
|
DRIVER_NOT_INITIALIZED,
|
||||||
@ -86,8 +89,8 @@ class Detail(object):
|
|||||||
NOT_ENOUGH_SPACE_FOR_IMAGE,
|
NOT_ENOUGH_SPACE_FOR_IMAGE,
|
||||||
UNMANAGE_ENC_NOT_SUPPORTED,
|
UNMANAGE_ENC_NOT_SUPPORTED,
|
||||||
NOTIFY_COMPUTE_SERVICE_FAILED,
|
NOTIFY_COMPUTE_SERVICE_FAILED,
|
||||||
DRIVER_FAILED_EXTEND
|
DRIVER_FAILED_EXTEND,
|
||||||
)
|
SIGNATURE_VERIFICATION_FAILED)
|
||||||
|
|
||||||
# Exception and detail mappings
|
# Exception and detail mappings
|
||||||
EXCEPTION_DETAIL_MAPPINGS = {
|
EXCEPTION_DETAIL_MAPPINGS = {
|
||||||
|
@ -14,13 +14,15 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
"""Unit tests for image utils."""
|
"""Unit tests for image utils."""
|
||||||
|
|
||||||
import ddt
|
|
||||||
import errno
|
import errno
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
import cryptography
|
||||||
|
import ddt
|
||||||
import mock
|
import mock
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_utils import units
|
from oslo_utils import units
|
||||||
|
from six.moves import builtins
|
||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.image import image_utils
|
from cinder.image import image_utils
|
||||||
@ -345,6 +347,132 @@ class TestFetch(test.TestCase):
|
|||||||
_user_id, _project_id)
|
_user_id, _project_id)
|
||||||
|
|
||||||
|
|
||||||
|
class MockVerifier(object):
|
||||||
|
def update(self, data):
|
||||||
|
return
|
||||||
|
|
||||||
|
def verify(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class BadVerifier(object):
|
||||||
|
def update(self, data):
|
||||||
|
return
|
||||||
|
|
||||||
|
def verify(self):
|
||||||
|
raise cryptography.exceptions.InvalidSignature(
|
||||||
|
'Invalid signature.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerifyImageSignature(test.TestCase):
|
||||||
|
|
||||||
|
@mock.patch('cursive.signature_utils.get_verifier')
|
||||||
|
@mock.patch('oslo_utils.fileutils.remove_path_on_error')
|
||||||
|
def test_image_signature_verify_failed(self, mock_remove, mock_get):
|
||||||
|
self.mock_object(builtins, 'open', mock.mock_open())
|
||||||
|
ctxt = mock.sentinel.context
|
||||||
|
metadata = {'name': 'test image',
|
||||||
|
'is_public': False,
|
||||||
|
'protected': False,
|
||||||
|
'properties':
|
||||||
|
{'img_signature_certificate_uuid': 'fake_uuid',
|
||||||
|
'img_signature_hash_method': 'SHA-256',
|
||||||
|
'img_signature': 'signature',
|
||||||
|
'img_signature_key_type': 'RSA-PSS'}}
|
||||||
|
|
||||||
|
class FakeImageService(object):
|
||||||
|
def show(self, context, image_id):
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
self.flags(verify_glance_signatures='enabled')
|
||||||
|
mock_get.return_value = BadVerifier()
|
||||||
|
|
||||||
|
self.assertRaises(exception.ImageSignatureVerificationException,
|
||||||
|
image_utils.verify_glance_image_signature,
|
||||||
|
ctxt, FakeImageService(), 'fake_id',
|
||||||
|
'fake_path')
|
||||||
|
mock_get.assert_called_once_with(
|
||||||
|
context=ctxt,
|
||||||
|
img_signature_certificate_uuid='fake_uuid',
|
||||||
|
img_signature_hash_method='SHA-256',
|
||||||
|
img_signature='signature',
|
||||||
|
img_signature_key_type='RSA-PSS')
|
||||||
|
|
||||||
|
@mock.patch('cursive.signature_utils.get_verifier')
|
||||||
|
def test_image_signature_metadata_missing(self, mock_get):
|
||||||
|
ctxt = mock.sentinel.context
|
||||||
|
metadata = {'name': 'test image',
|
||||||
|
'is_public': False,
|
||||||
|
'protected': False,
|
||||||
|
'properties': {}}
|
||||||
|
|
||||||
|
class FakeImageService(object):
|
||||||
|
def show(self, context, image_id):
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
self.flags(verify_glance_signatures='enabled')
|
||||||
|
|
||||||
|
result = image_utils.verify_glance_image_signature(
|
||||||
|
ctxt, FakeImageService(), 'fake_id', 'fake_path')
|
||||||
|
self.assertFalse(result)
|
||||||
|
mock_get.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch('cursive.signature_utils.get_verifier')
|
||||||
|
def test_image_signature_metadata_incomplete(self, mock_get):
|
||||||
|
ctxt = mock.sentinel.context
|
||||||
|
metadata = {'name': 'test image',
|
||||||
|
'is_public': False,
|
||||||
|
'protected': False,
|
||||||
|
'properties':
|
||||||
|
{'img_signature_certificate_uuid': None,
|
||||||
|
'img_signature_hash_method': 'SHA-256',
|
||||||
|
'img_signature': 'signature',
|
||||||
|
'img_signature_key_type': 'RSA-PSS'}}
|
||||||
|
|
||||||
|
class FakeImageService(object):
|
||||||
|
def show(self, context, image_id):
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
self.flags(verify_glance_signatures='enabled')
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidSignatureImage,
|
||||||
|
image_utils.verify_glance_image_signature, ctxt,
|
||||||
|
FakeImageService(), 'fake_id', 'fake_path')
|
||||||
|
mock_get.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch('cursive.signature_utils.get_verifier')
|
||||||
|
@mock.patch('oslo_utils.fileutils.remove_path_on_error')
|
||||||
|
def test_image_signature_verify_success(self, mock_remove, mock_get):
|
||||||
|
self.mock_object(builtins, 'open', mock.mock_open())
|
||||||
|
ctxt = mock.sentinel.context
|
||||||
|
metadata = {'name': 'test image',
|
||||||
|
'is_public': False,
|
||||||
|
'protected': False,
|
||||||
|
'properties':
|
||||||
|
{'img_signature_certificate_uuid': 'fake_uuid',
|
||||||
|
'img_signature_hash_method': 'SHA-256',
|
||||||
|
'img_signature': 'signature',
|
||||||
|
'img_signature_key_type': 'RSA-PSS'}}
|
||||||
|
|
||||||
|
class FakeImageService(object):
|
||||||
|
def show(self, context, image_id):
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
self.flags(verify_glance_signatures='enabled')
|
||||||
|
mock_get.return_value = MockVerifier()
|
||||||
|
|
||||||
|
result = image_utils.verify_glance_image_signature(
|
||||||
|
ctxt, FakeImageService(), 'fake_id', 'fake_path')
|
||||||
|
self.assertTrue(result)
|
||||||
|
mock_get.assert_called_once_with(
|
||||||
|
context=ctxt,
|
||||||
|
img_signature_certificate_uuid='fake_uuid',
|
||||||
|
img_signature_hash_method='SHA-256',
|
||||||
|
img_signature='signature',
|
||||||
|
img_signature_key_type='RSA-PSS')
|
||||||
|
|
||||||
|
|
||||||
class TestVerifyImage(test.TestCase):
|
class TestVerifyImage(test.TestCase):
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
@mock.patch('cinder.image.image_utils.fileutils')
|
@mock.patch('cinder.image.image_utils.fileutils')
|
||||||
|
@ -2227,8 +2227,10 @@ class ManagedRBDTestCase(test_driver.BaseDriverTestCase):
|
|||||||
@mock.patch.object(cinder.image.glance, 'get_default_image_service')
|
@mock.patch.object(cinder.image.glance, 'get_default_image_service')
|
||||||
@mock.patch('cinder.image.image_utils.TemporaryImages.fetch')
|
@mock.patch('cinder.image.image_utils.TemporaryImages.fetch')
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
|
@mock.patch('cinder.image.image_utils.verify_glance_image_signature')
|
||||||
def test_create_vol_from_non_raw_image_status_available(
|
def test_create_vol_from_non_raw_image_status_available(
|
||||||
self, mock_qemu_info, mock_fetch, mock_gdis, mock_check_space):
|
self, mock_verify, mock_qemu_info, mock_fetch, mock_gdis,
|
||||||
|
mock_check_space):
|
||||||
"""Clone non-raw image then verify volume is in available state."""
|
"""Clone non-raw image then verify volume is in available state."""
|
||||||
|
|
||||||
def _mock_clone_image(context, volume, image_location,
|
def _mock_clone_image(context, volume, image_location,
|
||||||
@ -2238,6 +2240,7 @@ class ManagedRBDTestCase(test_driver.BaseDriverTestCase):
|
|||||||
image_info = imageutils.QemuImgInfo()
|
image_info = imageutils.QemuImgInfo()
|
||||||
image_info.virtual_size = '1073741824'
|
image_info.virtual_size = '1073741824'
|
||||||
mock_qemu_info.return_value = image_info
|
mock_qemu_info.return_value = image_info
|
||||||
|
self.flags(verify_glance_signatures='disabled')
|
||||||
|
|
||||||
mock_fetch.return_value = mock.MagicMock(spec=utils.get_file_spec())
|
mock_fetch.return_value = mock.MagicMock(spec=utils.get_file_spec())
|
||||||
with mock.patch.object(self.volume.driver, 'clone_image') as \
|
with mock.patch.object(self.volume.driver, 'clone_image') as \
|
||||||
|
@ -1408,12 +1408,15 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
|
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
@mock.patch('cinder.image.image_utils.check_available_space')
|
@mock.patch('cinder.image.image_utils.check_available_space')
|
||||||
|
@mock.patch('cinder.image.image_utils.verify_glance_image_signature')
|
||||||
def test_create_from_image_cannot_use_cache(
|
def test_create_from_image_cannot_use_cache(
|
||||||
self, mock_qemu_info, mock_check_space, mock_get_internal_context,
|
self, mock_verify, mock_qemu_info, mock_check_space,
|
||||||
|
mock_get_internal_context,
|
||||||
mock_create_from_img_dl, mock_create_from_src,
|
mock_create_from_img_dl, mock_create_from_src,
|
||||||
mock_handle_bootable, mock_fetch_img):
|
mock_handle_bootable, mock_fetch_img):
|
||||||
mock_get_internal_context.return_value = None
|
mock_get_internal_context.return_value = None
|
||||||
self.mock_driver.clone_image.return_value = (None, False)
|
self.mock_driver.clone_image.return_value = (None, False)
|
||||||
|
self.flags(verify_glance_signatures='disabled')
|
||||||
volume = fake_volume.fake_volume_obj(self.ctxt,
|
volume = fake_volume.fake_volume_obj(self.ctxt,
|
||||||
host='host@backend#pool')
|
host='host@backend#pool')
|
||||||
image_info = imageutils.QemuImgInfo()
|
image_info = imageutils.QemuImgInfo()
|
||||||
@ -1509,8 +1512,10 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
@mock.patch('cinder.image.image_utils.check_available_space')
|
@mock.patch('cinder.image.image_utils.check_available_space')
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
@mock.patch('cinder.db.volume_update')
|
@mock.patch('cinder.db.volume_update')
|
||||||
|
@mock.patch('cinder.image.image_utils.verify_glance_image_signature')
|
||||||
def test_create_from_image_extend_failure(
|
def test_create_from_image_extend_failure(
|
||||||
self, mock_volume_update, mock_qemu_info, mock_check_size,
|
self, mock_verify, mock_volume_update, mock_qemu_info,
|
||||||
|
mock_check_size,
|
||||||
mock_get_internal_context, mock_create_from_img_dl,
|
mock_get_internal_context, mock_create_from_img_dl,
|
||||||
mock_create_from_src, mock_handle_bootable, mock_fetch_img,
|
mock_create_from_src, mock_handle_bootable, mock_fetch_img,
|
||||||
mock_cleanup_cg):
|
mock_cleanup_cg):
|
||||||
@ -1518,6 +1523,7 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
self.mock_cache.get_entry.return_value = None
|
self.mock_cache.get_entry.return_value = None
|
||||||
self.mock_driver.extend_volume.side_effect = (
|
self.mock_driver.extend_volume.side_effect = (
|
||||||
exception.CinderException('Error during extending'))
|
exception.CinderException('Error during extending'))
|
||||||
|
self.flags(verify_glance_signatures='disabled')
|
||||||
|
|
||||||
volume_size = 2
|
volume_size = 2
|
||||||
volume = fake_volume.fake_volume_obj(self.ctxt,
|
volume = fake_volume.fake_volume_obj(self.ctxt,
|
||||||
@ -1632,14 +1638,16 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
@mock.patch('cinder.objects.Volume.get_by_id')
|
@mock.patch('cinder.objects.Volume.get_by_id')
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
@mock.patch('cinder.image.image_utils.check_available_space')
|
@mock.patch('cinder.image.image_utils.check_available_space')
|
||||||
|
@mock.patch('cinder.image.image_utils.verify_glance_image_signature')
|
||||||
def test_create_from_image_cache_miss(
|
def test_create_from_image_cache_miss(
|
||||||
self, mock_check_size, mock_qemu_info, mock_volume_get,
|
self, mocl_verify, mock_check_size, mock_qemu_info,
|
||||||
mock_volume_update, mock_get_internal_context,
|
mock_volume_get, mock_volume_update, mock_get_internal_context,
|
||||||
mock_create_from_img_dl, mock_create_from_src,
|
mock_create_from_img_dl, mock_create_from_src,
|
||||||
mock_handle_bootable, mock_fetch_img):
|
mock_handle_bootable, mock_fetch_img):
|
||||||
mock_get_internal_context.return_value = self.ctxt
|
mock_get_internal_context.return_value = self.ctxt
|
||||||
mock_fetch_img.return_value = mock.MagicMock(
|
mock_fetch_img.return_value = mock.MagicMock(
|
||||||
spec=utils.get_file_spec())
|
spec=utils.get_file_spec())
|
||||||
|
self.flags(verify_glance_signatures='disabled')
|
||||||
image_info = imageutils.QemuImgInfo()
|
image_info = imageutils.QemuImgInfo()
|
||||||
image_info.virtual_size = '2147483648'
|
image_info.virtual_size = '2147483648'
|
||||||
mock_qemu_info.return_value = image_info
|
mock_qemu_info.return_value = image_info
|
||||||
@ -1703,9 +1711,10 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
@mock.patch('cinder.objects.Volume.get_by_id')
|
@mock.patch('cinder.objects.Volume.get_by_id')
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
@mock.patch('cinder.image.image_utils.check_available_space')
|
@mock.patch('cinder.image.image_utils.check_available_space')
|
||||||
|
@mock.patch('cinder.image.image_utils.verify_glance_image_signature')
|
||||||
def test_create_from_image_cache_miss_error_downloading(
|
def test_create_from_image_cache_miss_error_downloading(
|
||||||
self, mock_check_size, mock_qemu_info, mock_volume_get,
|
self, mock_verify, mock_check_size, mock_qemu_info,
|
||||||
mock_volume_update, mock_get_internal_context,
|
mock_volume_get, mock_volume_update, mock_get_internal_context,
|
||||||
mock_create_from_img_dl, mock_create_from_src,
|
mock_create_from_img_dl, mock_create_from_src,
|
||||||
mock_handle_bootable, mock_fetch_img):
|
mock_handle_bootable, mock_fetch_img):
|
||||||
mock_fetch_img.return_value = mock.MagicMock()
|
mock_fetch_img.return_value = mock.MagicMock()
|
||||||
@ -1714,6 +1723,7 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
mock_qemu_info.return_value = image_info
|
mock_qemu_info.return_value = image_info
|
||||||
self.mock_driver.clone_image.return_value = (None, False)
|
self.mock_driver.clone_image.return_value = (None, False)
|
||||||
self.mock_cache.get_entry.return_value = None
|
self.mock_cache.get_entry.return_value = None
|
||||||
|
self.flags(verify_glance_signatures='disabled')
|
||||||
|
|
||||||
volume = fake_volume.fake_volume_obj(self.ctxt, size=10,
|
volume = fake_volume.fake_volume_obj(self.ctxt, size=10,
|
||||||
host='foo@bar#pool')
|
host='foo@bar#pool')
|
||||||
@ -1769,12 +1779,15 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
|
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
@mock.patch('cinder.image.image_utils.check_available_space')
|
@mock.patch('cinder.image.image_utils.check_available_space')
|
||||||
|
@mock.patch('cinder.image.image_utils.verify_glance_image_signature')
|
||||||
def test_create_from_image_no_internal_context(
|
def test_create_from_image_no_internal_context(
|
||||||
self, mock_chk_space, mock_qemu_info, mock_get_internal_context,
|
self, mock_verify, mock_chk_space, mock_qemu_info,
|
||||||
|
mock_get_internal_context,
|
||||||
mock_create_from_img_dl, mock_create_from_src,
|
mock_create_from_img_dl, mock_create_from_src,
|
||||||
mock_handle_bootable, mock_fetch_img):
|
mock_handle_bootable, mock_fetch_img):
|
||||||
self.mock_driver.clone_image.return_value = (None, False)
|
self.mock_driver.clone_image.return_value = (None, False)
|
||||||
mock_get_internal_context.return_value = None
|
mock_get_internal_context.return_value = None
|
||||||
|
self.flags(verify_glance_signatures='disabled')
|
||||||
volume = fake_volume.fake_volume_obj(self.ctxt,
|
volume = fake_volume.fake_volume_obj(self.ctxt,
|
||||||
host='host@backend#pool')
|
host='host@backend#pool')
|
||||||
image_info = imageutils.QemuImgInfo()
|
image_info = imageutils.QemuImgInfo()
|
||||||
@ -1837,8 +1850,10 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
'_cleanup_cg_in_volume')
|
'_cleanup_cg_in_volume')
|
||||||
@mock.patch('cinder.image.image_utils.check_available_space')
|
@mock.patch('cinder.image.image_utils.check_available_space')
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
|
@mock.patch('cinder.image.image_utils.verify_glance_image_signature')
|
||||||
def test_create_from_image_cache_miss_error_size_invalid(
|
def test_create_from_image_cache_miss_error_size_invalid(
|
||||||
self, mock_qemu_info, mock_check_space, mock_get_internal_context,
|
self, mock_verify, mock_qemu_info, mock_check_space,
|
||||||
|
mock_get_internal_context,
|
||||||
mock_create_from_img_dl, mock_create_from_src,
|
mock_create_from_img_dl, mock_create_from_src,
|
||||||
mock_handle_bootable, mock_fetch_img, mock_cleanup_cg):
|
mock_handle_bootable, mock_fetch_img, mock_cleanup_cg):
|
||||||
mock_fetch_img.return_value = mock.MagicMock()
|
mock_fetch_img.return_value = mock.MagicMock()
|
||||||
@ -1847,6 +1862,7 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
mock_qemu_info.return_value = image_info
|
mock_qemu_info.return_value = image_info
|
||||||
self.mock_driver.clone_image.return_value = (None, False)
|
self.mock_driver.clone_image.return_value = (None, False)
|
||||||
self.mock_cache.get_entry.return_value = None
|
self.mock_cache.get_entry.return_value = None
|
||||||
|
self.flags(verify_glance_signatures='disabled')
|
||||||
|
|
||||||
volume = fake_volume.fake_volume_obj(self.ctxt, size=1,
|
volume = fake_volume.fake_volume_obj(self.ctxt, size=1,
|
||||||
host='foo@bar#pool')
|
host='foo@bar#pool')
|
||||||
@ -1943,8 +1959,10 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
@mock.patch('cinder.image.image_utils.check_available_space')
|
@mock.patch('cinder.image.image_utils.check_available_space')
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
@mock.patch('cinder.message.api.API.create')
|
@mock.patch('cinder.message.api.API.create')
|
||||||
|
@mock.patch('cinder.image.image_utils.verify_glance_image_signature')
|
||||||
def test_create_from_image_cache_insufficient_size(
|
def test_create_from_image_cache_insufficient_size(
|
||||||
self, mock_message_create, mock_qemu_info, mock_check_space,
|
self, mock_verify, mock_message_create, mock_qemu_info,
|
||||||
|
mock_check_space,
|
||||||
mock_get_internal_context,
|
mock_get_internal_context,
|
||||||
mock_create_from_img_dl, mock_create_from_src,
|
mock_create_from_img_dl, mock_create_from_src,
|
||||||
mock_handle_bootable, mock_fetch_img):
|
mock_handle_bootable, mock_fetch_img):
|
||||||
@ -1960,6 +1978,7 @@ class CreateVolumeFlowManagerImageCacheTestCase(test.TestCase):
|
|||||||
image_id = fakes.IMAGE_ID
|
image_id = fakes.IMAGE_ID
|
||||||
mock_create_from_img_dl.side_effect = exception.ImageTooBig(
|
mock_create_from_img_dl.side_effect = exception.ImageTooBig(
|
||||||
image_id=image_id, reason="fake")
|
image_id=image_id, reason="fake")
|
||||||
|
self.flags(verify_glance_signatures='disabled')
|
||||||
|
|
||||||
image_location = 'someImageLocationStr'
|
image_location = 'someImageLocationStr'
|
||||||
image_meta = mock.MagicMock()
|
image_meta = mock.MagicMock()
|
||||||
|
@ -98,6 +98,8 @@ class OnFailureRescheduleTask(flow_utils.CinderTask):
|
|||||||
exception.VolumeTypeNotFound,
|
exception.VolumeTypeNotFound,
|
||||||
exception.ImageUnacceptable,
|
exception.ImageUnacceptable,
|
||||||
exception.ImageTooBig,
|
exception.ImageTooBig,
|
||||||
|
exception.InvalidSignatureImage,
|
||||||
|
exception.ImageSignatureVerificationException
|
||||||
]
|
]
|
||||||
|
|
||||||
def execute(self, **kwargs):
|
def execute(self, **kwargs):
|
||||||
@ -811,6 +813,17 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
|
|||||||
with image_utils.TemporaryImages.fetch(
|
with image_utils.TemporaryImages.fetch(
|
||||||
image_service, context, image_id,
|
image_service, context, image_id,
|
||||||
backend_name) as tmp_image:
|
backend_name) as tmp_image:
|
||||||
|
if CONF.verify_glance_signatures != 'disabled':
|
||||||
|
# Verify image signature via reading content from
|
||||||
|
# temp image, and store the verification flag if
|
||||||
|
# required.
|
||||||
|
verified = \
|
||||||
|
image_utils.verify_glance_image_signature(
|
||||||
|
context, image_service,
|
||||||
|
image_id, tmp_image)
|
||||||
|
self.db.volume_glance_metadata_bulk_create(
|
||||||
|
context, volume.id,
|
||||||
|
{'signature_verified': verified})
|
||||||
# Try to create the volume as the minimal size,
|
# Try to create the volume as the minimal size,
|
||||||
# then we can extend once the image has been
|
# then we can extend once the image has been
|
||||||
# downloaded.
|
# downloaded.
|
||||||
@ -839,6 +852,15 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
|
|||||||
detail=
|
detail=
|
||||||
message_field.Detail.NOT_ENOUGH_SPACE_FOR_IMAGE,
|
message_field.Detail.NOT_ENOUGH_SPACE_FOR_IMAGE,
|
||||||
exception=e)
|
exception=e)
|
||||||
|
except exception.ImageSignatureVerificationException as err:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
self.message.create(
|
||||||
|
context,
|
||||||
|
message_field.Action.COPY_IMAGE_TO_VOLUME,
|
||||||
|
resource_uuid=volume.id,
|
||||||
|
detail=
|
||||||
|
message_field.Detail.SIGNATURE_VERIFICATION_FAILED,
|
||||||
|
exception=err)
|
||||||
|
|
||||||
if should_create_cache_entry:
|
if should_create_cache_entry:
|
||||||
# Update the newly created volume db entry before we clone it
|
# Update the newly created volume db entry before we clone it
|
||||||
|
@ -17,6 +17,7 @@ cmd2==0.8.1
|
|||||||
contextlib2==0.5.5
|
contextlib2==0.5.5
|
||||||
coverage==4.0
|
coverage==4.0
|
||||||
cryptography==2.1
|
cryptography==2.1
|
||||||
|
cursive==0.2.1
|
||||||
ddt==1.0.1
|
ddt==1.0.1
|
||||||
debtcollector==1.19.0
|
debtcollector==1.19.0
|
||||||
decorator==3.4.0
|
decorator==3.4.0
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added image signature verification support when creating volume
|
||||||
|
from image. This depends on signature metadata from glance.
|
||||||
|
This feature is turned on by default, administrators can
|
||||||
|
change behaviour by updating option ``verify_glance_signatures``.
|
||||||
|
Also, an additional image metadata ``signature_verified`` has
|
||||||
|
been added to indicate whether signature verification was performed
|
||||||
|
during creating process.
|
@ -65,3 +65,4 @@ tooz>=1.58.0 # Apache-2.0
|
|||||||
google-api-python-client>=1.4.2 # Apache-2.0
|
google-api-python-client>=1.4.2 # Apache-2.0
|
||||||
castellan>=0.16.0 # Apache-2.0
|
castellan>=0.16.0 # Apache-2.0
|
||||||
cryptography>=2.1 # BSD/Apache-2.0
|
cryptography>=2.1 # BSD/Apache-2.0
|
||||||
|
cursive>=0.2.1 # Apache-2.0
|
Loading…
x
Reference in New Issue
Block a user