Hyper-V: Shielded VMs
Shielded VMs in Windows Server 2016 protect virtual machines from Hyper-V administrators with the help of encryption technologies. Attaching vTPM devices to the Hyper-V VMs offers users the posibility to enhance their security and system integrity. This blueprint implements this feature. Depends-On: I28c54b1cab1ec98c60c7dba257291b1656429b80 Implements: blueprint hyper-v-shielded-vms Change-Id: Ibfb2dfd254cebdc203c2beb01ec8ecf31cd010e7
This commit is contained in:
parent
820f5f8ff4
commit
f37ce8b6bb
|
@ -103,6 +103,9 @@ REQUIRED = "required"
|
||||||
DISABLED = "disabled"
|
DISABLED = "disabled"
|
||||||
OPTIONAL = "optional"
|
OPTIONAL = "optional"
|
||||||
|
|
||||||
|
IMAGE_PROP_VTPM = "os_vtpm"
|
||||||
|
IMAGE_PROP_VTPM_SHIELDED = "os_shielded_vm"
|
||||||
|
|
||||||
BOOT_DEVICE_FLOPPY = 0
|
BOOT_DEVICE_FLOPPY = 0
|
||||||
BOOT_DEVICE_CDROM = 1
|
BOOT_DEVICE_CDROM = 1
|
||||||
BOOT_DEVICE_HARDDISK = 2
|
BOOT_DEVICE_HARDDISK = 2
|
||||||
|
|
|
@ -197,7 +197,7 @@ class MigrationOps(object):
|
||||||
ephemerals = block_device_info['ephemerals']
|
ephemerals = block_device_info['ephemerals']
|
||||||
self._check_ephemeral_disks(instance, ephemerals)
|
self._check_ephemeral_disks(instance, ephemerals)
|
||||||
|
|
||||||
self._vmops.create_instance(instance, network_info,
|
self._vmops.create_instance(context, instance, network_info,
|
||||||
root_device, block_device_info, vm_gen,
|
root_device, block_device_info, vm_gen,
|
||||||
image_meta)
|
image_meta)
|
||||||
|
|
||||||
|
@ -310,8 +310,9 @@ class MigrationOps(object):
|
||||||
ephemerals = block_device_info['ephemerals']
|
ephemerals = block_device_info['ephemerals']
|
||||||
self._check_ephemeral_disks(instance, ephemerals, resize_instance)
|
self._check_ephemeral_disks(instance, ephemerals, resize_instance)
|
||||||
|
|
||||||
self._vmops.create_instance(instance, network_info, root_device,
|
self._vmops.create_instance(context, instance, network_info,
|
||||||
block_device_info, vm_gen, image_meta)
|
root_device, block_device_info,
|
||||||
|
vm_gen, image_meta)
|
||||||
|
|
||||||
self._check_and_attach_config_drive(instance, vm_gen)
|
self._check_and_attach_config_drive(instance, vm_gen)
|
||||||
self._vmops.set_boot_order(vm_gen, block_device_info, instance_name)
|
self._vmops.set_boot_order(vm_gen, block_device_info, instance_name)
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
# Copyright 2016 Cloudbase Solutions Srl
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 barbicanclient import client as barbican_client
|
||||||
|
from keystoneclient import session
|
||||||
|
from nova import exception
|
||||||
|
from os_win._i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
class PDK(object):
|
||||||
|
|
||||||
|
def create_pdk(self, context, instance, image_meta, pdk_filepath):
|
||||||
|
"""Generates a pdk file using the barbican container referenced by
|
||||||
|
the image metadata or instance metadata. A pdk file is a shielding
|
||||||
|
data file which contains a RDP certificate, unattended file,
|
||||||
|
volume signature catalogs and guardian metadata.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(pdk_filepath, 'wb') as pdk_file_handle:
|
||||||
|
pdk_reference = self._get_pdk_reference(instance, image_meta)
|
||||||
|
pdk_container = self._get_pdk_container(context, instance,
|
||||||
|
pdk_reference)
|
||||||
|
pdk_data = self._get_pdk_data(pdk_container)
|
||||||
|
pdk_file_handle.write(pdk_data)
|
||||||
|
|
||||||
|
def _get_pdk_reference(self, instance, image_meta):
|
||||||
|
image_pdk_ref = image_meta['properties'].get('img_pdk_reference')
|
||||||
|
boot_metadata_pdk_ref = instance.metadata.get('img_pdk_reference')
|
||||||
|
|
||||||
|
if not (image_pdk_ref or boot_metadata_pdk_ref):
|
||||||
|
reason = _('A reference to a barbican container containing the '
|
||||||
|
'pdk file must be passed as an image property. This '
|
||||||
|
'is required in order to enable VTPM')
|
||||||
|
raise exception.MissingParameter(instance_id=instance.uuid,
|
||||||
|
reason=reason)
|
||||||
|
return boot_metadata_pdk_ref or image_pdk_ref
|
||||||
|
|
||||||
|
def _get_pdk_container(self, context, instance, pdk_reference):
|
||||||
|
"""Retrieves the barbican container containing the pdk file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
auth = context.get_auth_plugin()
|
||||||
|
sess = session.Session(auth=auth)
|
||||||
|
brb_client = barbican_client.Client(session=sess)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pdk_container = brb_client.containers.get(pdk_reference)
|
||||||
|
except Exception as e:
|
||||||
|
err_msg = _("Retrieving barbican container with reference "
|
||||||
|
"%(pdk_reference)s failed with error: %(error)s") % {
|
||||||
|
'pdk_reference': pdk_reference,
|
||||||
|
'error': e}
|
||||||
|
raise exception.InvalidMetadata(instance_id=instance.uuid,
|
||||||
|
reason=err_msg)
|
||||||
|
return pdk_container
|
||||||
|
|
||||||
|
def _get_pdk_data(self, pdk_container):
|
||||||
|
"""Return the data from all barbican container's secrets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
no_of_secrets = len(pdk_container.secrets)
|
||||||
|
data = bytes()
|
||||||
|
for index in range(no_of_secrets):
|
||||||
|
current_secret = pdk_container.secrets[str(index + 1)]
|
||||||
|
retrived_secret_data = current_secret.payload
|
||||||
|
data += retrived_secret_data
|
||||||
|
return data
|
|
@ -49,6 +49,7 @@ from hyperv.nova import block_device_manager
|
||||||
from hyperv.nova import constants
|
from hyperv.nova import constants
|
||||||
from hyperv.nova import imagecache
|
from hyperv.nova import imagecache
|
||||||
from hyperv.nova import pathutils
|
from hyperv.nova import pathutils
|
||||||
|
from hyperv.nova import pdk
|
||||||
from hyperv.nova import serialconsoleops
|
from hyperv.nova import serialconsoleops
|
||||||
from hyperv.nova import vif as vif_utils
|
from hyperv.nova import vif as vif_utils
|
||||||
from hyperv.nova import volumeops
|
from hyperv.nova import volumeops
|
||||||
|
@ -108,6 +109,7 @@ class VMOps(object):
|
||||||
self._vif_driver_cache = {}
|
self._vif_driver_cache = {}
|
||||||
self._block_device_manager = (
|
self._block_device_manager = (
|
||||||
block_device_manager.BlockDeviceInfoManager())
|
block_device_manager.BlockDeviceInfoManager())
|
||||||
|
self._pdk = pdk.PDK()
|
||||||
|
|
||||||
def list_instance_uuids(self):
|
def list_instance_uuids(self):
|
||||||
instance_uuids = []
|
instance_uuids = []
|
||||||
|
@ -273,7 +275,7 @@ class VMOps(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with self.wait_vif_plug_events(instance, network_info):
|
with self.wait_vif_plug_events(instance, network_info):
|
||||||
self.create_instance(instance, network_info,
|
self.create_instance(context, instance, network_info,
|
||||||
root_device, block_device_info,
|
root_device, block_device_info,
|
||||||
vm_gen, image_meta)
|
vm_gen, image_meta)
|
||||||
|
|
||||||
|
@ -359,7 +361,7 @@ class VMOps(object):
|
||||||
reason=reason)
|
reason=reason)
|
||||||
return requires_secure_boot
|
return requires_secure_boot
|
||||||
|
|
||||||
def create_instance(self, instance, network_info, root_device,
|
def create_instance(self, context, instance, network_info, root_device,
|
||||||
block_device_info, vm_gen, image_meta):
|
block_device_info, vm_gen, image_meta):
|
||||||
instance_name = instance.name
|
instance_name = instance.name
|
||||||
instance_path = os.path.join(CONF.instances_path, instance_name)
|
instance_path = os.path.join(CONF.instances_path, instance_name)
|
||||||
|
@ -427,6 +429,8 @@ class VMOps(object):
|
||||||
image_meta)
|
image_meta)
|
||||||
self._vmutils.enable_secure_boot(instance.name,
|
self._vmutils.enable_secure_boot(instance.name,
|
||||||
certificate_required)
|
certificate_required)
|
||||||
|
self._configure_secure_vm(context, instance, image_meta,
|
||||||
|
secure_boot_enabled)
|
||||||
|
|
||||||
def _attach_root_device(self, instance_name, root_dev_info):
|
def _attach_root_device(self, instance_name, root_dev_info):
|
||||||
if root_dev_info['type'] == constants.VOLUME:
|
if root_dev_info['type'] == constants.VOLUME:
|
||||||
|
@ -1024,3 +1028,106 @@ class VMOps(object):
|
||||||
if scope == 'storage_qos':
|
if scope == 'storage_qos':
|
||||||
storage_qos_specs[key] = value
|
storage_qos_specs[key] = value
|
||||||
return self._volumeops.parse_disk_qos_specs(storage_qos_specs)
|
return self._volumeops.parse_disk_qos_specs(storage_qos_specs)
|
||||||
|
|
||||||
|
def _configure_secure_vm(self, context, instance, image_meta,
|
||||||
|
secure_boot_enabled):
|
||||||
|
"""Adds and enables a vTPM, encrypting the disks.
|
||||||
|
Shielding option implies encryption option enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
requires_encryption = False
|
||||||
|
requires_shielded = self._feature_requested(
|
||||||
|
instance,
|
||||||
|
image_meta,
|
||||||
|
constants.IMAGE_PROP_VTPM_SHIELDED)
|
||||||
|
|
||||||
|
if not requires_shielded:
|
||||||
|
requires_encryption = self._feature_requested(
|
||||||
|
instance,
|
||||||
|
image_meta,
|
||||||
|
constants.IMAGE_PROP_VTPM)
|
||||||
|
|
||||||
|
if not (requires_shielded or requires_encryption):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._check_vtpm_requirements(instance, image_meta,
|
||||||
|
secure_boot_enabled)
|
||||||
|
|
||||||
|
with self._pathutils.temporary_file('.fsk') as fsk_filepath, \
|
||||||
|
self._pathutils.temporary_file('.pdk') as pdk_filepath:
|
||||||
|
self._create_fsk(instance, fsk_filepath)
|
||||||
|
|
||||||
|
self._pdk.create_pdk(context, instance, image_meta, pdk_filepath)
|
||||||
|
self._vmutils.add_vtpm(instance.name, pdk_filepath,
|
||||||
|
shielded=requires_shielded)
|
||||||
|
LOG.info(_LI("VTPM was added."), instance=instance)
|
||||||
|
self._vmutils.provision_vm(instance.name, fsk_filepath,
|
||||||
|
pdk_filepath)
|
||||||
|
|
||||||
|
def _feature_requested(self, instance, image_meta, image_prop):
|
||||||
|
image_props = image_meta['properties']
|
||||||
|
image_prop_option = image_props.get(image_prop)
|
||||||
|
|
||||||
|
feature_requested = image_prop_option == constants.REQUIRED
|
||||||
|
|
||||||
|
return feature_requested
|
||||||
|
|
||||||
|
def _check_vtpm_requirements(self, instance, image_meta,
|
||||||
|
secure_boot_enabled):
|
||||||
|
if not secure_boot_enabled:
|
||||||
|
reason = _("Adding a vtpm requires secure boot to be enabled.")
|
||||||
|
raise exception.InstanceUnacceptable(
|
||||||
|
instance_id=instance.uuid, reason=reason)
|
||||||
|
|
||||||
|
os_type = image_meta.get('properties', {}).get('os_type')
|
||||||
|
if os_type not in os_win_const.VTPM_SUPPORTED_OS:
|
||||||
|
reason = _('vTPM is not supported for this OS type: %(os_type)s. '
|
||||||
|
' Supported OS types: %(supported_os_types)s') % {
|
||||||
|
'os_type': os_type,
|
||||||
|
'supported_os_types':
|
||||||
|
','.join(os for os in os_win_const.VTPM_SUPPORTED_OS)}
|
||||||
|
raise exception.InstanceUnacceptable(instance_id=instance.uuid,
|
||||||
|
reason=reason)
|
||||||
|
|
||||||
|
if not self._hostutils.is_host_guarded():
|
||||||
|
reason = _('This host in not guarded.')
|
||||||
|
raise exception.InstanceUnacceptable(instance_id=instance.uuid,
|
||||||
|
reason=reason)
|
||||||
|
|
||||||
|
def _create_fsk(self, instance, fsk_filepath):
|
||||||
|
"""Writes in the fsk file all the substitution strings and their
|
||||||
|
values which will populate the unattended file used when
|
||||||
|
creating the pdk.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fsk_pairs = self._get_fsk_data(instance)
|
||||||
|
self._vmutils.populate_fsk(fsk_filepath, fsk_pairs)
|
||||||
|
|
||||||
|
def _get_fsk_data(self, instance):
|
||||||
|
"""The unattended file may contain substitution strings. Those with
|
||||||
|
their coresponding values are passed as metadata and will be added
|
||||||
|
to a fsk file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fsk_pairs = {'@@%s@@' % key.split('fsk:')[1]: value
|
||||||
|
for key, value in instance.metadata.items()
|
||||||
|
if key.startswith('fsk:')}
|
||||||
|
|
||||||
|
fsk_computername_key = '@@%s@@' % os_win_const.FSK_COMPUTERNAME
|
||||||
|
fsk_computer_name = fsk_pairs.get(fsk_computername_key)
|
||||||
|
|
||||||
|
if instance.hostname != fsk_computer_name and fsk_computer_name:
|
||||||
|
err_msg = _("The FSK mappings contain ComputerName "
|
||||||
|
"%(fsk_computer_name)s, which does not match the "
|
||||||
|
"instance name %(instance_name)s.") % {
|
||||||
|
'fsk_computer_name': fsk_computer_name,
|
||||||
|
'instance_name': instance.hostname}
|
||||||
|
raise exception.InstanceUnacceptable(instance_id=instance.uuid,
|
||||||
|
reason=err_msg)
|
||||||
|
|
||||||
|
# In case of not specifying the computer name as a FSK metadata value,
|
||||||
|
# it will be added by default in order to avoid a reboot when
|
||||||
|
# configuring the instance hostname
|
||||||
|
if not fsk_computer_name:
|
||||||
|
fsk_pairs[fsk_computername_key] = instance.hostname
|
||||||
|
return fsk_pairs
|
||||||
|
|
|
@ -269,8 +269,8 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
get_image_vm_gen.assert_called_once_with(
|
get_image_vm_gen.assert_called_once_with(
|
||||||
mock_instance.uuid, image_meta)
|
mock_instance.uuid, image_meta)
|
||||||
self._migrationops._vmops.create_instance.assert_called_once_with(
|
self._migrationops._vmops.create_instance.assert_called_once_with(
|
||||||
mock_instance, mock.sentinel.network_info, root_device,
|
self.context, mock_instance, mock.sentinel.network_info,
|
||||||
block_device_info, get_image_vm_gen.return_value,
|
root_device, block_device_info, get_image_vm_gen.return_value,
|
||||||
image_meta)
|
image_meta)
|
||||||
mock_check_attach_config_drive.assert_called_once_with(
|
mock_check_attach_config_drive.assert_called_once_with(
|
||||||
mock_instance, get_image_vm_gen.return_value)
|
mock_instance, get_image_vm_gen.return_value)
|
||||||
|
@ -446,8 +446,8 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
get_image_vm_gen.assert_called_once_with(mock_instance.uuid,
|
get_image_vm_gen.assert_called_once_with(mock_instance.uuid,
|
||||||
mock.sentinel.image_meta)
|
mock.sentinel.image_meta)
|
||||||
self._migrationops._vmops.create_instance.assert_called_once_with(
|
self._migrationops._vmops.create_instance.assert_called_once_with(
|
||||||
mock_instance, mock.sentinel.network_info, root_device,
|
self.context, mock_instance, mock.sentinel.network_info,
|
||||||
block_device_info, get_image_vm_gen.return_value,
|
root_device, block_device_info, get_image_vm_gen.return_value,
|
||||||
mock.sentinel.image_meta)
|
mock.sentinel.image_meta)
|
||||||
mock_check_attach_config_drive.assert_called_once_with(
|
mock_check_attach_config_drive.assert_called_once_with(
|
||||||
mock_instance, get_image_vm_gen.return_value)
|
mock_instance, get_image_vm_gen.return_value)
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
# Copyright 2016 Cloudbase Solutions Srl
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from nova import exception
|
||||||
|
|
||||||
|
from hyperv.nova import pdk
|
||||||
|
from hyperv.tests.unit import test_base
|
||||||
|
from six.moves import builtins
|
||||||
|
|
||||||
|
|
||||||
|
class PDKTestCase(test_base.HyperVBaseTestCase):
|
||||||
|
|
||||||
|
_FAKE_PDK_FILE_PATH = 'C:\\path\\to\\fakepdk.pdk'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(PDKTestCase, self).setUp()
|
||||||
|
self._pdk = pdk.PDK()
|
||||||
|
|
||||||
|
@mock.patch.object(builtins, 'open')
|
||||||
|
@mock.patch.object(pdk.PDK, '_get_pdk_data')
|
||||||
|
@mock.patch.object(pdk.PDK, '_get_pdk_container')
|
||||||
|
@mock.patch.object(pdk.PDK, '_get_pdk_reference')
|
||||||
|
def test_create_pdk(self, mock_get_pdk_reference, mock_get_pdk_container,
|
||||||
|
mock_get_pdk_data, mock_open):
|
||||||
|
mock_instance = mock.MagicMock()
|
||||||
|
pdk_file_handle = mock_open.return_value.__enter__.return_value
|
||||||
|
|
||||||
|
pdk_reference = mock_get_pdk_reference.return_value
|
||||||
|
pdk_container = mock_get_pdk_container.return_value
|
||||||
|
|
||||||
|
self._pdk.create_pdk(mock.sentinel.context,
|
||||||
|
mock_instance,
|
||||||
|
mock.sentinel.image_meta,
|
||||||
|
self._FAKE_PDK_FILE_PATH)
|
||||||
|
mock_get_pdk_reference.assert_called_once_with(
|
||||||
|
mock_instance, mock.sentinel.image_meta)
|
||||||
|
mock_get_pdk_container.assert_called_once_with(mock.sentinel.context,
|
||||||
|
mock_instance,
|
||||||
|
pdk_reference)
|
||||||
|
mock_get_pdk_data.assert_called_once_with(pdk_container)
|
||||||
|
pdk_file_handle.write.assert_called_once_with(
|
||||||
|
mock_get_pdk_data.return_value)
|
||||||
|
|
||||||
|
def _test_get_pdk_reference(self, pdk_reference=None,
|
||||||
|
image_meta_pdk_ref=None):
|
||||||
|
mock_instance = mock.MagicMock(
|
||||||
|
metadata={'img_pdk_reference': image_meta_pdk_ref})
|
||||||
|
image_meta = {
|
||||||
|
'properties': {'img_pdk_reference': pdk_reference}}
|
||||||
|
|
||||||
|
expected_result = image_meta_pdk_ref or pdk_reference
|
||||||
|
result = self._pdk._get_pdk_reference(mock_instance,
|
||||||
|
image_meta)
|
||||||
|
self.assertEqual(expected_result, result)
|
||||||
|
|
||||||
|
def test_get_pdk_boot_reference(self):
|
||||||
|
self._test_get_pdk_reference(
|
||||||
|
image_meta_pdk_ref=mock.sentinel.image_meta_pdk_ref)
|
||||||
|
|
||||||
|
def test_get_pdk_image_reference(self):
|
||||||
|
self._test_get_pdk_reference(pdk_reference=mock.sentinel.pdk_reference)
|
||||||
|
|
||||||
|
def test_get_pdk_no_reference(self):
|
||||||
|
image_meta = {'properties': {}}
|
||||||
|
mock_instance = mock.MagicMock(metadata={})
|
||||||
|
|
||||||
|
self.assertRaises(exception.MissingParameter,
|
||||||
|
self._pdk._get_pdk_reference,
|
||||||
|
mock_instance, image_meta)
|
||||||
|
|
||||||
|
@mock.patch('barbicanclient.client.Client')
|
||||||
|
@mock.patch('keystoneclient.session.Session')
|
||||||
|
def test_get_pdk_container(self, mock_session, mock_barbican_client):
|
||||||
|
instance = mock.MagicMock()
|
||||||
|
context = mock.MagicMock()
|
||||||
|
auth = context.get_auth_plugin.return_value
|
||||||
|
sess = mock_session.return_value
|
||||||
|
barbican_client = mock_barbican_client.return_value
|
||||||
|
barbican_client.containers.get.return_value = (
|
||||||
|
mock.sentinel.pdk_container)
|
||||||
|
|
||||||
|
result = self._pdk._get_pdk_container(context, instance,
|
||||||
|
mock.sentinel.pdk_reference)
|
||||||
|
|
||||||
|
self.assertEqual(mock.sentinel.pdk_container, result)
|
||||||
|
mock_session.assert_called_once_with(auth=auth)
|
||||||
|
mock_barbican_client.assert_called_once_with(session=sess)
|
||||||
|
|
||||||
|
@mock.patch('barbicanclient.client.Client')
|
||||||
|
@mock.patch('keystoneclient.session.Session')
|
||||||
|
def test_get_pdk_container_exception(self, mock_session,
|
||||||
|
mock_barbican_client):
|
||||||
|
instance = mock.MagicMock()
|
||||||
|
context = mock.MagicMock()
|
||||||
|
auth = context.get_auth_plugin.return_value
|
||||||
|
sess = mock_session.return_value
|
||||||
|
|
||||||
|
barbican_client = mock_barbican_client.return_value
|
||||||
|
barbican_client.containers.get.side_effect = [
|
||||||
|
exception.InvalidMetadata]
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidMetadata,
|
||||||
|
self._pdk._get_pdk_container,
|
||||||
|
context,
|
||||||
|
instance,
|
||||||
|
mock.sentinel.pdk_reference)
|
||||||
|
mock_session.assert_called_once_with(auth=auth)
|
||||||
|
mock_barbican_client.assert_called_once_with(session=sess)
|
||||||
|
|
||||||
|
def test_get_pdk_data(self):
|
||||||
|
pdk_container = mock.MagicMock()
|
||||||
|
pdk_container.secrets = {'1': mock.MagicMock(payload=b'fake_secret1'),
|
||||||
|
'2': mock.MagicMock(payload=b'fake_secret2')}
|
||||||
|
|
||||||
|
response = self._pdk._get_pdk_data(pdk_container)
|
||||||
|
expected_result = b'fake_secret1fake_secret2'
|
||||||
|
self.assertEqual(expected_result, response)
|
|
@ -32,6 +32,7 @@ import six
|
||||||
|
|
||||||
from hyperv.nova import block_device_manager
|
from hyperv.nova import block_device_manager
|
||||||
from hyperv.nova import constants
|
from hyperv.nova import constants
|
||||||
|
from hyperv.nova import pdk
|
||||||
from hyperv.nova import vmops
|
from hyperv.nova import vmops
|
||||||
from hyperv.nova import volumeops
|
from hyperv.nova import volumeops
|
||||||
from hyperv.tests import fake_instance
|
from hyperv.tests import fake_instance
|
||||||
|
@ -53,6 +54,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
FAKE_LOG = 'fake_log'
|
FAKE_LOG = 'fake_log'
|
||||||
_WIN_VERSION_6_3 = '6.3.0'
|
_WIN_VERSION_6_3 = '6.3.0'
|
||||||
_WIN_VERSION_6_4 = '6.4.0'
|
_WIN_VERSION_6_4 = '6.4.0'
|
||||||
|
_FAKE_PDK_FILE_PATH = 'C:\\path\\to\\fakepdk.pdk'
|
||||||
|
_FAKE_FSK_FILE_PATH = 'C:\\path\\to\\fakefsk.fsk'
|
||||||
|
|
||||||
ISO9660 = 'iso9660'
|
ISO9660 = 'iso9660'
|
||||||
_FAKE_CONFIGDRIVE_PATH = 'C:/fake_instance_dir/configdrive.vhd'
|
_FAKE_CONFIGDRIVE_PATH = 'C:/fake_instance_dir/configdrive.vhd'
|
||||||
|
@ -67,6 +70,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
self._vmops._vhdutils = mock.MagicMock()
|
self._vmops._vhdutils = mock.MagicMock()
|
||||||
self._vmops._pathutils = mock.MagicMock()
|
self._vmops._pathutils = mock.MagicMock()
|
||||||
self._vmops._hostutils = mock.MagicMock()
|
self._vmops._hostutils = mock.MagicMock()
|
||||||
|
self._vmops._pdk = mock.MagicMock()
|
||||||
self._vmops._serial_console_ops = mock.MagicMock()
|
self._vmops._serial_console_ops = mock.MagicMock()
|
||||||
|
|
||||||
def test_get_vif_driver_cached(self):
|
def test_get_vif_driver_cached(self):
|
||||||
|
@ -432,7 +436,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
block_device_info['ephemerals'])
|
block_device_info['ephemerals'])
|
||||||
mock_get_image_vm_gen.assert_called_once_with(
|
mock_get_image_vm_gen.assert_called_once_with(
|
||||||
mock_instance.uuid, mock_image_meta)
|
mock_instance.uuid, mock_image_meta)
|
||||||
mock_create_instance.assert_called_once_with(
|
mock_create_instance.assert_called_once_with(self.context,
|
||||||
mock_instance, [fake_network_info], root_device_info,
|
mock_instance, [fake_network_info], root_device_info,
|
||||||
block_device_info, fake_vm_gen, mock_image_meta)
|
block_device_info, fake_vm_gen, mock_image_meta)
|
||||||
mock_configdrive_required.assert_called_once_with(mock_instance)
|
mock_configdrive_required.assert_called_once_with(mock_instance)
|
||||||
|
@ -522,6 +526,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
self.assertEqual([('network-vif-plugged', mock.sentinel.vif_id2)],
|
self.assertEqual([('network-vif-plugged', mock.sentinel.vif_id2)],
|
||||||
events)
|
events)
|
||||||
|
|
||||||
|
@mock.patch.object(vmops.VMOps, '_configure_secure_vm')
|
||||||
@mock.patch.object(vmops.VMOps, '_requires_secure_boot')
|
@mock.patch.object(vmops.VMOps, '_requires_secure_boot')
|
||||||
@mock.patch.object(vmops.VMOps, '_requires_certificate')
|
@mock.patch.object(vmops.VMOps, '_requires_certificate')
|
||||||
@mock.patch('hyperv.nova.vif.get_vif_driver')
|
@mock.patch('hyperv.nova.vif.get_vif_driver')
|
||||||
|
@ -540,6 +545,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
mock_set_qos_specs, mock_get_vif_driver,
|
mock_set_qos_specs, mock_get_vif_driver,
|
||||||
mock_requires_certificate,
|
mock_requires_certificate,
|
||||||
mock_requires_secure_boot,
|
mock_requires_secure_boot,
|
||||||
|
mock_configure_secure_vm,
|
||||||
enable_instance_metrics,
|
enable_instance_metrics,
|
||||||
vm_gen=constants.VM_GEN_1, vnuma_enabled=False,
|
vm_gen=constants.VM_GEN_1, vnuma_enabled=False,
|
||||||
requires_sec_boot=True, remotefx=False):
|
requires_sec_boot=True, remotefx=False):
|
||||||
|
@ -574,6 +580,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
if remotefx is True and vm_gen == constants.VM_GEN_2:
|
if remotefx is True and vm_gen == constants.VM_GEN_2:
|
||||||
self.assertRaises(os_win_exc.HyperVException,
|
self.assertRaises(os_win_exc.HyperVException,
|
||||||
self._vmops.create_instance,
|
self._vmops.create_instance,
|
||||||
|
context=self.context,
|
||||||
instance=mock_instance,
|
instance=mock_instance,
|
||||||
network_info=[fake_network_info],
|
network_info=[fake_network_info],
|
||||||
block_device_info=block_device_info,
|
block_device_info=block_device_info,
|
||||||
|
@ -582,6 +589,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
image_meta=mock.sentinel.image_meta)
|
image_meta=mock.sentinel.image_meta)
|
||||||
else:
|
else:
|
||||||
self._vmops.create_instance(
|
self._vmops.create_instance(
|
||||||
|
context=self.context,
|
||||||
instance=mock_instance,
|
instance=mock_instance,
|
||||||
network_info=[fake_network_info],
|
network_info=[fake_network_info],
|
||||||
block_device_info=block_device_info,
|
block_device_info=block_device_info,
|
||||||
|
@ -634,6 +642,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
enable_secure_boot = self._vmops._vmutils.enable_secure_boot
|
enable_secure_boot = self._vmops._vmutils.enable_secure_boot
|
||||||
enable_secure_boot.assert_called_once_with(
|
enable_secure_boot.assert_called_once_with(
|
||||||
mock_instance.name, mock_requires_certificate.return_value)
|
mock_instance.name, mock_requires_certificate.return_value)
|
||||||
|
mock_configure_secure_vm.assert_called_once_with(self.context,
|
||||||
|
mock_instance, mock.sentinel.image_meta, requires_sec_boot)
|
||||||
|
|
||||||
def test_create_instance(self):
|
def test_create_instance(self):
|
||||||
self._test_create_instance(enable_instance_metrics=True)
|
self._test_create_instance(enable_instance_metrics=True)
|
||||||
|
@ -1826,3 +1836,159 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||||
|
|
||||||
def test_requires_certificate_os_type_none(self):
|
def test_requires_certificate_os_type_none(self):
|
||||||
self._test_requires_certificate(os_type=None)
|
self._test_requires_certificate(os_type=None)
|
||||||
|
|
||||||
|
@mock.patch.object(vmops.VMOps, '_check_vtpm_requirements')
|
||||||
|
@mock.patch.object(vmops.VMOps, '_feature_requested')
|
||||||
|
@mock.patch.object(vmops.VMOps, '_create_fsk')
|
||||||
|
@mock.patch.object(pdk.PDK, 'create_pdk')
|
||||||
|
def _test_configure_secure_vm(self, mock_create_pdk, mock_create_fsk,
|
||||||
|
mock_feature_requested,
|
||||||
|
mock_check_vtpm_requirements,
|
||||||
|
requires_shielded, requires_encryption):
|
||||||
|
instance = mock.MagicMock()
|
||||||
|
mock_tmp_file = self._vmops._pathutils.temporary_file
|
||||||
|
mock_tmp_file.return_value.__enter__.side_effect = [
|
||||||
|
self._FAKE_FSK_FILE_PATH, self._FAKE_PDK_FILE_PATH]
|
||||||
|
mock_feature_requested.side_effect = [requires_shielded,
|
||||||
|
requires_encryption]
|
||||||
|
|
||||||
|
self._vmops._configure_secure_vm(mock.sentinel.context, instance,
|
||||||
|
mock.sentinel.image_meta,
|
||||||
|
mock.sentinel.secure_boot_enabled)
|
||||||
|
|
||||||
|
expected_calls = [mock.call(instance,
|
||||||
|
mock.sentinel.image_meta,
|
||||||
|
constants.IMAGE_PROP_VTPM_SHIELDED)]
|
||||||
|
if not requires_shielded:
|
||||||
|
expected_calls.append(mock.call(instance,
|
||||||
|
mock.sentinel.image_meta,
|
||||||
|
constants.IMAGE_PROP_VTPM))
|
||||||
|
mock_feature_requested.has_calls(expected_calls)
|
||||||
|
|
||||||
|
mock_check_vtpm_requirements.assert_called_with(instance,
|
||||||
|
mock.sentinel.image_meta, mock.sentinel.secure_boot_enabled)
|
||||||
|
self._vmops._vmutils.add_vtpm.assert_called_once_with(
|
||||||
|
instance.name, self._FAKE_PDK_FILE_PATH,
|
||||||
|
shielded=requires_shielded)
|
||||||
|
self._vmops._vmutils.provision_vm.assert_called_once_with(
|
||||||
|
instance.name, self._FAKE_FSK_FILE_PATH, self._FAKE_PDK_FILE_PATH)
|
||||||
|
|
||||||
|
def test_configure_secure_vm_shielded(self):
|
||||||
|
self._test_configure_secure_vm(requires_shielded=True,
|
||||||
|
requires_encryption=True)
|
||||||
|
|
||||||
|
def test_configure_secure_vm_encryption(self):
|
||||||
|
self._test_configure_secure_vm(requires_shielded=False,
|
||||||
|
requires_encryption=True)
|
||||||
|
|
||||||
|
@mock.patch.object(vmops.VMOps, '_check_vtpm_requirements')
|
||||||
|
@mock.patch.object(vmops.VMOps, '_feature_requested')
|
||||||
|
def test_configure_regular_vm(self, mock_feature_requested,
|
||||||
|
mock_check_vtpm_requirements):
|
||||||
|
mock_feature_requested.side_effect = [False, False]
|
||||||
|
|
||||||
|
self._vmops._configure_secure_vm(mock.sentinel.context,
|
||||||
|
mock.MagicMock(),
|
||||||
|
mock.sentinel.image_meta,
|
||||||
|
mock.sentinel.secure_boot_enabled)
|
||||||
|
|
||||||
|
self.assertFalse(mock_check_vtpm_requirements.called)
|
||||||
|
|
||||||
|
def _test_feature_requested(self, image_prop, image_prop_required):
|
||||||
|
mock_instance = mock.MagicMock()
|
||||||
|
mock_image_meta = {'properties': {image_prop: image_prop_required}}
|
||||||
|
|
||||||
|
feature_requested = image_prop_required == constants.REQUIRED
|
||||||
|
|
||||||
|
result = self._vmops._feature_requested(mock_instance,
|
||||||
|
mock_image_meta,
|
||||||
|
image_prop)
|
||||||
|
self.assertEqual(feature_requested, result)
|
||||||
|
|
||||||
|
def test_vtpm_image_required(self):
|
||||||
|
self._test_feature_requested(
|
||||||
|
image_prop=constants.IMAGE_PROP_VTPM_SHIELDED,
|
||||||
|
image_prop_required=constants.REQUIRED)
|
||||||
|
|
||||||
|
def test_vtpm_image_disabled(self):
|
||||||
|
self._test_feature_requested(
|
||||||
|
image_prop=constants.IMAGE_PROP_VTPM_SHIELDED,
|
||||||
|
image_prop_required=constants.DISABLED)
|
||||||
|
|
||||||
|
def _test_check_vtpm_requirements(self, os_type='windows',
|
||||||
|
secure_boot_enabled=True,
|
||||||
|
guarded_host=True):
|
||||||
|
mock_instance = mock.MagicMock()
|
||||||
|
mock_image_meta = {'properties': {'os_type': os_type}}
|
||||||
|
guarded_host = self._vmops._hostutils.is_host_guarded.return_value
|
||||||
|
|
||||||
|
if (not secure_boot_enabled or not guarded_host or
|
||||||
|
os_type not in os_win_const.VTPM_SUPPORTED_OS):
|
||||||
|
self.assertRaises(exception.InstanceUnacceptable,
|
||||||
|
self._vmops._check_vtpm_requirements,
|
||||||
|
mock_instance,
|
||||||
|
mock_image_meta,
|
||||||
|
secure_boot_enabled)
|
||||||
|
else:
|
||||||
|
self._vmops._check_vtpm_requirements(mock_instance,
|
||||||
|
mock_image_meta,
|
||||||
|
secure_boot_enabled)
|
||||||
|
|
||||||
|
def test_vtpm_requirements_all_satisfied(self):
|
||||||
|
self._test_check_vtpm_requirements()
|
||||||
|
|
||||||
|
def test_vtpm_requirement_no_secureboot(self):
|
||||||
|
self._test_check_vtpm_requirements(secure_boot_enabled=False)
|
||||||
|
|
||||||
|
def test_vtpm_requirement_not_supported_os(self):
|
||||||
|
self._test_check_vtpm_requirements(
|
||||||
|
os_type=mock.sentinel.unsupported_os)
|
||||||
|
|
||||||
|
def test_vtpm_requirement_host_not_guarded(self):
|
||||||
|
self._test_check_vtpm_requirements(guarded_host=False)
|
||||||
|
|
||||||
|
@mock.patch.object(vmops.VMOps, '_get_fsk_data')
|
||||||
|
def test_create_fsk(self, mock_get_fsk_data):
|
||||||
|
mock_instance = mock.MagicMock()
|
||||||
|
fsk_pairs = mock_get_fsk_data.return_value
|
||||||
|
|
||||||
|
self._vmops._create_fsk(mock_instance, mock.sentinel.fsk_filename)
|
||||||
|
mock_get_fsk_data.assert_called_once_with(mock_instance)
|
||||||
|
self._vmops._vmutils.populate_fsk.assert_called_once_with(
|
||||||
|
mock.sentinel.fsk_filename, fsk_pairs)
|
||||||
|
|
||||||
|
def _test_get_fsk_data(self, metadata, instance_name,
|
||||||
|
expected_fsk_pairs=None):
|
||||||
|
mock_instance = mock.MagicMock()
|
||||||
|
mock_instance.metadata = metadata
|
||||||
|
mock_instance.hostname = instance_name
|
||||||
|
|
||||||
|
result = self._vmops._get_fsk_data(mock_instance)
|
||||||
|
self.assertEqual(expected_fsk_pairs, result)
|
||||||
|
|
||||||
|
def test_get_fsk_data_no_computername(self):
|
||||||
|
metadata = {'TimeZone': mock.sentinel.timezone}
|
||||||
|
expected_fsk_pairs = {'@@ComputerName@@': mock.sentinel.instance_name}
|
||||||
|
self._test_get_fsk_data(metadata,
|
||||||
|
mock.sentinel.instance_name,
|
||||||
|
expected_fsk_pairs)
|
||||||
|
|
||||||
|
def test_get_fsk_data_with_computername(self):
|
||||||
|
metadata = {'fsk:ComputerName': mock.sentinel.instance_name,
|
||||||
|
'fsk:TimeZone': mock.sentinel.timezone}
|
||||||
|
expected_fsk_pairs = {'@@ComputerName@@': mock.sentinel.instance_name,
|
||||||
|
'@@TimeZone@@': mock.sentinel.timezone}
|
||||||
|
self._test_get_fsk_data(metadata,
|
||||||
|
mock.sentinel.instance_name,
|
||||||
|
expected_fsk_pairs)
|
||||||
|
|
||||||
|
def test_get_fsk_data_computername_exception(self):
|
||||||
|
mock_instance = mock.MagicMock()
|
||||||
|
mock_instance.metadata = {
|
||||||
|
'fsk:ComputerName': mock.sentinel.computer_name,
|
||||||
|
'fsk:TimeZone': mock.sentinel.timezone}
|
||||||
|
mock_instance.hostname = mock.sentinel.instance_name
|
||||||
|
|
||||||
|
self.assertRaises(exception.InstanceUnacceptable,
|
||||||
|
self._vmops._get_fsk_data,
|
||||||
|
mock_instance)
|
||||||
|
|
|
@ -14,3 +14,4 @@ oslo.utils>=3.5.0 # Apache-2.0
|
||||||
oslo.i18n>=2.1.0 # Apache-2.0
|
oslo.i18n>=2.1.0 # Apache-2.0
|
||||||
|
|
||||||
eventlet!=0.18.3,>=0.18.2 # MIT
|
eventlet!=0.18.3,>=0.18.2 # MIT
|
||||||
|
python-barbicanclient>=4.0.0 # Apache-2.0
|
||||||
|
|
Loading…
Reference in New Issue