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:
Petrut Lucian 2013-09-27 15:40:56 +03:00 committed by Lucian Petrut
parent f474441263
commit 9ea92029f7
6 changed files with 194 additions and 25 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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,

View File

@ -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):