summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--nova/compute/api.py18
-rw-r--r--nova/tests/unit/compute/test_api.py57
-rw-r--r--nova/tests/unit/virt/disk/vfs/fakeguestfs.py2
-rw-r--r--nova/tests/unit/virt/libvirt/test_driver.py43
-rw-r--r--nova/tests/unit/virt/libvirt/test_guest.py42
-rw-r--r--nova/virt/disk/api.py4
-rw-r--r--nova/virt/disk/vfs/guestfs.py9
-rw-r--r--nova/virt/libvirt/driver.py18
-rw-r--r--nova/virt/libvirt/guest.py19
9 files changed, 173 insertions, 39 deletions
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 9c47d03989..f4d8900b83 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -5483,11 +5483,25 @@ class API(base.Base):
bdm = self._get_bdm_by_volume_id(
context, volume_id, expected_attrs=['instance'])
- # We allow deleting the snapshot in any vm_state as long as there is
- # no task being performed on the instance and it has a host.
@check_instance_host()
@check_instance_state(vm_state=None)
def do_volume_snapshot_delete(self, context, instance):
+ # FIXME(lyarwood): Avoid bug #1919487 by rejecting the request
+ # to delete an intermediary volume snapshot offline as this isn't
+ # currently implemented within the libvirt driver and will fail.
+ # This should be fixed in a future release but as it is essentially
+ # a new feature wouldn't be something we could backport. As such
+ # reject the request here so n-api can respond correctly to c-vol.
+ if (delete_info.get('merge_target_file') is not None and
+ instance.vm_state != vm_states.ACTIVE
+ ):
+ raise exception.InstanceInvalidState(
+ attr='vm_state',
+ instance_uuid=instance.uuid,
+ state=instance.vm_state,
+ method='volume_snapshot_delete'
+ )
+
self.compute_rpcapi.volume_snapshot_delete(context, instance,
volume_id, snapshot_id, delete_info)
diff --git a/nova/tests/unit/compute/test_api.py b/nova/tests/unit/compute/test_api.py
index 2356161f69..822bfc1a15 100644
--- a/nova/tests/unit/compute/test_api.py
+++ b/nova/tests/unit/compute/test_api.py
@@ -3601,23 +3601,29 @@ class _ComputeAPIUnitTestMixIn(object):
'boot_index': -1})
fake_bdm['instance'] = fake_instance.fake_db_instance(
launched_at=timeutils.utcnow(),
- vm_state=vm_states.STOPPED)
+ vm_state=vm_states.ACTIVE)
fake_bdm['instance_uuid'] = fake_bdm['instance']['uuid']
fake_bdm = objects.BlockDeviceMapping._from_db_object(
self.context, objects.BlockDeviceMapping(),
fake_bdm, expected_attrs=['instance'])
mock_get_bdm.return_value = fake_bdm
+ delete_info = {
+ 'merge_target_file': 'foo.qcow2',
+ }
- with mock.patch.object(self.compute_api.compute_rpcapi,
- 'volume_snapshot_delete') as mock_snapshot:
- self.compute_api.volume_snapshot_delete(self.context, volume_id,
- snapshot_id, {})
-
+ with mock.patch.object(
+ self.compute_api.compute_rpcapi, 'volume_snapshot_delete'
+ ) as mock_snapshot:
+ self.compute_api.volume_snapshot_delete(
+ self.context, volume_id, snapshot_id, delete_info
+ )
mock_get_bdm.assert_called_once_with(self.context, volume_id,
expected_attrs=['instance'])
mock_snapshot.assert_called_once_with(
- self.context, fake_bdm['instance'], volume_id, snapshot_id, {})
+ self.context, fake_bdm['instance'], volume_id, snapshot_id,
+ delete_info
+ )
@mock.patch.object(
objects.BlockDeviceMapping, 'get_by_volume',
@@ -3651,6 +3657,43 @@ class _ComputeAPIUnitTestMixIn(object):
self.context, mock.sentinel.volume_id,
mock.sentinel.snapshot_id, mock.sentinel.delete_info)
+ @mock.patch.object(compute_api.API, '_get_bdm_by_volume_id')
+ def test_volume_snapshot_delete_intermediary_commit(self, mock_get_bdm):
+ fake_bdm = fake_block_device.FakeDbBlockDeviceDict({
+ 'id': 123,
+ 'device_name': '/dev/sda2',
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'connection_info': "{'fake': 'connection_info'}",
+ 'volume_id': uuids.volume_id,
+ 'boot_index': -1
+ })
+ fake_bdm['instance'] = fake_instance.fake_db_instance(
+ launched_at=timeutils.utcnow(),
+ vm_state=vm_states.STOPPED)
+ fake_bdm['instance_uuid'] = fake_bdm['instance']['uuid']
+ fake_bdm = objects.BlockDeviceMapping._from_db_object(
+ self.context, objects.BlockDeviceMapping(),
+ fake_bdm, expected_attrs=['instance'])
+ mock_get_bdm.return_value = fake_bdm
+
+ # c-vol can provide delete_info with merge_target_file pointing to
+ # an intermediary snapshot to commit into it's base. This is only
+ # supported while the instance is running at present.
+ delete_info = {
+ 'merge_target_file': 'snap.img'
+ }
+
+ # Assert that the request is rejected as offline commit isn't supported
+ self.assertRaises(
+ exception.InstanceInvalidState,
+ self.compute_api.volume_snapshot_delete,
+ self.context,
+ uuids.volume_id,
+ uuids.snapshot_id,
+ delete_info
+ )
+
def _create_instance_with_disabled_disk_config(self, object=False):
sys_meta = {"image_auto_disk_config": "Disabled"}
params = {"system_metadata": sys_meta}
diff --git a/nova/tests/unit/virt/disk/vfs/fakeguestfs.py b/nova/tests/unit/virt/disk/vfs/fakeguestfs.py
index 76304e1f15..01715a9f7b 100644
--- a/nova/tests/unit/virt/disk/vfs/fakeguestfs.py
+++ b/nova/tests/unit/virt/disk/vfs/fakeguestfs.py
@@ -109,7 +109,7 @@ class GuestFS(object):
"mode": 0o700
}
- return self.files[path]["content"]
+ return bytes(self.files[path]["content"].encode())
def write(self, path, content):
if path not in self.files:
diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py
index 4e052c4865..faa166f903 100644
--- a/nova/tests/unit/virt/libvirt/test_driver.py
+++ b/nova/tests/unit/virt/libvirt/test_driver.py
@@ -10005,9 +10005,9 @@ class LibvirtConnTestCase(test.NoDBTestCase,
@mock.patch('os_brick.encryptors.get_encryption_metadata')
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_volume_encryptor')
- @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._allow_native_luksv1')
- def test_detach_encryptor_encrypted_volume_meta_missing(self,
- mock_allow_native_luksv1, mock_get_encryptor, mock_get_metadata):
+ def test_detach_encryptor_encrypted_volume_meta_missing(
+ self, mock_get_encryptor, mock_get_metadata
+ ):
"""Assert that if missing the encryption metadata of an encrypted
volume is fetched and then used to detach the encryptor for the volume.
"""
@@ -10016,8 +10016,12 @@ class LibvirtConnTestCase(test.NoDBTestCase,
mock_get_encryptor.return_value = mock_encryptor
encryption = {'provider': 'luks', 'control_location': 'front-end'}
mock_get_metadata.return_value = encryption
- connection_info = {'data': {'volume_id': uuids.volume_id}}
- mock_allow_native_luksv1.return_value = False
+ connection_info = {
+ 'data': {
+ 'device_path': '/dev/foo',
+ 'volume_id': uuids.volume_id
+ }
+ }
drvr._detach_encryptor(self.context, connection_info, None)
@@ -10029,9 +10033,11 @@ class LibvirtConnTestCase(test.NoDBTestCase,
@mock.patch('os_brick.encryptors.get_encryption_metadata')
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_volume_encryptor')
- @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._allow_native_luksv1')
- def test_detach_encryptor_encrypted_volume_meta_provided(self,
- mock_allow_native_luksv1, mock_get_encryptor, mock_get_metadata):
+ def test_detach_encryptor_encrypted_volume_meta_provided(
+ self,
+ mock_get_encryptor,
+ mock_get_metadata
+ ):
"""Assert that when provided there are no further attempts to fetch the
encryption metadata for the volume and that the provided metadata is
then used to detach the volume.
@@ -10040,8 +10046,12 @@ class LibvirtConnTestCase(test.NoDBTestCase,
mock_encryptor = mock.MagicMock()
mock_get_encryptor.return_value = mock_encryptor
encryption = {'provider': 'luks', 'control_location': 'front-end'}
- connection_info = {'data': {'volume_id': uuids.volume_id}}
- mock_allow_native_luksv1.return_value = False
+ connection_info = {
+ 'data': {
+ 'device_path': '/dev/foo',
+ 'volume_id': uuids.volume_id
+ }
+ }
drvr._detach_encryptor(self.context, connection_info, encryption)
@@ -10051,20 +10061,19 @@ class LibvirtConnTestCase(test.NoDBTestCase,
mock_encryptor.detach_volume.assert_called_once_with(**encryption)
@mock.patch('nova.virt.libvirt.host.Host.find_secret')
- @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._allow_native_luksv1')
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_volume_encryptor')
- def test_detach_encryptor_native_luks_device_path_secret_missing(self,
- mock_get_encryptor, mock_allow_native_luksv1, mock_find_secret):
- """Assert that the encryptor is not built when native LUKS is
- available, the associated volume secret is missing and device_path is
- also missing from the connection_info.
+ def test_detach_encryptor_native_luks_device_path_secret_missing(
+ self, mock_get_encryptor, mock_find_secret
+ ):
+ """Assert that the encryptor is not built when the associated volume
+ secret is missing and device_path is also missing from the
+ connection_info.
"""
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
encryption = {'provider': 'luks', 'control_location': 'front-end',
'encryption_key_id': uuids.encryption_key_id}
connection_info = {'data': {'volume_id': uuids.volume_id}}
mock_find_secret.return_value = False
- mock_allow_native_luksv1.return_value = True
drvr._detach_encryptor(self.context, connection_info, encryption)
diff --git a/nova/tests/unit/virt/libvirt/test_guest.py b/nova/tests/unit/virt/libvirt/test_guest.py
index d967470350..f09c8ae71d 100644
--- a/nova/tests/unit/virt/libvirt/test_guest.py
+++ b/nova/tests/unit/virt/libvirt/test_guest.py
@@ -377,6 +377,48 @@ class GuestTestCase(test.NoDBTestCase):
error_message="device not found: disk vdb not found",
supports_device_missing=True)
+ def test_detach_device_with_already_in_process_of_unplug_error(self):
+ # Assert that DeviceNotFound is raised when encountering
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1878659
+ # This is raised as QEMU returns a VIR_ERR_INTERNAL_ERROR when
+ # a request to device_del is made while another is about to complete.
+
+ self.domain.isPersistent.return_value = True
+ conf = mock.Mock(spec=vconfig.LibvirtConfigGuestDevice)
+ conf.to_xml.return_value = "</xml>"
+
+ existing_unplug_exc = fakelibvirt.make_libvirtError(
+ fakelibvirt.libvirtError, "",
+ error_message='device vdb is already in the process of unplug',
+ error_code=fakelibvirt.VIR_ERR_INTERNAL_ERROR,
+ error_domain=fakelibvirt.VIR_FROM_DOMAIN
+ )
+ device_missing_exc = fakelibvirt.make_libvirtError(
+ fakelibvirt.libvirtError, "",
+ error_message='device not found: disk vdb not found',
+ error_code=fakelibvirt.VIR_ERR_DEVICE_MISSING,
+ error_domain=fakelibvirt.VIR_FROM_DOMAIN
+ )
+
+ # Raise VIR_ERR_INTERNAL_ERROR on the second call before raising
+ # VIR_ERR_DEVICE_MISSING to mock the first call successfully detaching
+ # the device asynchronously.
+ self.domain.detachDeviceFlags.side_effect = [
+ None,
+ existing_unplug_exc,
+ device_missing_exc
+ ]
+
+ retry_detach = self.guest.detach_device_with_retry(
+ mock.Mock(return_value=conf),
+ 'vdb',
+ live=True,
+ inc_sleep_time=.01
+ )
+
+ # Assert that we raise exception.DeviceNotFound
+ self.assertRaises(exception.DeviceNotFound, retry_detach)
+
def test_get_xml_desc(self):
self.guest.get_xml_desc()
self.domain.XMLDesc.assert_called_once_with(flags=0)
diff --git a/nova/virt/disk/api.py b/nova/virt/disk/api.py
index 79d629f283..9902c0608b 100644
--- a/nova/virt/disk/api.py
+++ b/nova/virt/disk/api.py
@@ -615,8 +615,8 @@ def _set_passwd(username, admin_passwd, passwd_data, shadow_data):
:param username: the username
:param admin_passwd: the admin password
- :param passwd_data: path to the passwd file
- :param shadow_data: path to the shadow password file
+ :param passwd_data: Data from the passwd file decoded as a string
+ :param shadow_data: Data from the shadow file decoded as a string
:returns: nothing
:raises: exception.NovaException(), IOError()
diff --git a/nova/virt/disk/vfs/guestfs.py b/nova/virt/disk/vfs/guestfs.py
index 90586ef8f1..861ad80746 100644
--- a/nova/virt/disk/vfs/guestfs.py
+++ b/nova/virt/disk/vfs/guestfs.py
@@ -306,7 +306,14 @@ class VFSGuestFS(vfs.VFS):
def read_file(self, path):
LOG.debug("Read file path=%s", path)
path = self._canonicalize_path(path)
- return self.handle.read_file(path)
+ data = self.handle.read_file(path)
+ # NOTE(lyarwood): libguestfs v1.41.1 (0ee02e0117527) switched the
+ # return type of read_file from string to bytes and as such we need to
+ # handle both here, decoding and returning a string if bytes is
+ # provided. https://bugzilla.redhat.com/show_bug.cgi?id=1661871
+ if isinstance(data, bytes):
+ return data.decode()
+ return data
def has_file(self, path):
LOG.debug("Has file path=%s", path)
diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py
index b5eb984fef..8ee8757e40 100644
--- a/nova/virt/libvirt/driver.py
+++ b/nova/virt/libvirt/driver.py
@@ -1964,14 +1964,18 @@ class LibvirtDriver(driver.ComputeDriver):
return self._host.delete_secret('volume', volume_id)
if encryption is None:
encryption = self._get_volume_encryption(context, connection_info)
- # NOTE(lyarwood): Handle bug #1821696 where volume secrets have been
- # removed manually by returning if a LUKS provider is being used
- # and device_path is not present in the connection_info. This avoids
- # VolumeEncryptionNotSupported being thrown when we incorrectly build
- # the encryptor below due to the secrets not being present above.
- if (encryption and self._allow_native_luksv1(encryption=encryption) and
- not connection_info['data'].get('device_path')):
+
+ # NOTE(lyarwood): Handle bugs #1821696 and #1917619 by avoiding the use
+ # of the os-brick encryptors if we don't have a device_path. The lack
+ # of a device_path here suggests the volume was natively attached to
+ # QEMU anyway as volumes without a device_path are not supported by
+ # os-brick encryptors. For volumes with a device_path the calls to
+ # the os-brick encryptors are safe as they are actually idempotent,
+ # ignoring any failures caused by the volumes actually being natively
+ # attached previously.
+ if (encryption and connection_info['data'].get('device_path') is None):
return
+
if encryption:
encryptor = self._get_volume_encryptor(connection_info,
encryption)
diff --git a/nova/virt/libvirt/guest.py b/nova/virt/libvirt/guest.py
index b738097377..66e06896c6 100644
--- a/nova/virt/libvirt/guest.py
+++ b/nova/virt/libvirt/guest.py
@@ -434,12 +434,27 @@ class Guest(object):
LOG.debug('Successfully detached device %s from guest. '
'Persistent? %s. Live? %s',
device, persistent, live)
-
except libvirt.libvirtError as ex:
with excutils.save_and_reraise_exception(reraise=False) as ctx:
- if ex.get_error_code() == libvirt.VIR_ERR_DEVICE_MISSING:
+ code = ex.get_error_code()
+ msg = ex.get_error_message()
+ if code == libvirt.VIR_ERR_DEVICE_MISSING:
raise exception.DeviceNotFound(
device=alternative_device_name)
+ # NOTE(lyarwood): https://bugzilla.redhat.com/1878659
+ # Ignore this known QEMU bug for the time being allowing
+ # our retry logic to fire again and hopefully see that
+ # the device has been removed asynchronously by QEMU
+ # in the meantime when the next call to detach raises
+ # VIR_ERR_DEVICE_MISSING.
+ if (code == libvirt.VIR_ERR_INTERNAL_ERROR and
+ msg and 'already in the process of unplug' in msg
+ ):
+ LOG.debug('Ignoring QEMU rejecting our request to '
+ 'detach as it is caused by a previous '
+ 'request still being in progress.')
+ return
+
# Re-raise the original exception if we're not raising
# DeviceNotFound instead. This will avoid logging of a
# "Original exception being dropped" traceback.