f8e24c33f8
QEMU 2.6 and Libvirt 2.2.0 allow LUKS encrypted RAW files, block devices and network devices (such as rbd) to be decrypted natively by QEMU. This change enables the use of this feature within Nova when the appropriate versions of QEMU and Libvirt are installed and the encryption provider for the volume is of type 'luks'. When these conditions are met a Libvirt secret is created when connecting encrypted volumes to a compute host to hold the LUKS passphrase used to unlock the volume. The presence of this Libvirt secret is then used by the volume driver to generate the required encryption XML for the disk. QEMU is then able to natively read from and write to the encrypted disk, removing the need for the os-brick supplied dm-crypt style encryptors, previously used to handle encrypted volumes. When disconnecting a volume the presence of a Libvirt secret will result in no attempt being made to detach an os-brick provided encryptor from the volume. This will only occur when no secret for the volume is found on the compute host. This will allow encrypted volumes attached prior to this change to still be detached correctly. Attempts to swap between volumes while using native QEMU decryption will be blocked in the same manner as they are when using volumes that do not provide a local block device. Both use cases still require additional implementation work in Libvirt before being allowed within Nova. LibvirtLiveMigrateData and LibvirtLiveMigrateBDMInfo are both extended to support the following matrix of live migration (LM) scenarios: - Pike using os-brick encryptors to Queens using os-brick encryptors - Queens using os-brick encryptors to Queens using os-brick encryptors - Queens using os-brick encryptors to Queens using native QEMU decrypt - Queens using native QEMU decrypt to Queens using native QEMU decrypt A new 'src_supports_native_luks' attribute has been added to LibvirtLiveMigrateData in Queens to indicate that the source host is capable of configuring QEMU LUKS decryption for a volume during LM. The presence of this attribute in migrate_data during pre_live_migration on the destination is then used to decide how encrypted volumes are connected on that host ahead of LM starting. When missing or False the os-brick encryptors will be attached, when present and True the native QEMU decryption approach will be taken only if the destination host also supports native LUKS decryption by QEMU. The UUID of this secret is then stored in the individual LibvirtLiveMigrateBDMInfo object associated with the volume and passed back to the source host to be added to the volume configuration prior to the start of the live migration. Implements: blueprint libvirt-qemu-native-luks Change-Id: Ibfa64f18bbd2fb70db7791330ed1a64fe61c1355
385 lines
16 KiB
Python
385 lines
16 KiB
Python
# Copyright 2015 Red Hat, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from oslo_log import log
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import versionutils
|
|
|
|
from nova import objects
|
|
from nova.objects import base as obj_base
|
|
from nova.objects import fields
|
|
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
@obj_base.NovaObjectRegistry.register_if(False)
|
|
class LiveMigrateData(obj_base.NovaObject):
|
|
fields = {
|
|
'is_volume_backed': fields.BooleanField(),
|
|
'migration': fields.ObjectField('Migration'),
|
|
# old_vol_attachment_ids is a dict used to store the old attachment_ids
|
|
# for each volume so they can be restored on a migration rollback. The
|
|
# key is the volume_id, and the value is the attachment_id.
|
|
'old_vol_attachment_ids': fields.DictOfStringsField(),
|
|
}
|
|
|
|
def to_legacy_dict(self, pre_migration_result=False):
|
|
legacy = {}
|
|
if self.obj_attr_is_set('is_volume_backed'):
|
|
legacy['is_volume_backed'] = self.is_volume_backed
|
|
if self.obj_attr_is_set('migration'):
|
|
legacy['migration'] = self.migration
|
|
if pre_migration_result:
|
|
legacy['pre_live_migration_result'] = {}
|
|
|
|
return legacy
|
|
|
|
def from_legacy_dict(self, legacy):
|
|
if 'is_volume_backed' in legacy:
|
|
self.is_volume_backed = legacy['is_volume_backed']
|
|
if 'migration' in legacy:
|
|
self.migration = legacy['migration']
|
|
|
|
@classmethod
|
|
def detect_implementation(cls, legacy_dict):
|
|
if 'instance_relative_path' in legacy_dict:
|
|
obj = LibvirtLiveMigrateData()
|
|
elif 'image_type' in legacy_dict:
|
|
obj = LibvirtLiveMigrateData()
|
|
elif 'migrate_data' in legacy_dict:
|
|
obj = XenapiLiveMigrateData()
|
|
else:
|
|
obj = LiveMigrateData()
|
|
obj.from_legacy_dict(legacy_dict)
|
|
return obj
|
|
|
|
|
|
@obj_base.NovaObjectRegistry.register
|
|
class LibvirtLiveMigrateBDMInfo(obj_base.NovaObject):
|
|
# VERSION 1.0 : Initial version
|
|
# VERSION 1.1 : Added encryption_secret_uuid for tracking volume secret
|
|
# uuid created on dest during migration with encrypted vols.
|
|
VERSION = '1.1'
|
|
|
|
fields = {
|
|
# FIXME(danms): some of these can be enums?
|
|
'serial': fields.StringField(),
|
|
'bus': fields.StringField(),
|
|
'dev': fields.StringField(),
|
|
'type': fields.StringField(),
|
|
'format': fields.StringField(nullable=True),
|
|
'boot_index': fields.IntegerField(nullable=True),
|
|
'connection_info_json': fields.StringField(),
|
|
'encryption_secret_uuid': fields.UUIDField(nullable=True),
|
|
}
|
|
|
|
def obj_make_compatible(self, primitive, target_version):
|
|
super(LibvirtLiveMigrateBDMInfo, self).obj_make_compatible(
|
|
primitive, target_version)
|
|
target_version = versionutils.convert_version_to_tuple(target_version)
|
|
if target_version < (1, 1) and 'encryption_secret_uuid' in primitive:
|
|
del primitive['encryption_secret_uuid']
|
|
|
|
# NOTE(danms): We don't have a connection_info object right
|
|
# now, and instead mostly store/pass it as JSON that we're
|
|
# careful with. When we get a connection_info object in the
|
|
# future, we should use it here, so make this easy to convert
|
|
# for later.
|
|
@property
|
|
def connection_info(self):
|
|
return jsonutils.loads(self.connection_info_json)
|
|
|
|
@connection_info.setter
|
|
def connection_info(self, info):
|
|
self.connection_info_json = jsonutils.dumps(info)
|
|
|
|
def as_disk_info(self):
|
|
info_dict = {
|
|
'dev': self.dev,
|
|
'bus': self.bus,
|
|
'type': self.type,
|
|
}
|
|
if self.obj_attr_is_set('format') and self.format:
|
|
info_dict['format'] = self.format
|
|
if self.obj_attr_is_set('boot_index') and self.boot_index is not None:
|
|
info_dict['boot_index'] = str(self.boot_index)
|
|
return info_dict
|
|
|
|
|
|
@obj_base.NovaObjectRegistry.register
|
|
class LibvirtLiveMigrateData(LiveMigrateData):
|
|
# Version 1.0: Initial version
|
|
# Version 1.1: Added target_connect_addr
|
|
# Version 1.2: Added 'serial_listen_ports' to allow live migration with
|
|
# serial console.
|
|
# Version 1.3: Added 'supported_perf_events'
|
|
# Version 1.4: Added old_vol_attachment_ids
|
|
# Version 1.5: Added src_supports_native_luks
|
|
VERSION = '1.5'
|
|
|
|
fields = {
|
|
'filename': fields.StringField(),
|
|
# FIXME: image_type should be enum?
|
|
'image_type': fields.StringField(),
|
|
'block_migration': fields.BooleanField(),
|
|
'disk_over_commit': fields.BooleanField(),
|
|
'disk_available_mb': fields.IntegerField(nullable=True),
|
|
'is_shared_instance_path': fields.BooleanField(),
|
|
'is_shared_block_storage': fields.BooleanField(),
|
|
'instance_relative_path': fields.StringField(),
|
|
'graphics_listen_addr_vnc': fields.IPAddressField(nullable=True),
|
|
'graphics_listen_addr_spice': fields.IPAddressField(nullable=True),
|
|
'serial_listen_addr': fields.StringField(nullable=True),
|
|
'serial_listen_ports': fields.ListOfIntegersField(),
|
|
'bdms': fields.ListOfObjectsField('LibvirtLiveMigrateBDMInfo'),
|
|
'target_connect_addr': fields.StringField(nullable=True),
|
|
'supported_perf_events': fields.ListOfStringsField(),
|
|
'src_supports_native_luks': fields.BooleanField(),
|
|
}
|
|
|
|
def obj_make_compatible(self, primitive, target_version):
|
|
super(LibvirtLiveMigrateData, self).obj_make_compatible(
|
|
primitive, target_version)
|
|
target_version = versionutils.convert_version_to_tuple(target_version)
|
|
if target_version < (1, 5):
|
|
if 'src_supports_native_luks' in primitive:
|
|
del primitive['src_supports_native_luks']
|
|
if target_version < (1, 4):
|
|
if 'old_vol_attachment_ids' in primitive:
|
|
del primitive['old_vol_attachment_ids']
|
|
if target_version < (1, 3):
|
|
if 'supported_perf_events' in primitive:
|
|
del primitive['supported_perf_events']
|
|
if target_version < (1, 2):
|
|
if 'serial_listen_ports' in primitive:
|
|
del primitive['serial_listen_ports']
|
|
if target_version < (1, 1) and 'target_connect_addr' in primitive:
|
|
del primitive['target_connect_addr']
|
|
|
|
def _bdms_to_legacy(self, legacy):
|
|
if not self.obj_attr_is_set('bdms'):
|
|
return
|
|
legacy['volume'] = {}
|
|
for bdmi in self.bdms:
|
|
legacy['volume'][bdmi.serial] = {
|
|
'disk_info': bdmi.as_disk_info(),
|
|
'connection_info': bdmi.connection_info}
|
|
|
|
def _bdms_from_legacy(self, legacy_pre_result):
|
|
self.bdms = []
|
|
volume = legacy_pre_result.get('volume', {})
|
|
for serial in volume:
|
|
vol = volume[serial]
|
|
bdmi = objects.LibvirtLiveMigrateBDMInfo(serial=serial)
|
|
bdmi.connection_info = vol['connection_info']
|
|
bdmi.bus = vol['disk_info']['bus']
|
|
bdmi.dev = vol['disk_info']['dev']
|
|
bdmi.type = vol['disk_info']['type']
|
|
if 'format' in vol:
|
|
bdmi.format = vol['disk_info']['format']
|
|
if 'boot_index' in vol:
|
|
bdmi.boot_index = int(vol['disk_info']['boot_index'])
|
|
self.bdms.append(bdmi)
|
|
|
|
def to_legacy_dict(self, pre_migration_result=False):
|
|
LOG.debug('Converting to legacy: %s', self)
|
|
legacy = super(LibvirtLiveMigrateData, self).to_legacy_dict()
|
|
keys = (set(self.fields.keys()) -
|
|
set(LiveMigrateData.fields.keys()) - {'bdms'})
|
|
legacy.update({k: getattr(self, k) for k in keys
|
|
if self.obj_attr_is_set(k)})
|
|
|
|
graphics_vnc = legacy.pop('graphics_listen_addr_vnc', None)
|
|
graphics_spice = legacy.pop('graphics_listen_addr_spice', None)
|
|
transport_target = legacy.pop('target_connect_addr', None)
|
|
live_result = {
|
|
'graphics_listen_addrs': {
|
|
'vnc': graphics_vnc and str(graphics_vnc),
|
|
'spice': graphics_spice and str(graphics_spice),
|
|
},
|
|
'serial_listen_addr': legacy.pop('serial_listen_addr', None),
|
|
'target_connect_addr': transport_target,
|
|
}
|
|
|
|
if pre_migration_result:
|
|
legacy['pre_live_migration_result'] = live_result
|
|
self._bdms_to_legacy(live_result)
|
|
|
|
LOG.debug('Legacy result: %s', legacy)
|
|
return legacy
|
|
|
|
def from_legacy_dict(self, legacy):
|
|
LOG.debug('Converting legacy dict to obj: %s', legacy)
|
|
super(LibvirtLiveMigrateData, self).from_legacy_dict(legacy)
|
|
keys = set(self.fields.keys()) - set(LiveMigrateData.fields.keys())
|
|
for k in keys - {'bdms'}:
|
|
if k in legacy:
|
|
setattr(self, k, legacy[k])
|
|
if 'pre_live_migration_result' in legacy:
|
|
pre_result = legacy['pre_live_migration_result']
|
|
self.graphics_listen_addr_vnc = \
|
|
pre_result['graphics_listen_addrs'].get('vnc')
|
|
self.graphics_listen_addr_spice = \
|
|
pre_result['graphics_listen_addrs'].get('spice')
|
|
self.target_connect_addr = pre_result.get('target_connect_addr')
|
|
if 'serial_listen_addr' in pre_result:
|
|
self.serial_listen_addr = pre_result['serial_listen_addr']
|
|
self._bdms_from_legacy(pre_result)
|
|
LOG.debug('Converted object: %s', self)
|
|
|
|
def is_on_shared_storage(self):
|
|
return self.is_shared_block_storage or self.is_shared_instance_path
|
|
|
|
|
|
@obj_base.NovaObjectRegistry.register
|
|
class XenapiLiveMigrateData(LiveMigrateData):
|
|
# Version 1.0: Initial version
|
|
# Version 1.1: Added vif_uuid_map
|
|
# Version 1.2: Added old_vol_attachment_ids
|
|
VERSION = '1.2'
|
|
|
|
fields = {
|
|
'block_migration': fields.BooleanField(nullable=True),
|
|
'destination_sr_ref': fields.StringField(nullable=True),
|
|
'migrate_send_data': fields.DictOfStringsField(nullable=True),
|
|
'sr_uuid_map': fields.DictOfStringsField(),
|
|
'kernel_file': fields.StringField(),
|
|
'ramdisk_file': fields.StringField(),
|
|
'vif_uuid_map': fields.DictOfStringsField(),
|
|
}
|
|
|
|
def to_legacy_dict(self, pre_migration_result=False):
|
|
legacy = super(XenapiLiveMigrateData, self).to_legacy_dict()
|
|
if self.obj_attr_is_set('block_migration'):
|
|
legacy['block_migration'] = self.block_migration
|
|
if self.obj_attr_is_set('migrate_send_data'):
|
|
legacy['migrate_data'] = {
|
|
'migrate_send_data': self.migrate_send_data,
|
|
'destination_sr_ref': self.destination_sr_ref,
|
|
}
|
|
live_result = {
|
|
'sr_uuid_map': ('sr_uuid_map' in self and self.sr_uuid_map
|
|
or {}),
|
|
'vif_uuid_map': ('vif_uuid_map' in self and self.vif_uuid_map
|
|
or {}),
|
|
}
|
|
if pre_migration_result:
|
|
legacy['pre_live_migration_result'] = live_result
|
|
return legacy
|
|
|
|
def from_legacy_dict(self, legacy):
|
|
super(XenapiLiveMigrateData, self).from_legacy_dict(legacy)
|
|
if 'block_migration' in legacy:
|
|
self.block_migration = legacy['block_migration']
|
|
else:
|
|
self.block_migration = False
|
|
if 'migrate_data' in legacy:
|
|
self.migrate_send_data = \
|
|
legacy['migrate_data']['migrate_send_data']
|
|
self.destination_sr_ref = \
|
|
legacy['migrate_data']['destination_sr_ref']
|
|
if 'pre_live_migration_result' in legacy:
|
|
self.sr_uuid_map = \
|
|
legacy['pre_live_migration_result']['sr_uuid_map']
|
|
self.vif_uuid_map = \
|
|
legacy['pre_live_migration_result'].get('vif_uuid_map', {})
|
|
|
|
def obj_make_compatible(self, primitive, target_version):
|
|
super(XenapiLiveMigrateData, self).obj_make_compatible(
|
|
primitive, target_version)
|
|
target_version = versionutils.convert_version_to_tuple(target_version)
|
|
if target_version < (1, 2):
|
|
if 'old_vol_attachment_ids' in primitive:
|
|
del primitive['old_vol_attachment_ids']
|
|
if target_version < (1, 1):
|
|
if 'vif_uuid_map' in primitive:
|
|
del primitive['vif_uuid_map']
|
|
|
|
|
|
@obj_base.NovaObjectRegistry.register
|
|
class HyperVLiveMigrateData(LiveMigrateData):
|
|
# Version 1.0: Initial version
|
|
# Version 1.1: Added is_shared_instance_path
|
|
# Version 1.2: Added old_vol_attachment_ids
|
|
VERSION = '1.2'
|
|
|
|
fields = {'is_shared_instance_path': fields.BooleanField()}
|
|
|
|
def obj_make_compatible(self, primitive, target_version):
|
|
super(HyperVLiveMigrateData, self).obj_make_compatible(
|
|
primitive, target_version)
|
|
target_version = versionutils.convert_version_to_tuple(target_version)
|
|
if target_version < (1, 2):
|
|
if 'old_vol_attachment_ids' in primitive:
|
|
del primitive['old_vol_attachment_ids']
|
|
if target_version < (1, 1):
|
|
if 'is_shared_instance_path' in primitive:
|
|
del primitive['is_shared_instance_path']
|
|
|
|
def to_legacy_dict(self, pre_migration_result=False):
|
|
legacy = super(HyperVLiveMigrateData, self).to_legacy_dict()
|
|
if self.obj_attr_is_set('is_shared_instance_path'):
|
|
legacy['is_shared_instance_path'] = self.is_shared_instance_path
|
|
|
|
return legacy
|
|
|
|
def from_legacy_dict(self, legacy):
|
|
super(HyperVLiveMigrateData, self).from_legacy_dict(legacy)
|
|
if 'is_shared_instance_path' in legacy:
|
|
self.is_shared_instance_path = legacy['is_shared_instance_path']
|
|
|
|
|
|
@obj_base.NovaObjectRegistry.register
|
|
class PowerVMLiveMigrateData(LiveMigrateData):
|
|
# Version 1.0: Initial version
|
|
# Version 1.1: Added the Virtual Ethernet Adapter VLAN mappings.
|
|
# Version 1.2: Added old_vol_attachment_ids
|
|
VERSION = '1.2'
|
|
|
|
fields = {
|
|
'host_mig_data': fields.DictOfNullableStringsField(),
|
|
'dest_ip': fields.StringField(),
|
|
'dest_user_id': fields.StringField(),
|
|
'dest_sys_name': fields.StringField(),
|
|
'public_key': fields.StringField(),
|
|
'dest_proc_compat': fields.StringField(),
|
|
'vol_data': fields.DictOfNullableStringsField(),
|
|
'vea_vlan_mappings': fields.DictOfNullableStringsField(),
|
|
}
|
|
|
|
def obj_make_compatible(self, primitive, target_version):
|
|
super(PowerVMLiveMigrateData, self).obj_make_compatible(
|
|
primitive, target_version)
|
|
target_version = versionutils.convert_version_to_tuple(target_version)
|
|
if target_version < (1, 2):
|
|
if 'old_vol_attachment_ids' in primitive:
|
|
del primitive['old_vol_attachment_ids']
|
|
if target_version < (1, 1):
|
|
if 'vea_vlan_mappings' in primitive:
|
|
del primitive['vea_vlan_mappings']
|
|
|
|
def to_legacy_dict(self, pre_migration_result=False):
|
|
legacy = super(PowerVMLiveMigrateData, self).to_legacy_dict()
|
|
for field in self.fields:
|
|
if self.obj_attr_is_set(field):
|
|
legacy[field] = getattr(self, field)
|
|
return legacy
|
|
|
|
def from_legacy_dict(self, legacy):
|
|
super(PowerVMLiveMigrateData, self).from_legacy_dict(legacy)
|
|
for field in self.fields:
|
|
if field in legacy:
|
|
setattr(self, field, legacy[field])
|