check minimum VM version when setting VM snapshots

Some Hyper-V operations require a minimum VM version in order to succeed.
For example, Production Checkpoints are supported on VM Versions 6.2 and
newer.

Clustering Hyper-V compute nodes may change the list of supported VM
versions list and the default VM version on that host.

https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/deploy/upgrade-virtual-machine-version-in-hyper-v-on-windows-or-windows-server

Partial-Bug: #1707605
Change-Id: Iac795f570649ce28847b88c5a21e575daefefc49
This commit is contained in:
Claudiu Belu 2017-07-21 18:02:58 +03:00
parent 742c6d1bbb
commit e0d7032dfb
7 changed files with 78 additions and 3 deletions

View File

@ -15,6 +15,8 @@
# under the License.
import ctypes
import inspect
from pkg_resources import parse_version
import textwrap
import time
import types
@ -29,6 +31,7 @@ from oslo_utils import excutils
from oslo_utils import reflection
import six
from os_win import constants
from os_win import exceptions
@ -256,3 +259,43 @@ def hex_str_to_byte_array(string):
def byte_array_to_hex_str(byte_aray):
return ''.join('{:02X}'.format(b) for b in byte_aray)
def required_vm_version(min_version=constants.VM_VERSION_5_0,
max_version=constants.VM_VERSION_254_0):
"""Ensures that the wrapped method's VM meets the version requirements.
Some Hyper-V operations require a minimum VM version in order to succeed.
For example, Production Checkpoints are supported on VM Versions 6.2 and
newer.
Clustering Hyper-V compute nodes may change the list of supported VM
versions list and the default VM version on that host.
:param min_version: string, the VM's minimum version required for the
operation to succeed.
:param max_version: string, the VM's maximum version required for the
operation to succeed.
:raises exceptions.InvalidVMVersion: if the VM's version does not meet the
given requirements.
"""
def wrapper(func):
def inner(*args, **kwargs):
all_args = inspect.getcallargs(func, *args, **kwargs)
vmsettings = all_args['vmsettings']
# NOTE(claudiub): VMs on Windows / Hyper-V Server 2012 do not have
# a Version field, but they are 4.0.
vm_version_str = getattr(vmsettings, 'Version', '4.0')
vm_version = parse_version(vm_version_str)
if (vm_version >= parse_version(min_version) and
vm_version <= parse_version(max_version)):
return func(*args, **kwargs)
raise exceptions.InvalidVMVersion(
vm_name=vmsettings.ElementName, version=vm_version_str,
min_version=min_version, max_version=max_version)
return inner
return wrapper

View File

@ -211,6 +211,11 @@ VM_SNAPSHOT_TYPE_PROD_FALLBACK = 3
VM_SNAPSHOT_TYPE_PROD_ENFORCED = 4
VM_SNAPSHOT_TYPE_STANDARD = 5
VM_VERSION_5_0 = '5.0'
VM_VERSION_6_2 = '6.2'
VM_VERSION_8_0 = '8.0'
VM_VERSION_254_0 = '254.0'
DEFAULT_WMI_EVENT_TIMEOUT_MS = 2000
SCSI_UID_SCSI_NAME_STRING = 8

View File

@ -104,6 +104,12 @@ class InvalidParameterValue(Invalid):
"%(param_name)s=%(param_value)s")
class InvalidVMVersion(Invalid):
msg_fmt = _("VM '%(vm_name)s' has an invalid version for this operation: "
"%(version)s. Version is expected to be between: "
"%(min_version)s and %(max_version)s.")
class SMBException(OSWinException):
pass

View File

@ -23,6 +23,7 @@ import mock
from oslotest import base
from os_win import _utils
from os_win import constants
from os_win import exceptions
@ -286,3 +287,20 @@ class UtilsTestCase(base.BaseTestCase):
expected_string = '000102'
self.assertEqual(expected_string, resulted_string)
def test_required_vm_version(self):
@_utils.required_vm_version()
def foo(bar, vmsettings):
pass
mock_vmsettings = mock.Mock()
for good_version in [constants.VM_VERSION_5_0,
constants.VM_VERSION_254_0]:
mock_vmsettings.Version = good_version
foo(mock.sentinel.bar, mock_vmsettings)
for bad_version in ['4.99', '254.1']:
mock_vmsettings.Version = bad_version
self.assertRaises(exceptions.InvalidVMVersion, foo,
mock.sentinel.bar, mock_vmsettings)

View File

@ -351,7 +351,7 @@ class VMUtils10TestCase(test_base.OsWinBaseTestCase):
mock_get_element_associated_class.return_value)
def test_set_snapshot_type(self):
vmsettings = mock.Mock()
vmsettings = mock.Mock(Version='6.2')
self._vmutils._set_vm_snapshot_type(
vmsettings, mock.sentinel.snapshot_type)

View File

@ -296,7 +296,7 @@ class VMUtils(baseutils.BaseUtilsVirt):
vcpus_per_numa_node, limit_cpu_features, dynamic_mem_ratio,
configuration_root_dir=None, snapshot_dir=None,
host_shutdown_action=None, vnuma_enabled=None,
snapshot_type=constants.VM_SNAPSHOT_TYPE_PROD_FALLBACK,
snapshot_type=None,
is_planned_vm=False):
vmsetting = self._lookup_vm_check(vm_name, for_update=True)
@ -318,7 +318,8 @@ class VMUtils(baseutils.BaseUtilsVirt):
self._set_vm_vcpus(vmsetting, vcpus_num, vcpus_per_numa_node,
limit_cpu_features)
self._set_vm_snapshot_type(vmsetting, snapshot_type)
if snapshot_type:
self._set_vm_snapshot_type(vmsetting, snapshot_type)
self._modify_virtual_system(vmsetting)

View File

@ -19,6 +19,7 @@ from oslo_log import log as logging
import six
from os_win._i18n import _
from os_win import _utils
from os_win import constants
from os_win import exceptions
from os_win.utils import _wqlutils
@ -289,6 +290,7 @@ class VMUtils10(vmutils.VMUtils):
if pci_sds:
self._jobutils.remove_multiple_virt_resources(pci_sds)
@_utils.required_vm_version(min_version=constants.VM_VERSION_6_2)
def _set_vm_snapshot_type(self, vmsettings, snapshot_type):
# We expect the caller to actually push the vmsettings update.
vmsettings.UserSnapshotType = snapshot_type