Fixes Hyper-V VHDX snapshot bigger than instance
In Hyper-V, the deployed images will be extended having their metadata size added to their base image size. This may lead to snapshots bigger than the flavor size. This patch adds support to the V2 utils module, so that the maximum internal size of a requested image may be calculated in a similar way it is done in the V1 module. VHDX format specs: http://www.microsoft.com/en-us/download/details.aspx?id=34750 Closes-bug: #1231911 Change-Id: I0d6aacb275f8af24554d3e8b6f04fe8a454b913f
This commit is contained in:
parent
f474441263
commit
9ea92029f7
|
@ -976,7 +976,8 @@ class HyperVAPITestCase(test.NoDBTestCase):
|
|||
mox.IsA(str), mox.IsA(object))
|
||||
m.AndReturn(1025)
|
||||
|
||||
vhdutils.VHDUtils.resize_vhd(mox.IsA(str), mox.IsA(object))
|
||||
vhdutils.VHDUtils.resize_vhd(mox.IsA(str), mox.IsA(object),
|
||||
is_file_max_size=False)
|
||||
|
||||
def _setup_spawn_instance_mocks(self, cow, setup_vif_mocks_func=None,
|
||||
with_exception=False,
|
||||
|
@ -1016,7 +1017,8 @@ class HyperVAPITestCase(test.NoDBTestCase):
|
|||
m = vhdutils.VHDUtils.get_internal_vhd_size_by_file_size(
|
||||
mox.IsA(str), mox.IsA(object))
|
||||
m.AndReturn(1025)
|
||||
vhdutils.VHDUtils.resize_vhd(mox.IsA(str), mox.IsA(object))
|
||||
vhdutils.VHDUtils.resize_vhd(mox.IsA(str), mox.IsA(object),
|
||||
is_file_max_size=False)
|
||||
|
||||
self._setup_check_admin_permissions_mocks(
|
||||
admin_permissions=admin_permissions)
|
||||
|
|
|
@ -16,6 +16,7 @@ import mock
|
|||
|
||||
from nova import test
|
||||
|
||||
from nova.openstack.common import units
|
||||
from nova.virt.hyperv import constants
|
||||
from nova.virt.hyperv import vhdutilsv2
|
||||
|
||||
|
@ -26,16 +27,24 @@ class VHDUtilsV2TestCase(test.NoDBTestCase):
|
|||
_FAKE_VHD_PATH = "C:\\fake_path.vhdx"
|
||||
_FAKE_PARENT_VHD_PATH = "C:\\fake_parent_path.vhdx"
|
||||
_FAKE_FORMAT = 3
|
||||
_FAKE_MAK_INTERNAL_SIZE = 1000
|
||||
_FAKE_MAK_INTERNAL_SIZE = units.Gi
|
||||
_FAKE_TYPE = 3
|
||||
_FAKE_JOB_PATH = 'fake_job_path'
|
||||
_FAKE_RET_VAL = 0
|
||||
_FAKE_VHD_FORMAT = 'vhdx'
|
||||
_FAKE_BLOCK_SIZE = 33554432
|
||||
_FAKE_LOG_SIZE = 1048576
|
||||
_FAKE_LOGICAL_SECTOR_SIZE = 4096
|
||||
_FAKE_METADATA_SIZE = 1048576
|
||||
|
||||
def setUp(self):
|
||||
self._vhdutils = vhdutilsv2.VHDUtilsV2()
|
||||
self._vhdutils._conn = mock.MagicMock()
|
||||
self._vhdutils._vmutils = mock.MagicMock()
|
||||
self._vhdutils.get_vhd_format = mock.MagicMock(
|
||||
return_value=self._FAKE_VHD_FORMAT)
|
||||
|
||||
self._fake_file_handle = mock.MagicMock()
|
||||
self._fake_vhd_info_xml = (
|
||||
'<INSTANCE CLASSNAME="Msvm_VirtualHardDiskSettingData">'
|
||||
'<PROPERTY NAME="BlockSize" TYPE="uint32">'
|
||||
|
@ -144,6 +153,8 @@ class VHDUtilsV2TestCase(test.NoDBTestCase):
|
|||
mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0]
|
||||
mock_img_svc.ResizeVirtualHardDisk.return_value = (self._FAKE_JOB_PATH,
|
||||
self._FAKE_RET_VAL)
|
||||
self._vhdutils.get_internal_vhd_size_by_file_size = mock.MagicMock(
|
||||
return_value=self._FAKE_MAK_INTERNAL_SIZE)
|
||||
|
||||
self._vhdutils.resize_vhd(self._FAKE_VHD_PATH,
|
||||
self._FAKE_MAK_INTERNAL_SIZE)
|
||||
|
@ -151,3 +162,75 @@ class VHDUtilsV2TestCase(test.NoDBTestCase):
|
|||
mock_img_svc.ResizeVirtualHardDisk.assert_called_once_with(
|
||||
Path=self._FAKE_VHD_PATH,
|
||||
MaxInternalSize=self._FAKE_MAK_INTERNAL_SIZE)
|
||||
|
||||
self.mock_get = self._vhdutils.get_internal_vhd_size_by_file_size
|
||||
self.mock_get.assert_called_once_with(self._FAKE_VHD_PATH,
|
||||
self._FAKE_MAK_INTERNAL_SIZE)
|
||||
|
||||
def test_get_vhdx_internal_size(self):
|
||||
self._vhdutils.get_vhd_info = mock.MagicMock(
|
||||
return_value={'ParentPath': self._FAKE_PARENT_VHD_PATH,
|
||||
'Format': self._FAKE_FORMAT,
|
||||
'BlockSize': self._FAKE_BLOCK_SIZE,
|
||||
'LogicalSectorSize': self._FAKE_LOGICAL_SECTOR_SIZE,
|
||||
'Type': self._FAKE_TYPE})
|
||||
self._vhdutils._get_vhdx_log_size = mock.MagicMock(
|
||||
return_value=self._FAKE_LOG_SIZE)
|
||||
self._vhdutils._get_vhdx_metadata_size_and_offset = mock.MagicMock(
|
||||
return_value=(self._FAKE_METADATA_SIZE, 1024))
|
||||
self._vhdutils._get_vhdx_block_size = mock.MagicMock(
|
||||
return_value=self._FAKE_BLOCK_SIZE)
|
||||
|
||||
file_mock = mock.MagicMock()
|
||||
with mock.patch('__builtin__.open', file_mock):
|
||||
internal_size = (
|
||||
self._vhdutils.get_internal_vhd_size_by_file_size(
|
||||
self._FAKE_VHD_PATH, self._FAKE_MAK_INTERNAL_SIZE))
|
||||
|
||||
self.assertEqual(self._FAKE_MAK_INTERNAL_SIZE - self._FAKE_BLOCK_SIZE,
|
||||
internal_size)
|
||||
|
||||
def test_get_vhdx_current_header(self):
|
||||
VHDX_HEADER_OFFSETS = [64 * 1024, 128 * 1024]
|
||||
fake_sequence_numbers = ['\x01\x00\x00\x00\x00\x00\x00\x00',
|
||||
'\x02\x00\x00\x00\x00\x00\x00\x00']
|
||||
self._fake_file_handle.read = mock.MagicMock(
|
||||
side_effect=fake_sequence_numbers)
|
||||
|
||||
offset = self._vhdutils._get_vhdx_current_header_offset(
|
||||
self._fake_file_handle)
|
||||
self.assertEqual(offset, VHDX_HEADER_OFFSETS[1])
|
||||
|
||||
def test_get_vhdx_metadata_size(self):
|
||||
fake_metadata_offset = '\x01\x00\x00\x00\x00\x00\x00\x00'
|
||||
fake_metadata_size = '\x01\x00\x00\x00'
|
||||
self._fake_file_handle.read = mock.MagicMock(
|
||||
side_effect=[fake_metadata_offset, fake_metadata_size])
|
||||
|
||||
metadata_size, metadata_offset = (
|
||||
self._vhdutils._get_vhdx_metadata_size_and_offset(
|
||||
self._fake_file_handle))
|
||||
self.assertEqual(metadata_size, 1)
|
||||
self.assertEqual(metadata_offset, 1)
|
||||
|
||||
def test_get_block_size(self):
|
||||
self._vhdutils._get_vhdx_metadata_size_and_offset = mock.MagicMock(
|
||||
return_value=(self._FAKE_METADATA_SIZE, 1024))
|
||||
fake_block_size = '\x01\x00\x00\x00'
|
||||
self._fake_file_handle.read = mock.MagicMock(
|
||||
return_value=fake_block_size)
|
||||
|
||||
block_size = self._vhdutils._get_vhdx_block_size(
|
||||
self._fake_file_handle)
|
||||
self.assertEqual(block_size, 1)
|
||||
|
||||
def test_get_log_size(self):
|
||||
fake_current_header_offset = 64 * 1024
|
||||
self._vhdutils._get_vhdx_current_header_offset = mock.MagicMock(
|
||||
return_value=fake_current_header_offset)
|
||||
fake_log_size = '\x01\x00\x00\x00'
|
||||
self._fake_file_handle.read = mock.MagicMock(
|
||||
return_value=fake_log_size)
|
||||
|
||||
log_size = self._vhdutils._get_vhdx_log_size(self._fake_file_handle)
|
||||
self.assertEqual(log_size, 1)
|
||||
|
|
|
@ -26,7 +26,6 @@ from nova.openstack.common import log as logging
|
|||
from nova.openstack.common import units
|
||||
from nova import utils
|
||||
from nova.virt.hyperv import utilsfactory
|
||||
from nova.virt.hyperv import vhdutilsv2
|
||||
from nova.virt.hyperv import vmutils
|
||||
from nova.virt import images
|
||||
|
||||
|
@ -65,14 +64,9 @@ class ImageCache(object):
|
|||
root_vhd_size_gb = self._get_root_vhd_size_gb(instance)
|
||||
root_vhd_size = root_vhd_size_gb * units.Gi
|
||||
|
||||
# NOTE(lpetrut): Checking the namespace is needed as the following
|
||||
# method is not yet implemented in the vhdutilsv2 module.
|
||||
if not isinstance(self._vhdutils, vhdutilsv2.VHDUtilsV2):
|
||||
root_vhd_internal_size = (
|
||||
root_vhd_internal_size = (
|
||||
self._vhdutils.get_internal_vhd_size_by_file_size(
|
||||
vhd_path, root_vhd_size))
|
||||
else:
|
||||
root_vhd_internal_size = root_vhd_size
|
||||
|
||||
if root_vhd_internal_size < vhd_size:
|
||||
raise vmutils.HyperVException(
|
||||
|
@ -101,7 +95,8 @@ class ImageCache(object):
|
|||
{'resized_vhd_path': resized_vhd_path,
|
||||
'root_vhd_size': root_vhd_size})
|
||||
self._vhdutils.resize_vhd(resized_vhd_path,
|
||||
root_vhd_size)
|
||||
root_vhd_internal_size,
|
||||
is_file_max_size=False)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
if self._pathutils.exists(resized_vhd_path):
|
||||
|
|
|
@ -95,6 +95,10 @@ class VHDUtils(object):
|
|||
DestinationPath=dest_vhd_path)
|
||||
self._vmutils.check_ret_val(ret_val, job_path)
|
||||
|
||||
def _get_resize_method(self):
|
||||
image_man_svc = self._conn.Msvm_ImageManagementService()[0]
|
||||
return image_man_svc.ExpandVirtualHardDisk
|
||||
|
||||
def resize_vhd(self, vhd_path, new_max_size, is_file_max_size=True):
|
||||
if is_file_max_size:
|
||||
new_internal_max_size = self.get_internal_vhd_size_by_file_size(
|
||||
|
@ -102,9 +106,9 @@ class VHDUtils(object):
|
|||
else:
|
||||
new_internal_max_size = new_max_size
|
||||
|
||||
image_man_svc = self._conn.Msvm_ImageManagementService()[0]
|
||||
resize = self._get_resize_method()
|
||||
|
||||
(job_path, ret_val) = image_man_svc.ExpandVirtualHardDisk(
|
||||
(job_path, ret_val) = resize(
|
||||
Path=vhd_path, MaxInternalSize=new_internal_max_size)
|
||||
self._vmutils.check_ret_val(ret_val, job_path)
|
||||
|
||||
|
|
|
@ -18,12 +18,14 @@ Utility class for VHD related operations.
|
|||
Based on the "root/virtualization/v2" namespace available starting with
|
||||
Hyper-V Server / Windows Server 2012.
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
if sys.platform == 'win32':
|
||||
import wmi
|
||||
|
||||
from nova.openstack.common.gettextutils import _
|
||||
from nova.openstack.common import units
|
||||
from nova.virt.hyperv import constants
|
||||
from nova.virt.hyperv import vhdutils
|
||||
from nova.virt.hyperv import vmutils
|
||||
|
@ -31,6 +33,15 @@ from nova.virt.hyperv import vmutilsv2
|
|||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
VHDX_BAT_ENTRY_SIZE = 8
|
||||
VHDX_HEADER_OFFSETS = [64 * units.Ki, 128 * units.Ki]
|
||||
VHDX_HEADER_SECTION_SIZE = units.Mi
|
||||
VHDX_LOG_LENGTH_OFFSET = 68
|
||||
VHDX_METADATA_SIZE_OFFSET = 64
|
||||
VHDX_REGION_TABLE_OFFSET = 192 * units.Ki
|
||||
VHDX_BS_METADATA_ENTRY_OFFSET = 48
|
||||
|
||||
|
||||
class VHDUtilsV2(vhdutils.VHDUtils):
|
||||
|
||||
_VHD_TYPE_DYNAMIC = 3
|
||||
|
@ -97,13 +108,91 @@ class VHDUtilsV2(vhdutils.VHDUtils):
|
|||
|
||||
self._vmutils.check_ret_val(ret_val, job_path)
|
||||
|
||||
def resize_vhd(self, vhd_path, new_max_size):
|
||||
def _get_resize_method(self):
|
||||
image_man_svc = self._conn.Msvm_ImageManagementService()[0]
|
||||
return image_man_svc.ResizeVirtualHardDisk
|
||||
|
||||
(job_path, ret_val) = image_man_svc.ResizeVirtualHardDisk(
|
||||
Path=vhd_path, MaxInternalSize=new_max_size)
|
||||
def get_internal_vhd_size_by_file_size(self, vhd_path,
|
||||
new_vhd_file_size):
|
||||
"""VHDX Size = Header (1 MB)
|
||||
+ Log
|
||||
+ Metadata Region
|
||||
+ BAT
|
||||
+ Payload Blocks
|
||||
Chunk size = maximum number of bytes described by a SB block
|
||||
= 2 ** 23 * LogicalSectorSize
|
||||
"""
|
||||
vhd_format = self.get_vhd_format(vhd_path)
|
||||
if vhd_format == constants.DISK_FORMAT_VHD:
|
||||
return super(VHDUtilsV2,
|
||||
self).get_internal_vhd_size_by_file_size(
|
||||
vhd_path, new_vhd_file_size)
|
||||
else:
|
||||
vhd_info = self.get_vhd_info(vhd_path)
|
||||
vhd_type = vhd_info['Type']
|
||||
if vhd_type == self._VHD_TYPE_DIFFERENCING:
|
||||
raise vmutils.HyperVException(_("Differencing VHDX images "
|
||||
"are not supported"))
|
||||
else:
|
||||
try:
|
||||
with open(vhd_path, 'rb') as f:
|
||||
hs = VHDX_HEADER_SECTION_SIZE
|
||||
bes = VHDX_BAT_ENTRY_SIZE
|
||||
|
||||
self._vmutils.check_ret_val(ret_val, job_path)
|
||||
lss = vhd_info['LogicalSectorSize']
|
||||
bs = self._get_vhdx_block_size(f)
|
||||
ls = self._get_vhdx_log_size(f)
|
||||
ms = self._get_vhdx_metadata_size_and_offset(f)[0]
|
||||
|
||||
chunk_ratio = (1 << 23) * lss / bs
|
||||
size = new_vhd_file_size
|
||||
|
||||
max_internal_size = (bs * chunk_ratio * (size - hs -
|
||||
ls - ms - bes - bes / chunk_ratio) / (bs *
|
||||
chunk_ratio + bes * chunk_ratio + bes))
|
||||
|
||||
return max_internal_size - (max_internal_size % bs)
|
||||
|
||||
except IOError as ex:
|
||||
raise vmutils.HyperVException(_("Unable to obtain "
|
||||
"internal size from VHDX: "
|
||||
"%(vhd_path)s. Exception: "
|
||||
"%(ex)s") %
|
||||
{"vhd_path": vhd_path,
|
||||
"ex": ex})
|
||||
|
||||
def _get_vhdx_current_header_offset(self, vhdx_file):
|
||||
sequence_numbers = []
|
||||
for offset in VHDX_HEADER_OFFSETS:
|
||||
vhdx_file.seek(offset + 8)
|
||||
sequence_numbers.append(struct.unpack('<Q',
|
||||
vhdx_file.read(8))[0])
|
||||
current_header = sequence_numbers.index(max(sequence_numbers))
|
||||
return VHDX_HEADER_OFFSETS[current_header]
|
||||
|
||||
def _get_vhdx_log_size(self, vhdx_file):
|
||||
current_header_offset = self._get_vhdx_current_header_offset(vhdx_file)
|
||||
offset = current_header_offset + VHDX_LOG_LENGTH_OFFSET
|
||||
vhdx_file.seek(offset)
|
||||
log_size = struct.unpack('<I', vhdx_file.read(4))[0]
|
||||
return log_size
|
||||
|
||||
def _get_vhdx_metadata_size_and_offset(self, vhdx_file):
|
||||
offset = VHDX_METADATA_SIZE_OFFSET + VHDX_REGION_TABLE_OFFSET
|
||||
vhdx_file.seek(offset)
|
||||
metadata_offset = struct.unpack('<Q', vhdx_file.read(8))[0]
|
||||
metadata_size = struct.unpack('<I', vhdx_file.read(4))[0]
|
||||
return metadata_size, metadata_offset
|
||||
|
||||
def _get_vhdx_block_size(self, vhdx_file):
|
||||
metadata_offset = self._get_vhdx_metadata_size_and_offset(vhdx_file)[1]
|
||||
offset = metadata_offset + VHDX_BS_METADATA_ENTRY_OFFSET
|
||||
vhdx_file.seek(offset)
|
||||
file_parameter_offset = struct.unpack('<I', vhdx_file.read(4))[0]
|
||||
|
||||
vhdx_file.seek(file_parameter_offset + metadata_offset)
|
||||
block_size = struct.unpack('<I', vhdx_file.read(4))[0]
|
||||
return block_size
|
||||
|
||||
def _get_vhd_info_xml(self, image_man_svc, vhd_path):
|
||||
(job_path,
|
||||
|
|
|
@ -35,7 +35,6 @@ from nova.virt import configdrive
|
|||
from nova.virt.hyperv import constants
|
||||
from nova.virt.hyperv import imagecache
|
||||
from nova.virt.hyperv import utilsfactory
|
||||
from nova.virt.hyperv import vhdutilsv2
|
||||
from nova.virt.hyperv import vmutils
|
||||
from nova.virt.hyperv import volumeops
|
||||
|
||||
|
@ -160,14 +159,9 @@ class VMOps(object):
|
|||
base_vhd_size = base_vhd_info['MaxInternalSize']
|
||||
root_vhd_size = instance['root_gb'] * units.Gi
|
||||
|
||||
# NOTE(lpetrut): Checking the namespace is needed as the
|
||||
# following method is not yet implemented in vhdutilsv2.
|
||||
if not isinstance(self._vhdutils, vhdutilsv2.VHDUtilsV2):
|
||||
root_vhd_internal_size = (
|
||||
root_vhd_internal_size = (
|
||||
self._vhdutils.get_internal_vhd_size_by_file_size(
|
||||
root_vhd_path, root_vhd_size))
|
||||
else:
|
||||
root_vhd_internal_size = root_vhd_size
|
||||
|
||||
if root_vhd_internal_size < base_vhd_size:
|
||||
error_msg = _("Cannot resize a VHD to a smaller size, the"
|
||||
|
@ -181,7 +175,9 @@ class VMOps(object):
|
|||
"size %(root_vhd_size)s"),
|
||||
{'base_vhd_path': base_vhd_path,
|
||||
'root_vhd_path': root_vhd_path})
|
||||
self._vhdutils.resize_vhd(root_vhd_path, root_vhd_size)
|
||||
self._vhdutils.resize_vhd(root_vhd_path,
|
||||
root_vhd_internal_size,
|
||||
is_file_max_size=False)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
if self._pathutils.exists(root_vhd_path):
|
||||
|
|
Loading…
Reference in New Issue