diff --git a/nova/tests/virt/vmwareapi/db_fakes.py b/nova/tests/virt/vmwareapi/db_fakes.py index a2e385cbd7f..d7f21547465 100644 --- a/nova/tests/virt/vmwareapi/db_fakes.py +++ b/nova/tests/virt/vmwareapi/db_fakes.py @@ -38,6 +38,27 @@ def stub_out_db_instance_api(stubs): 'm1.xlarge': dict(memory_mb=16384, vcpus=8, root_gb=160, flavorid=5)} + class FakeModel(object): + """Stubs out for model.""" + + def __init__(self, values): + self.values = values + + def __getattr__(self, name): + return self.values[name] + + def get(self, attr): + try: + return self.__getattr__(attr) + except KeyError: + return None + + def __getitem__(self, key): + if key in self.values: + return self.values[key] + else: + raise NotImplementedError() + def fake_instance_create(context, values): """Stubs out the db.instance_create method.""" diff --git a/nova/tests/virt/vmwareapi/test_configdrive.py b/nova/tests/virt/vmwareapi/test_configdrive.py new file mode 100644 index 00000000000..55048462507 --- /dev/null +++ b/nova/tests/virt/vmwareapi/test_configdrive.py @@ -0,0 +1,150 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 IBM Corp. +# Copyright 2011 OpenStack Foundation +# +# 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 copy +import fixtures +import mox + +from nova import context +from nova import test +import nova.tests.image.fake +from nova.tests import utils +from nova.tests.virt.vmwareapi import stubs +from nova.virt import fake +from nova.virt.vmwareapi import driver +from nova.virt.vmwareapi import fake as vmwareapi_fake +from nova.virt.vmwareapi import vmops +from nova.virt.vmwareapi import vmware_images + + +class ConfigDriveTestCase(test.TestCase): + def setUp(self): + super(ConfigDriveTestCase, self).setUp() + self.context = context.RequestContext('fake', 'fake', is_admin=False) + self.flags(host_ip='test_url', + host_username='test_username', + host_password='test_pass', + use_linked_clone=False, group='vmware') + self.flags(vnc_enabled=False) + vmwareapi_fake.reset() + stubs.set_stubs(self.stubs) + nova.tests.image.fake.stub_out_image_service(self.stubs) + self.conn = driver.VMwareVCDriver(fake.FakeVirtAPI) + self.network_info = utils.get_test_network_info() + self.image = { + 'id': 'c1c8ce3d-c2e0-4247-890c-ccf5cc1c004c', + 'disk_format': 'vhd', + 'size': 512, + } + self.test_instance = {'node': 'test_url', + 'vm_state': 'building', + 'project_id': 'fake', + 'user_id': 'fake', + 'name': '1', + 'kernel_id': '1', + 'ramdisk_id': '1', + 'mac_addresses': [ + {'address': 'de:ad:be:ef:be:ef'} + ], + 'memory_mb': 8192, + 'instance_type': 'm1.large', + 'vcpus': 4, + 'root_gb': 80, + 'image_ref': '1', + 'host': 'fake_host', + 'task_state': + 'scheduling', + 'reservation_id': 'r-3t8muvr0', + 'id': 1, + 'uuid': 'fake-uuid'} + + class FakeInstanceMetadata(object): + def __init__(self, instance, content=None, extra_md=None): + pass + + def metadata_for_config_drive(self): + return [] + + self.useFixture(fixtures.MonkeyPatch( + 'nova.api.metadata.base.InstanceMetadata', + FakeInstanceMetadata)) + + def fake_make_drive(_self, _path): + pass + # We can't actually make a config drive v2 because ensure_tree has + # been faked out + self.stubs.Set(nova.virt.configdrive.ConfigDriveBuilder, + 'make_drive', fake_make_drive) + + def fake_upload_iso_to_datastore(iso_path, instance, **kwargs): + pass + self.stubs.Set(vmware_images, + 'upload_iso_to_datastore', + fake_upload_iso_to_datastore) + + def tearDown(self): + super(ConfigDriveTestCase, self).tearDown() + vmwareapi_fake.cleanup() + nova.tests.image.fake.FakeImageService_reset() + + def test_create_vm_with_config_drive_verify_method_invocation(self): + self.instance = copy.deepcopy(self.test_instance) + self.instance['config_drive'] = True + self.mox.StubOutWithMock(vmops.VMwareVMOps, '_create_config_drive') + self.mox.StubOutWithMock(vmops.VMwareVMOps, '_attach_cdrom_to_vm') + self.conn._vmops._create_config_drive(self.instance, + mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.conn._vmops._attach_cdrom_to_vm(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.mox.ReplayAll() + # if spawn does not call the _create_config_drive or + # _attach_cdrom_to_vm call with the correct set of parameters + # then mox's VerifyAll will throw a Expected methods never called + # Exception + self.conn.spawn(self.context, self.instance, self.image, + injected_files=[], admin_password=None, + network_info=self.network_info, + block_device_info=None) + + def test_create_vm_without_config_drive(self): + self.instance = copy.deepcopy(self.test_instance) + self.instance['config_drive'] = False + self.mox.StubOutWithMock(vmops.VMwareVMOps, '_create_config_drive') + self.mox.StubOutWithMock(vmops.VMwareVMOps, '_attach_cdrom_to_vm') + self.mox.ReplayAll() + # if spawn ends up calling _create_config_drive or + # _attach_cdrom_to_vm then mox will log a Unexpected method call + # exception + self.conn.spawn(self.context, self.instance, self.image, + injected_files=[], admin_password=None, + network_info=self.network_info, + block_device_info=None) + + def test_create_vm_with_config_drive(self): + self.instance = copy.deepcopy(self.test_instance) + self.instance['config_drive'] = True + self.conn.spawn(self.context, self.instance, self.image, + injected_files=[], admin_password=None, + network_info=self.network_info, + block_device_info=None) diff --git a/nova/tests/virt/vmwareapi/test_vmwareapi_vm_util.py b/nova/tests/virt/vmwareapi/test_vmwareapi_vm_util.py index 9dbec9ff0db..2da6c8402e6 100644 --- a/nova/tests/virt/vmwareapi/test_vmwareapi_vm_util.py +++ b/nova/tests/virt/vmwareapi/test_vmwareapi_vm_util.py @@ -221,3 +221,47 @@ def test_get_datastore_ref_and_name_inaccessible_ds(self): self.assertRaises(exception.DatastoreNotFound, vm_util.get_datastore_ref_and_name, fake_session(fake_objects)) + + def test_get_cdrom_attach_config_spec(self): + + result = vm_util.get_cdrom_attach_config_spec(fake.FakeFactory(), + fake.Datastore(), + "/tmp/foo.iso", + 0) + expected = """{ + 'deviceChange': [ + { + 'device': { + 'connectable': { + 'allowGuestControl': False, + 'startConnected': True, + 'connected': True, + 'obj_name': 'ns0: VirtualDeviceConnectInfo' + }, + 'backing': { + 'datastore': { + "summary.type": "VMFS", + "summary.freeSpace": 536870912000, + "summary.capacity": 1099511627776, + "summary.accessible":true, + "summary.name": "fake-ds" + }, + 'fileName': '/tmp/foo.iso', + 'obj_name': 'ns0: VirtualCdromIsoBackingInfo' + }, + 'controllerKey': 200, + 'unitNumber': 0, + 'key': -1, + 'obj_name': 'ns0: VirtualCdrom' + }, + 'operation': 'add', + 'obj_name': 'ns0: VirtualDeviceConfigSpec' + } + ], + 'obj_name': 'ns0: VirtualMachineConfigSpec' +} +""" + + expected = re.sub(r'\s+', '', expected) + result = re.sub(r'\s+', '', repr(result)) + self.assertEqual(expected, result) diff --git a/nova/virt/vmwareapi/driver.py b/nova/virt/vmwareapi/driver.py index 293fe0ed714..10b6521cada 100644 --- a/nova/virt/vmwareapi/driver.py +++ b/nova/virt/vmwareapi/driver.py @@ -179,8 +179,8 @@ def list_instances(self): def spawn(self, context, instance, image_meta, injected_files, admin_password, network_info=None, block_device_info=None): """Create VM instance.""" - self._vmops.spawn(context, instance, image_meta, network_info, - block_device_info) + self._vmops.spawn(context, instance, image_meta, injected_files, + admin_password, network_info, block_device_info) def snapshot(self, context, instance, name, update_task_state): """Create snapshot from a running VM instance.""" diff --git a/nova/virt/vmwareapi/fake.py b/nova/virt/vmwareapi/fake.py index 7d01037315e..a2e895d87e1 100644 --- a/nova/virt/vmwareapi/fake.py +++ b/nova/virt/vmwareapi/fake.py @@ -27,6 +27,7 @@ from nova import exception from nova.openstack.common.gettextutils import _ +from nova.openstack.common import jsonutils from nova.openstack.common import log as logging from nova.virt.vmwareapi import error_util @@ -200,12 +201,19 @@ def __getattr__(self, attr): raise exception.NovaException(msg % {'attr': attr, 'name': self.objName}) + def __repr__(self): + return jsonutils.dumps(dict([(elem.name, elem.val) + for elem in self.propSet])) + class DataObject(object): """Data object base class.""" def __init__(self, obj_name=None): self.obj_name = obj_name + def __repr__(self): + return str(self.__dict__) + class HostInternetScsiHba(): pass @@ -284,6 +292,9 @@ def reconfig(self, factory, val): setting of the Virtual Machine object. """ try: + if len(val.deviceChange) < 2: + return + # Case of Reconfig of VM to attach disk controller_key = val.deviceChange[1].device.controllerKey filename = val.deviceChange[1].device.backing.fileName diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index c20fed7c0e6..7e711f7bc0a 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -224,6 +224,29 @@ def get_vmdk_attach_config_spec(client_factory, return config_spec +def get_cdrom_attach_config_spec(client_factory, + datastore, + file_path, + cdrom_unit_number): + """Builds and returns the cdrom attach config spec.""" + config_spec = client_factory.create('ns0:VirtualMachineConfigSpec') + + device_config_spec = [] + # For IDE devices, there are these two default controllers created in the + # VM having keys 200 and 201 + controller_key = 200 + virtual_device_config_spec = create_virtual_cdrom_spec(client_factory, + datastore, + controller_key, + file_path, + cdrom_unit_number) + + device_config_spec.append(virtual_device_config_spec) + + config_spec.deviceChange = device_config_spec + return config_spec + + def get_vmdk_detach_config_spec(client_factory, device): """Builds the vmdk detach config spec.""" config_spec = client_factory.create('ns0:VirtualMachineConfigSpec') @@ -320,6 +343,39 @@ def get_rdm_create_spec(client_factory, device, adapter_type="lsiLogic", return create_vmdk_spec +def create_virtual_cdrom_spec(client_factory, + datastore, + controller_key, + file_path, + cdrom_unit_number): + """Builds spec for the creation of a new Virtual CDROM to the VM.""" + config_spec = client_factory.create( + 'ns0:VirtualDeviceConfigSpec') + config_spec.operation = "add" + + cdrom = client_factory.create('ns0:VirtualCdrom') + + cdrom_device_backing = client_factory.create( + 'ns0:VirtualCdromIsoBackingInfo') + cdrom_device_backing.datastore = datastore + cdrom_device_backing.fileName = file_path + + cdrom.backing = cdrom_device_backing + cdrom.controllerKey = controller_key + cdrom.unitNumber = cdrom_unit_number + cdrom.key = -1 + + connectable_spec = client_factory.create('ns0:VirtualDeviceConnectInfo') + connectable_spec.startConnected = True + connectable_spec.allowGuestControl = False + connectable_spec.connected = True + + cdrom.connectable = connectable_spec + + config_spec.device = cdrom + return config_spec + + def create_virtual_disk_spec(client_factory, controller_key, disk_type="preallocated", file_path=None, diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index b20d33c053f..97f4e13e860 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -31,6 +31,7 @@ from oslo.config import cfg +from nova.api.metadata import base as instance_metadata from nova import block_device from nova import compute from nova.compute import power_state @@ -41,6 +42,7 @@ from nova.openstack.common.gettextutils import _ from nova.openstack.common import log as logging from nova import utils +from nova.virt import configdrive from nova.virt import driver from nova.virt.vmwareapi import vif as vmwarevif from nova.virt.vmwareapi import vim_util @@ -124,8 +126,8 @@ def list_instances(self): LOG.debug(_("Got total of %s instances") % str(len(lst_vm_names))) return lst_vm_names - def spawn(self, context, instance, image_meta, network_info, - block_device_info=None): + def spawn(self, context, instance, image_meta, injected_files, + admin_password, network_info, block_device_info=None): """ Creates a VM instance. @@ -371,6 +373,9 @@ def _copy_virtual_disk(): uploaded_vmdk_path = vm_util.build_datastore_path(data_store_name, uploaded_vmdk_name) + session_vim = self._session._get_vim() + cookies = session_vim.client.options.transport.cookiejar + if not (linked_clone and self._check_if_folder_file_exists( data_store_ref, data_store_name, upload_folder, upload_name + ".vmdk")): @@ -396,8 +401,6 @@ def _copy_virtual_disk(): _create_virtual_disk() _delete_disk_file(flat_uploaded_vmdk_path) - cookies = \ - self._session._get_vim().client.options.transport.cookiejar _fetch_image_on_esx_datastore() if disk_type == "sparse": @@ -415,6 +418,23 @@ def _copy_virtual_disk(): vm_ref, instance, adapter_type, disk_type, uploaded_vmdk_path, vmdk_file_size_in_kb, linked_clone) + + if configdrive.required_by(instance): + uploaded_iso_path = self._create_config_drive(instance, + injected_files, + admin_password, + data_store_name, + instance['uuid'], + cookies) + uploaded_iso_path = vm_util.build_datastore_path( + data_store_name, + uploaded_iso_path) + self._attach_cdrom_to_vm( + vm_ref, instance, + data_store_ref, + uploaded_iso_path, + 1 if adapter_type in ['ide'] else 0) + else: # Attach the root disk to the VM. root_disk = driver.block_device_info_get_mapping( @@ -434,6 +454,66 @@ def _power_on_vm(): LOG.debug(_("Powered on the VM instance"), instance=instance) _power_on_vm() + def _create_config_drive(self, instance, injected_files, admin_password, + data_store_name, upload_folder, cookies): + if CONF.config_drive_format != 'iso9660': + reason = (_('Invalid config_drive_format "%s"') % + CONF.config_drive_format) + raise exception.InstancePowerOnFailure(reason=reason) + + LOG.info(_('Using config drive for instance'), instance=instance) + extra_md = {} + if admin_password: + extra_md['admin_pass'] = admin_password + + inst_md = instance_metadata.InstanceMetadata(instance, + content=injected_files, + extra_md=extra_md) + try: + with configdrive.ConfigDriveBuilder(instance_md=inst_md) as cdb: + with utils.tempdir() as tmp_path: + tmp_file = os.path.join(tmp_path, 'configdrive.iso') + cdb.make_drive(tmp_file) + dc_name = self._get_datacenter_ref_and_name()[1] + + upload_iso_path = "%s/configdrive.iso" % ( + upload_folder) + vmware_images.upload_iso_to_datastore( + tmp_file, instance, + host=self._session._host_ip, + data_center_name=dc_name, + datastore_name=data_store_name, + cookies=cookies, + file_path=upload_iso_path) + return upload_iso_path + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_('Creating config drive failed with error: %s'), + e, instance=instance) + + def _attach_cdrom_to_vm(self, vm_ref, instance, + datastore, file_path, + cdrom_unit_number): + """Attach cdrom to VM by reconfiguration.""" + instance_name = instance['name'] + instance_uuid = instance['uuid'] + client_factory = self._session._get_vim().client.factory + vmdk_attach_config_spec = vm_util.get_cdrom_attach_config_spec( + client_factory, datastore, file_path, + cdrom_unit_number) + + LOG.debug(_("Reconfiguring VM instance %(instance_name)s to attach " + "cdrom %(file_path)s"), + {'instance_name': instance_name, 'file_path': file_path}) + reconfig_task = self._session._call_method( + self._session._get_vim(), + "ReconfigVM_Task", vm_ref, + spec=vmdk_attach_config_spec) + self._session._wait_for_task(instance_uuid, reconfig_task) + LOG.debug(_("Reconfigured VM instance %(instance_name)s to attach " + "cdrom %(file_path)s"), + {'instance_name': instance_name, 'file_path': file_path}) + def snapshot(self, context, instance, snapshot_name, update_task_state): """Create snapshot from a running VM instance. @@ -804,7 +884,8 @@ def rescue(self, context, instance, network_info, image_meta): r_instance = copy.deepcopy(instance) r_instance['name'] = r_instance['name'] + self._rescue_suffix r_instance['uuid'] = r_instance['uuid'] + self._rescue_suffix - self.spawn(context, r_instance, image_meta, network_info) + self.spawn(context, r_instance, image_meta, + None, None, network_info) # Attach vmdk to the rescue VM hardware_devices = self._session._call_method(vim_util, diff --git a/nova/virt/vmwareapi/vmware_images.py b/nova/virt/vmwareapi/vmware_images.py index 831bad394b0..6fd52022e2a 100644 --- a/nova/virt/vmwareapi/vmware_images.py +++ b/nova/virt/vmwareapi/vmware_images.py @@ -19,6 +19,8 @@ Utility functions for Image transfer. """ +import os + from nova import exception from nova.image import glance from nova.openstack.common.gettextutils import _ @@ -88,6 +90,31 @@ def start_transfer(context, read_file_handle, data_size, write_file_handle.close() +def upload_iso_to_datastore(iso_path, instance, **kwargs): + LOG.debug(_("Uploading iso %s to datastore") % iso_path, + instance=instance) + with open(iso_path, 'r') as iso_file: + write_file_handle = read_write_util.VMwareHTTPWriteFile( + kwargs.get("host"), + kwargs.get("data_center_name"), + kwargs.get("datastore_name"), + kwargs.get("cookies"), + kwargs.get("file_path"), + os.fstat(iso_file.fileno()).st_size) + + LOG.debug(_("Uploading iso of size : %s ") % + os.fstat(iso_file.fileno()).st_size) + block_size = 0x10000 + data = iso_file.read(block_size) + while len(data) > 0: + write_file_handle.write(data) + data = iso_file.read(block_size) + write_file_handle.close() + + LOG.debug(_("Uploaded iso %s to datastore") % iso_path, + instance=instance) + + def fetch_image(context, image, instance, **kwargs): """Download image from the glance image server.""" LOG.debug(_("Downloading image %s from glance image server") % image,