diff --git a/nova/tests/unit/virt/xenapi/image/test_vdi_through_dev.py b/nova/tests/unit/virt/xenapi/image/test_vdi_through_dev.py index 964707d5f140..220cd8ffc205 100644 --- a/nova/tests/unit/virt/xenapi/image/test_vdi_through_dev.py +++ b/nova/tests/unit/virt/xenapi/image/test_vdi_through_dev.py @@ -58,7 +58,7 @@ class TestUploadToGlanceAsRawTgz(test.NoDBTestCase): self.mox.StubOutWithMock(vdi_through_dev, 'utils') store._get_vdi_ref().AndReturn('vdi_ref') - vdi_through_dev.vm_utils.vdi_attached_here( + vdi_through_dev.vm_utils.vdi_attached( 'session', 'vdi_ref', read_only=True).AndReturn( fake_context('dev')) vdi_through_dev.utils.make_dev_path('dev').AndReturn('devpath') diff --git a/nova/tests/unit/virt/xenapi/plugins/test_partition_utils.py b/nova/tests/unit/virt/xenapi/plugins/test_partition_utils.py new file mode 100644 index 000000000000..3761588d5cd1 --- /dev/null +++ b/nova/tests/unit/virt/xenapi/plugins/test_partition_utils.py @@ -0,0 +1,102 @@ +# Copyright (c) 2016 OpenStack Foundation +# 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 test +from nova.tests.unit.virt.xenapi.plugins import plugin_test + + +class PartitionUtils(plugin_test.PluginTestBase): + def setUp(self): + super(PartitionUtils, self).setUp() + self.partition_utils = self.load_plugin("partition_utils.py") + + def test_wait_for_dev_ok(self): + mock_sleep = self.mock_patch_object(self.partition_utils.time, + 'sleep') + mock_exists = self.mock_patch_object(self.partition_utils.os.path, + 'exists') + mock_exists.side_effect = [False, True] + ret = self.partition_utils.wait_for_dev('session', '/fake', 2) + + self.assertEqual(1, mock_sleep.call_count) + self.assertEqual(ret, "/fake") + + def test_wait_for_dev_timeout(self): + mock_sleep = self.mock_patch_object(self.partition_utils.time, + 'sleep') + mock_exists = self.mock_patch_object(self.partition_utils.os.path, + 'exists') + mock_exists.side_effect = [False, False, True] + ret = self.partition_utils.wait_for_dev('session', '/fake', 2) + + self.assertEqual(2, mock_sleep.call_count) + self.assertEqual(ret, "") + + def test_mkfs_removes_partitions_ok(self): + mock_run = self.mock_patch_object(self.partition_utils.utils, + 'run_command') + mock__mkfs = self.mock_patch_object(self.partition_utils, '_mkfs') + + self.partition_utils.mkfs('session', 'fakedev', '1', 'ext3', 'label') + mock__mkfs.assert_called_with('ext3', '/dev/mapper/fakedevp1', + 'label') + expected_calls = [mock.call(['kpartx', '-avspp', '/dev/fakedev'])] + expected_calls.append(mock.call(['kpartx', '-dvspp', '/dev/fakedev'])) + mock_run.assert_has_calls(expected_calls) + + def test_mkfs_removes_partitions_exc(self): + mock_run = self.mock_patch_object(self.partition_utils.utils, + 'run_command') + mock__mkfs = self.mock_patch_object(self.partition_utils, '_mkfs') + mock__mkfs.side_effect = test.TestingException() + + self.assertRaises(test.TestingException, self.partition_utils.mkfs, + 'session', 'fakedev', '1', 'ext3', 'label') + expected_calls = [mock.call(['kpartx', '-avspp', '/dev/fakedev'])] + expected_calls.append(mock.call(['kpartx', '-dvspp', '/dev/fakedev'])) + mock_run.assert_has_calls(expected_calls) + + def test_mkfs_ext3_no_label(self): + mock_run = self.mock_patch_object(self.partition_utils.utils, + 'run_command') + + self.partition_utils._mkfs('ext3', '/dev/sda1', None) + mock_run.assert_called_with(['mkfs', '-t', 'ext3', '-F', '/dev/sda1']) + + def test_mkfs_ext3(self): + mock_run = self.mock_patch_object(self.partition_utils.utils, + 'run_command') + + self.partition_utils._mkfs('ext3', '/dev/sda1', 'label') + mock_run.assert_called_with(['mkfs', '-t', 'ext3', '-F', '-L', + 'label', '/dev/sda1']) + + def test_mkfs_swap(self): + mock_run = self.mock_patch_object(self.partition_utils.utils, + 'run_command') + + self.partition_utils._mkfs('swap', '/dev/sda1', 'ignored') + mock_run.assert_called_with(['mkswap', '/dev/sda1']) + + def test_make_partition(self): + mock_run = self.mock_patch_object(self.partition_utils.utils, + 'run_command') + + self.partition_utils.make_partition('session', 'dev', 'start', '-') + + mock_run.assert_called_with(['sfdisk', '-uS', '/dev/dev'], + 'start,;\n') diff --git a/nova/tests/unit/virt/xenapi/stubs.py b/nova/tests/unit/virt/xenapi/stubs.py index 641f97adc81d..1a27f5fa0246 100644 --- a/nova/tests/unit/virt/xenapi/stubs.py +++ b/nova/tests/unit/virt/xenapi/stubs.py @@ -257,7 +257,7 @@ def stub_out_vm_methods(stubs): def fake_generate_ephemeral(*args): pass - def fake_wait_for_device(dev): + def fake_wait_for_device(session, dev, dom0, max_seconds): pass stubs.Set(vmops.VMOps, "_acquire_bootlock", fake_acquire_bootlock) diff --git a/nova/tests/unit/virt/xenapi/test_vm_utils.py b/nova/tests/unit/virt/xenapi/test_vm_utils.py index c00608d45f2e..3ade97f1d2b1 100644 --- a/nova/tests/unit/virt/xenapi/test_vm_utils.py +++ b/nova/tests/unit/virt/xenapi/test_vm_utils.py @@ -41,7 +41,6 @@ from nova.tests.unit import fake_flavor from nova.tests.unit import fake_instance from nova.tests.unit.objects import test_flavor from nova.tests.unit.virt.xenapi import stubs -from nova.tests.unit.virt.xenapi import test_xenapi from nova.tests import uuidsentinel as uuids from nova import utils from nova.virt import hardware @@ -168,8 +167,8 @@ class GenerateConfigDriveTestCase(VMUtilsTestBase): 'configdrive', 64 * units.Mi).AndReturn('vdi_ref') - self.mox.StubOutWithMock(vm_utils, 'vdi_attached_here') - vm_utils.vdi_attached_here( + self.mox.StubOutWithMock(vm_utils, 'vdi_attached') + vm_utils.vdi_attached( 'session', 'vdi_ref', read_only=False).AndReturn( contextified('mounted_dev')) @@ -205,7 +204,7 @@ class GenerateConfigDriveTestCase(VMUtilsTestBase): 'userdevice', "nw_info") @mock.patch.object(vm_utils, "destroy_vdi") - @mock.patch.object(vm_utils, "vdi_attached_here") + @mock.patch.object(vm_utils, "vdi_attached") @mock.patch.object(vm_utils, "create_vdi") @mock.patch.object(vm_utils, "safe_find_sr") def test_vdi_cleaned_up(self, mock_find, mock_create_vdi, mock_attached, @@ -1065,119 +1064,140 @@ class VDIOtherConfigTestCase(VMUtilsTestBase): class GenerateDiskTestCase(VMUtilsTestBase): - def setUp(self): - super(GenerateDiskTestCase, self).setUp() - self.fixture = self.useFixture(config_fixture.Config(lockutils.CONF)) - self.fixture.config(disable_process_locking=True, - group='oslo_concurrency') - self.flags(instance_name_template='%d', - firewall_driver='nova.virt.xenapi.firewall.' - 'Dom0IptablesFirewallDriver') - self.flags(connection_url='test_url', - connection_password='test_pass', - group='xenserver') - stubs.stubout_session(self.stubs, fake.SessionBase) - driver = xenapi_conn.XenAPIDriver(False) - self.session = driver._session - self.session.is_local_connection = False - self.vm_ref = fake.create_vm("foo", "Running") - def tearDown(self): - super(GenerateDiskTestCase, self).tearDown() - fake.destroy_vm(self.vm_ref) + @mock.patch.object(vm_utils, 'vdi_attached') + @mock.patch.object(vm_utils.utils, 'mkfs', + side_effect = test.TestingException()) + @mock.patch.object(vm_utils, '_get_dom0_ref', return_value='dom0_ref') + @mock.patch.object(vm_utils, 'safe_find_sr', return_value='sr_ref') + @mock.patch.object(vm_utils, 'create_vdi', return_value='vdi_ref') + @mock.patch.object(vm_utils, 'create_vbd') + def test_generate_disk_with_no_fs_given(self, mock_create_vbd, + mock_create_vdi, mock_findsr, + mock_dom0ref, mock_mkfs, + mock_attached_here): + session = _get_fake_session() + vdi_ref = mock.MagicMock() + mock_attached_here.return_value = vdi_ref - def _expect_parted_calls(self): - self.mox.StubOutWithMock(utils, "execute") - self.mox.StubOutWithMock(utils, "trycmd") - self.mox.StubOutWithMock(vm_utils, "destroy_vdi") - self.mox.StubOutWithMock(vm_utils.os.path, "exists") - if self.session.is_local_connection: - utils.execute('parted', '--script', '/dev/fakedev', 'mklabel', - 'msdos', check_exit_code=False, run_as_root=True) - utils.execute('parted', '--script', '/dev/fakedev', '--', 'mkpart', - 'primary', '2048s', '-0', - check_exit_code=False, run_as_root=True) - vm_utils.os.path.exists('/dev/mapper/fakedev1').AndReturn(True) - utils.trycmd('kpartx', '-a', '/dev/fakedev', - discard_warnings=True, run_as_root=True) - else: - utils.execute('parted', '--script', '/dev/fakedev', 'mklabel', - 'msdos', check_exit_code=True, run_as_root=True) - utils.execute('parted', '--script', '/dev/fakedev', '--', 'mkpart', - 'primary', '2048s', '-0', - check_exit_code=True, run_as_root=True) + instance = {'uuid': 'fake_uuid'} + vm_utils._generate_disk(session, instance, 'vm_ref', '2', + 'name', 'user', 10, None, None) - def _check_vdi(self, vdi_ref, check_attached=True): - vdi_rec = self.session.call_xenapi("VDI.get_record", vdi_ref) - self.assertEqual(str(10 * units.Mi), vdi_rec["virtual_size"]) - if check_attached: - vbd_ref = vdi_rec["VBDs"][0] - vbd_rec = self.session.call_xenapi("VBD.get_record", vbd_ref) - self.assertEqual(self.vm_ref, vbd_rec['VM']) - else: - self.assertEqual(0, len(vdi_rec["VBDs"])) + mock_attached_here.assert_called_once_with(session, 'vdi_ref', + read_only=False, + dom0=True) - @test_xenapi.stub_vm_utils_with_vdi_attached_here - def test_generate_disk_with_no_fs_given(self): - self._expect_parted_calls() + mock_create_vbd.assert_called_with(session, 'vm_ref', 'vdi_ref', '2', + bootable=False) + + @mock.patch.object(vm_utils, 'vdi_attached') + @mock.patch.object(vm_utils.utils, 'mkfs') + @mock.patch.object(vm_utils, '_get_dom0_ref', return_value='dom0_ref') + @mock.patch.object(vm_utils, 'safe_find_sr', return_value='sr_ref') + @mock.patch.object(vm_utils, 'create_vdi', return_value='vdi_ref') + @mock.patch.object(vm_utils.utils, 'make_dev_path', + return_value='/dev/fake_devp1') + @mock.patch.object(vm_utils, 'create_vbd') + def test_generate_disk_swap(self, mock_create_vbd, mock_make_path, + mock_create_vdi, + mock_findsr, mock_dom0ref, mock_mkfs, + mock_attached_here): + session = _get_fake_session() + vdi_dev = mock.MagicMock() + mock_attached_here.return_value = vdi_dev + vdi_dev.__enter__.return_value = 'fakedev' + instance = {'uuid': 'fake_uuid'} + + vm_utils._generate_disk(session, instance, 'vm_ref', '2', + 'name', 'user', 10, 'swap', + 'swap-1') + + mock_attached_here.assert_any_call(session, 'vdi_ref', + read_only=False, + dom0=True) + + # As swap is supported in dom0, mkfs will run there + session.call_plugin_serialized.assert_any_call( + 'partition_utils.py', 'mkfs', 'fakedev', '1', 'swap', 'swap-1') + + mock_create_vbd.assert_called_with(session, 'vm_ref', 'vdi_ref', '2', + bootable=False) + + @mock.patch.object(vm_utils, 'vdi_attached') + @mock.patch.object(vm_utils.utils, 'mkfs') + @mock.patch.object(vm_utils, '_get_dom0_ref', return_value='dom0_ref') + @mock.patch.object(vm_utils, 'safe_find_sr', return_value='sr_ref') + @mock.patch.object(vm_utils, 'create_vdi', return_value='vdi_ref') + @mock.patch.object(vm_utils.utils, 'make_dev_path', + return_value='/dev/fake_devp1') + @mock.patch.object(vm_utils, 'create_vbd') + def test_generate_disk_ephemeral(self, mock_create_vbd, mock_make_path, + mock_create_vdi, mock_findsr, + mock_dom0ref, mock_mkfs, + mock_attached_here): + session = _get_fake_session() + vdi_ref = mock.MagicMock() + mock_attached_here.return_value = vdi_ref + instance = {'uuid': 'fake_uuid'} + + vm_utils._generate_disk(session, instance, 'vm_ref', '2', + 'name', 'ephemeral', 10, 'ext4', + 'ephemeral-1') + + mock_attached_here.assert_any_call(session, 'vdi_ref', + read_only=False, + dom0=True) + + # As ext4 is not supported in dom0, mkfs will run in domU + mock_attached_here.assert_any_call(session, 'vdi_ref', + read_only=False) + mock_mkfs.assert_called_with('ext4', '/dev/fake_devp1', + 'ephemeral-1', run_as_root=True) + + mock_create_vbd.assert_called_with(session, 'vm_ref', 'vdi_ref', '2', + bootable=False) + + @mock.patch.object(vm_utils, 'safe_find_sr', return_value='sr_ref') + @mock.patch.object(vm_utils, 'create_vdi', return_value='vdi_ref') + @mock.patch.object(vm_utils, '_get_dom0_ref', + side_effect = test.TestingException()) + @mock.patch.object(vm_utils, 'safe_destroy_vdis') + def test_generate_disk_ensure_cleanup_called(self, mock_destroy_vdis, + mock_dom0ref, + mock_create_vdi, + mock_findsr): + session = _get_fake_session() + instance = {'uuid': 'fake_uuid'} + + self.assertRaises(test.TestingException, vm_utils._generate_disk, + session, instance, None, '2', 'name', 'user', 10, + None, None) + + mock_destroy_vdis.assert_called_once_with(session, ['vdi_ref']) + + @mock.patch.object(vm_utils, 'safe_find_sr', return_value='sr_ref') + @mock.patch.object(vm_utils, 'create_vdi', return_value='vdi_ref') + @mock.patch.object(vm_utils, 'vdi_attached') + @mock.patch.object(vm_utils, '_get_dom0_ref', return_value='dom0_ref') + @mock.patch.object(vm_utils, 'create_vbd') + def test_generate_disk_ephemeral_no_vmref(self, mock_create_vbd, + mock_dom0_ref, + mock_attached_here, + mock_create_vdi, + mock_findsr): + session = _get_fake_session() + vdi_ref = mock.MagicMock() + mock_attached_here.return_value = vdi_ref + instance = {'uuid': 'fake_uuid'} - self.mox.ReplayAll() vdi_ref = vm_utils._generate_disk( - self.session, {"uuid": "fake_uuid"}, - self.vm_ref, "2", "name", "user", 10, None, None) - self._check_vdi(vdi_ref) + session, instance, + None, None, 'name', 'user', 10, None, None) - @test_xenapi.stub_vm_utils_with_vdi_attached_here - def test_generate_disk_swap(self): - self._expect_parted_calls() - utils.execute('mkswap', '/dev/fakedev1', run_as_root=True) - - self.mox.ReplayAll() - vdi_ref = vm_utils._generate_disk( - self.session, {"uuid": "fake_uuid"}, - self.vm_ref, "2", "name", "swap", 10, "swap", None) - self._check_vdi(vdi_ref) - - @test_xenapi.stub_vm_utils_with_vdi_attached_here - def test_generate_disk_ephemeral(self): - self._expect_parted_calls() - utils.execute('mkfs', '-t', 'ext4', '-F', '-L', 'ephemeral', - '/dev/fakedev1', run_as_root=True) - - self.mox.ReplayAll() - vdi_ref = vm_utils._generate_disk( - self.session, {"uuid": "fake_uuid"}, self.vm_ref, - "4", "name", "ephemeral", 10, "ext4", "ephemeral") - self._check_vdi(vdi_ref) - - @test_xenapi.stub_vm_utils_with_vdi_attached_here - def test_generate_disk_ensure_cleanup_called(self): - self._expect_parted_calls() - utils.execute( - 'mkfs', '-t', 'ext4', '-F', '-L', 'ephemeral', '/dev/fakedev1', - run_as_root=True).AndRaise(test.TestingException) - vm_utils.destroy_vdi( - self.session, - mox.IgnoreArg()).AndRaise(exception.StorageError(reason="")) - - self.mox.ReplayAll() - self.assertRaises( - test.TestingException, vm_utils._generate_disk, - self.session, {"uuid": "fake_uuid"}, - self.vm_ref, "4", "name", "ephemeral", 10, "ext4", "ephemeral") - - @test_xenapi.stub_vm_utils_with_vdi_attached_here - def test_generate_disk_ephemeral_local_not_attached(self): - self.session.is_local_connection = True - self._expect_parted_calls() - utils.execute('mkfs', '-t', 'ext4', '-F', '-L', 'ephemeral', - '/dev/mapper/fakedev1', run_as_root=True) - - self.mox.ReplayAll() - vdi_ref = vm_utils._generate_disk( - self.session, {"uuid": "fake_uuid"}, - None, "4", "name", "ephemeral", 10, "ext4", "ephemeral") - self._check_vdi(vdi_ref, check_attached=False) + mock_attached_here.assert_called_once_with(session, 'vdi_ref', + read_only=False, dom0=True) + self.assertFalse(mock_create_vbd.called) class GenerateEphemeralTestCase(VMUtilsTestBase): @@ -1827,22 +1847,6 @@ class GetAllVdisTestCase(VMUtilsTestBase): session.call_xenapi.assert_called_once_with("SR.get_VDIs", sr_ref) -class VDIAttachedHere(VMUtilsTestBase): - @mock.patch.object(vm_utils, 'destroy_vbd') - @mock.patch.object(vm_utils, '_get_this_vm_ref') - @mock.patch.object(vm_utils, 'create_vbd') - @mock.patch.object(vm_utils, '_remap_vbd_dev') - @mock.patch.object(vm_utils, '_wait_for_device') - @mock.patch.object(utils, 'execute') - def test_sync_called(self, mock_execute, mock_wait_for_device, - mock_remap_vbd_dev, mock_create_vbd, - mock_get_this_vm_ref, mock_destroy_vbd): - session = _get_fake_session() - with vm_utils.vdi_attached_here(session, 'vdi_ref'): - pass - mock_execute.assert_called_with('sync', run_as_root=True) - - class SnapshotAttachedHereTestCase(VMUtilsTestBase): @mock.patch.object(vm_utils, '_snapshot_attached_here_impl') def test_snapshot_attached_here(self, mock_impl): diff --git a/nova/tests/unit/virt/xenapi/test_xenapi.py b/nova/tests/unit/virt/xenapi/test_xenapi.py index cb65c2c9653e..6665c6f6a718 100644 --- a/nova/tests/unit/virt/xenapi/test_xenapi.py +++ b/nova/tests/unit/virt/xenapi/test_xenapi.py @@ -178,30 +178,30 @@ def get_fake_device_info(): return fake -def stub_vm_utils_with_vdi_attached_here(function): - """vm_utils.with_vdi_attached_here needs to be stubbed out because it +def stub_vm_utils_with_vdi_attached(function): + """vm_utils.with_vdi_attached needs to be stubbed out because it calls down to the filesystem to attach a vdi. This provides a decorator to handle that. """ @functools.wraps(function) def decorated_function(self, *args, **kwargs): @contextlib.contextmanager - def fake_vdi_attached_here(*args, **kwargs): + def fake_vdi_attached(*args, **kwargs): fake_dev = 'fakedev' yield fake_dev def fake_image_download(*args, **kwargs): pass - orig_vdi_attached_here = vm_utils.vdi_attached_here + orig_vdi_attached = vm_utils.vdi_attached orig_image_download = fake_image._FakeImageService.download try: - vm_utils.vdi_attached_here = fake_vdi_attached_here + vm_utils.vdi_attached = fake_vdi_attached fake_image._FakeImageService.download = fake_image_download return function(self, *args, **kwargs) finally: fake_image._FakeImageService.download = orig_image_download - vm_utils.vdi_attached_here = orig_vdi_attached_here + vm_utils.vdi_attached = orig_vdi_attached return decorated_function @@ -772,6 +772,9 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): 'ipxe', 'inject', '/sr/path', mox.IgnoreArg(), 'http://boot.example.com', '192.168.1.100', '255.255.255.0', '192.168.1.1', '192.168.1.3', '/root/mkisofs') + self.conn._session.call_plugin_serialized('partition_utils.py', + 'make_partition', + 'fakedev', '2048', '-') self.mox.ReplayAll() self._test_spawn(IMAGE_IPXE_ISO, None, None) @@ -781,8 +784,11 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): ipxe_boot_menu_url='http://boot.example.com', group='xenserver') - # call_plugin_serialized shouldn't be called + # ipxe inject shouldn't be called self.mox.StubOutWithMock(self.conn._session, 'call_plugin_serialized') + self.conn._session.call_plugin_serialized('partition_utils.py', + 'make_partition', + 'fakedev', '2048', '-') self.mox.ReplayAll() self._test_spawn(IMAGE_IPXE_ISO, None, None) @@ -792,8 +798,11 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): ipxe_boot_menu_url=None, group='xenserver') - # call_plugin_serialized shouldn't be called + # ipxe inject shouldn't be called self.mox.StubOutWithMock(self.conn._session, 'call_plugin_serialized') + self.conn._session.call_plugin_serialized('partition_utils.py', + 'make_partition', + 'fakedev', '2048', '-') self.mox.ReplayAll() self._test_spawn(IMAGE_IPXE_ISO, None, None) @@ -803,8 +812,11 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): ipxe_boot_menu_url='http://boot.example.com', group='xenserver') - # call_plugin_serialized shouldn't be called + # ipxe inject shouldn't be called self.mox.StubOutWithMock(self.conn._session, 'call_plugin_serialized') + self.conn._session.call_plugin_serialized('partition_utils.py', + 'make_partition', + 'fakedev', '2048', '-') self.mox.ReplayAll() self._test_spawn(IMAGE_IPXE_ISO, None, None) @@ -1871,7 +1883,7 @@ class XenAPIMigrateInstance(stubs.XenAPITestBase): dict(base_copy='hurr', cow='durr'), network_info, image_meta, resize_instance=False) - @stub_vm_utils_with_vdi_attached_here + @stub_vm_utils_with_vdi_attached def test_migrate_too_many_partitions_no_resize_down(self): instance = self._create_instance() xenapi_fake.create_vm(instance['name'], 'Running') @@ -1883,12 +1895,13 @@ class XenAPIMigrateInstance(stubs.XenAPITestBase): self.stubs.Set(vm_utils, '_get_partitions', fake_get_partitions) + self.mox.ReplayAll() self.assertRaises(exception.InstanceFaultRollback, conn.migrate_disk_and_power_off, self.context, instance, '127.0.0.1', flavor, None) - @stub_vm_utils_with_vdi_attached_here + @stub_vm_utils_with_vdi_attached def test_migrate_bad_fs_type_no_resize_down(self): instance = self._create_instance() xenapi_fake.create_vm(instance['name'], 'Running') @@ -1900,6 +1913,7 @@ class XenAPIMigrateInstance(stubs.XenAPITestBase): self.stubs.Set(vm_utils, '_get_partitions', fake_get_partitions) + self.mox.ReplayAll() self.assertRaises(exception.InstanceFaultRollback, conn.migrate_disk_and_power_off, self.context, instance, @@ -2334,6 +2348,8 @@ class XenAPIAutoDiskConfigTestCase(stubs.XenAPITestBase): {'id': uuids.image_id, 'disk_format': 'vhd', 'properties': {'vm_mode': 'xen'}}) + + self.mox.ReplayAll() self.conn._vmops._attach_disks(instance, image_meta, vm_ref, instance['name'], vdis, disk_image_type, "fake_nw_inf") @@ -2346,7 +2362,7 @@ class XenAPIAutoDiskConfigTestCase(stubs.XenAPITestBase): self.instance_values['auto_disk_config'] = False self.assertIsPartitionCalled(False) - @stub_vm_utils_with_vdi_attached_here + @stub_vm_utils_with_vdi_attached def test_instance_auto_disk_config_fails_safe_two_partitions(self): # Should not partition unless fail safes pass. self.instance_values['auto_disk_config'] = True @@ -2358,7 +2374,7 @@ class XenAPIAutoDiskConfigTestCase(stubs.XenAPITestBase): self.assertIsPartitionCalled(False) - @stub_vm_utils_with_vdi_attached_here + @stub_vm_utils_with_vdi_attached def test_instance_auto_disk_config_fails_safe_badly_numbered(self): # Should not partition unless fail safes pass. self.instance_values['auto_disk_config'] = True @@ -2370,7 +2386,7 @@ class XenAPIAutoDiskConfigTestCase(stubs.XenAPITestBase): self.assertIsPartitionCalled(False) - @stub_vm_utils_with_vdi_attached_here + @stub_vm_utils_with_vdi_attached def test_instance_auto_disk_config_fails_safe_bad_fstype(self): # Should not partition unless fail safes pass. self.instance_values['auto_disk_config'] = True @@ -2382,7 +2398,7 @@ class XenAPIAutoDiskConfigTestCase(stubs.XenAPITestBase): self.assertIsPartitionCalled(False) - @stub_vm_utils_with_vdi_attached_here + @stub_vm_utils_with_vdi_attached def test_instance_auto_disk_config_passes_fail_safes(self): """Should partition if instance is marked as auto_disk_config=True and virt-layer specific fail-safe checks pass. diff --git a/nova/virt/xenapi/client/session.py b/nova/virt/xenapi/client/session.py index 483c63a6f0ef..a886cc5662b4 100644 --- a/nova/virt/xenapi/client/session.py +++ b/nova/virt/xenapi/client/session.py @@ -70,7 +70,7 @@ class XenAPISession(object): # changed in development environments. # MAJOR VERSION: Incompatible changes with the plugins # MINOR VERSION: Compatible changes, new plguins, etc - PLUGIN_REQUIRED_VERSION = '1.6' + PLUGIN_REQUIRED_VERSION = '1.7' def __init__(self, url, user, pw): version_string = version.version_string_with_package() @@ -82,6 +82,7 @@ class XenAPISession(object): self.XenAPI = XenAPI self._sessions = queue.Queue() self.is_slave = False + self.host_checked = False exception = self.XenAPI.Failure(_("Unable to log in to XenAPI " "(is the Dom0 disk full?)")) url = self._create_first_session(url, user, pw, exception) diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py index a44c2df219f9..575acabe1f45 100644 --- a/nova/virt/xenapi/fake.py +++ b/nova/virt/xenapi/fake.py @@ -81,7 +81,8 @@ def reset(): create_vm('fake dom 0', 'Running', is_a_template=False, - is_control_domain=True) + is_control_domain=True, + domid='0') def reset_table(table): @@ -125,12 +126,12 @@ def create_vm(name_label, status, **kwargs): domid = "-1" resident_on = '' - vm_rec = kwargs.copy() - vm_rec.update({'name_label': name_label, - 'domid': domid, - 'power_state': status, - 'blocked_operations': {}, - 'resident_on': resident_on}) + vm_rec = {'name_label': name_label, + 'domid': domid, + 'power_state': status, + 'blocked_operations': {}, + 'resident_on': resident_on} + vm_rec.update(kwargs.copy()) vm_ref = _create_object('VM', vm_rec) after_VM_create(vm_ref, vm_rec) return vm_ref @@ -494,7 +495,7 @@ class SessionBase(object): if rec['currently_attached']: raise Failure(['DEVICE_ALREADY_ATTACHED', ref]) rec['currently_attached'] = True - rec['device'] = rec['userdevice'] + rec['device'] = 'fakedev' def VBD_unplug(self, _1, ref): rec = get_record('VBD', ref) @@ -760,12 +761,16 @@ class SessionBase(object): return base64.b64encode(zlib.compress("dom_id: %s" % dom_id)) def _plugin_nova_plugin_version_get_version(self, method, args): - return pickle.dumps("1.6") + return pickle.dumps("1.7") def _plugin_xenhost_query_gc(self, method, args): return pickle.dumps("False") + def _plugin_partition_utils_dot_py_make_partition(self, method, args): + return pickle.dumps(None) + def host_call_plugin(self, _1, _2, plugin, method, args): + plugin = plugin.replace('.', '_dot_') func = getattr(self, '_plugin_%s_%s' % (plugin, method), None) if not func: raise Exception('No simulation in host_call_plugin for %s,%s' % diff --git a/nova/virt/xenapi/image/vdi_through_dev.py b/nova/virt/xenapi/image/vdi_through_dev.py index 4f1cc514caaf..0a5aba99ac9a 100644 --- a/nova/virt/xenapi/image/vdi_through_dev.py +++ b/nova/virt/xenapi/image/vdi_through_dev.py @@ -76,8 +76,8 @@ class UploadToGlanceAsRawTgz(object): def upload_image(self): vdi_ref = self._get_vdi_ref() - with vm_utils.vdi_attached_here(self.session, vdi_ref, - read_only=True) as dev: + with vm_utils.vdi_attached(self.session, vdi_ref, + read_only=True) as dev: devpath = utils.make_dev_path(dev) with utils.temporary_chown(devpath): self._perform_upload(devpath) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 13061d038840..1ab67a5f86bc 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -893,12 +893,15 @@ def _auto_configure_disk(session, vdi_ref, new_gb): 2. The disk must have only one partition. 3. The file-system on the one partition must be ext3 or ext4. + + 4. We are not running in independent_compute mode (checked by + vdi_attached) """ if new_gb == 0: LOG.debug("Skipping auto_config_disk as destination size is 0GB") return - with vdi_attached_here(session, vdi_ref, read_only=False) as dev: + with vdi_attached(session, vdi_ref, read_only=False) as dev: partitions = _get_partitions(dev) if len(partitions) != 1: @@ -963,13 +966,20 @@ def _generate_disk(session, instance, vm_ref, userdevice, name_label, disk_type, size_mb, fs_type, fs_label=None): """Steps to programmatically generate a disk: - 1. Create VDI of desired size + 1. Create VDI of desired size - 2. Attach VDI to compute worker + 2. Attach VDI to Dom0 - 3. Create partition + 3. Create partition + 3.a. If the partition type is supported by dom0 (currently ext3, + swap) then create it while the VDI is attached to dom0. + 3.b. If the partition type is not supported by dom0, attach the + VDI to the domU and create there. + This split between DomU/Dom0 ensures that we can create most + VM types in the "isolated compute" case. + + 4. Create VBD between instance VM and VDI - 4. Create VBD between instance VM and VDI """ # 1. Create VDI sr_ref = safe_find_sr(session) @@ -979,16 +989,26 @@ def _generate_disk(session, instance, vm_ref, userdevice, name_label, virtual_size) try: - # 2. Attach VDI to compute worker (VBD hotplug) - with vdi_attached_here(session, vdi_ref, read_only=False) as dev: + # 2. Attach VDI to Dom0 (VBD hotplug) + mkfs_in_dom0 = fs_type in ('ext3', 'swap') + with vdi_attached(session, vdi_ref, read_only=False, + dom0=True) as dev: # 3. Create partition - partition_start = "2048s" - partition_end = "-0" + partition_start = "2048" + partition_end = "-" - partition_path = _make_partition(session, dev, - partition_start, partition_end) + session.call_plugin_serialized('partition_utils.py', + 'make_partition', dev, + partition_start, partition_end) - if fs_type is not None: + if mkfs_in_dom0: + session.call_plugin_serialized('partition_utils.py', 'mkfs', + dev, '1', fs_type, fs_label) + + # 3.a. dom0 does not support nfs/ext4, so may have to mkfs in domU + if fs_type is not None and not mkfs_in_dom0: + with vdi_attached(session, vdi_ref, read_only=False) as dev: + partition_path = utils.make_dev_path(dev, partition=1) utils.mkfs(fs_type, partition_path, fs_label, run_as_root=True) @@ -1084,7 +1104,7 @@ def generate_configdrive(session, instance, vm_ref, userdevice, 'configdrive', configdrive.CONFIGDRIVESIZE_BYTES) try: - with vdi_attached_here(session, vdi_ref, read_only=False) as dev: + with vdi_attached(session, vdi_ref, read_only=False) as dev: extra_md = {} if admin_password: extra_md['admin_pass'] = admin_password @@ -1496,7 +1516,7 @@ def _fetch_disk_image(context, session, instance, name_label, image_id, filename = None vdi_uuid = session.call_xenapi("VDI.get_uuid", vdi_ref) - with vdi_attached_here(session, vdi_ref, read_only=False) as dev: + with vdi_attached(session, vdi_ref, read_only=False) as dev: _stream_disk( session, image.stream_to, image_type, virtual_size, dev) @@ -1658,7 +1678,7 @@ def preconfigure_instance(session, instance, vdi_ref, network_info): if not mount_required: return - with vdi_attached_here(session, vdi_ref, read_only=False) as dev: + with vdi_attached(session, vdi_ref, read_only=False) as dev: _mounted_processing(dev, key, net, metadata) @@ -2080,16 +2100,24 @@ def _remap_vbd_dev(dev): return remapped_dev -def _wait_for_device(dev): +def _wait_for_device(session, dev, dom0, max_seconds): """Wait for device node to appear.""" - for i in range(0, CONF.xenserver.block_device_creation_timeout): - dev_path = utils.make_dev_path(dev) - if os.path.exists(dev_path): - return - time.sleep(1) + dev_path = utils.make_dev_path(dev) + found_path = None + if dom0: + found_path = session.call_plugin_serialized('partition_utils.py', + 'wait_for_dev', + dev_path, max_seconds) + else: + for i in range(0, max_seconds): + if os.path.exists(dev_path): + found_path = dev_path + break + time.sleep(1) - raise exception.StorageError( - reason=_('Timeout waiting for device %s to be created') % dev) + if found_path is None: + raise exception.StorageError( + reason=_('Timeout waiting for device %s to be created') % dev) def cleanup_attached_vdis(session): @@ -2116,8 +2144,13 @@ def cleanup_attached_vdis(session): @contextlib.contextmanager -def vdi_attached_here(session, vdi_ref, read_only=False): - this_vm_ref = _get_this_vm_ref(session) +def vdi_attached(session, vdi_ref, read_only=False, dom0=False): + if dom0: + this_vm_ref = _get_dom0_ref(session) + else: + # Make sure we are running as a domU. + ensure_correct_host(session) + this_vm_ref = _get_this_vm_ref(session) vbd_ref = create_vbd(session, this_vm_ref, vdi_ref, 'autodetect', read_only=read_only, bootable=False) @@ -2134,10 +2167,13 @@ def vdi_attached_here(session, vdi_ref, read_only=False): LOG.debug('VBD %(vbd_ref)s plugged into wrong dev, ' 'remapping to %(dev)s', {'vbd_ref': vbd_ref, 'dev': dev}) - _wait_for_device(dev) + _wait_for_device(session, dev, dom0, + CONF.xenserver.block_device_creation_timeout) yield dev finally: - utils.execute('sync', run_as_root=True) + # As we can not have filesystems mounted here (we cannot + # destroy the VBD with filesystems mounted), it is not + # useful to call sync. LOG.debug('Destroying VBD for VDI %s ... ', vdi_ref) unplug_vbd(session, vbd_ref, this_vm_ref) finally: @@ -2154,11 +2190,19 @@ def _get_sys_hypervisor_uuid(): return f.readline().strip() +def _get_dom0_ref(session): + vms = session.call_xenapi("VM.get_all_records_where", + 'field "domid"="0" and ' + 'field "resident_on"="%s"' % + session.host_ref) + return list(vms.keys())[0] + + def get_this_vm_uuid(session): if session and session.is_local_connection: # UUID is the control domain running on this host vms = session.call_xenapi("VM.get_all_records_where", - 'field "is_control_domain"="true" and ' + 'field "domid"="0" and ' 'field "resident_on"="%s"' % session.host_ref) return vms[list(vms.keys())[0]]['uuid'] @@ -2353,10 +2397,10 @@ def _copy_partition(session, src_ref, dst_ref, partition, virtual_size): # Part of disk taken up by MBR virtual_size -= MBR_SIZE_BYTES - with vdi_attached_here(session, src_ref, read_only=True) as src: + with vdi_attached(session, src_ref, read_only=True) as src: src_path = utils.make_dev_path(src, partition=partition) - with vdi_attached_here(session, dst_ref, read_only=False) as dst: + with vdi_attached(session, dst_ref, read_only=False) as dst: dst_path = utils.make_dev_path(dst, partition=partition) _write_partition(session, virtual_size, dst) @@ -2421,12 +2465,17 @@ def _mounted_processing(device, key, net, metadata): def ensure_correct_host(session): """Ensure we're connected to the host we're running on. This is the - required configuration for anything that uses vdi_attached_here. + required configuration for anything that uses vdi_attached without + the dom0 flag. """ + if session.host_checked: + return + this_vm_uuid = get_this_vm_uuid(session) try: session.call_xenapi('VM.get_by_uuid', this_vm_uuid) + session.host_checked = True except session.XenAPI.Failure as exc: if exc.details[0] != 'UUID_INVALID': raise diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version b/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version index f700c6f6dbb7..77d74f6843cd 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version @@ -32,7 +32,8 @@ import utils # 1.4 - Add support of Glance v2 api # 1.5 - Added function for network configuration on ovs bridge # 1.6 - Add function for network configuration on Linux bridge -PLUGIN_VERSION = "1.6" +# 1.7 - Add Partition utilities plugin +PLUGIN_VERSION = "1.7" def get_version(session): diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/partition_utils.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/partition_utils.py new file mode 100755 index 000000000000..14f56b582b44 --- /dev/null +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/partition_utils.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# Copyright (c) 2012 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. + +# NOTE: XenServer still only supports Python 2.4 in it's dom0 userspace +# which means the Nova xenapi plugins must use only Python 2.4 features + +import logging +import os +import time + +import pluginlib_nova as pluginlib +import utils + +pluginlib.configure_logging("disk_utils") +_ = pluginlib._ + + +def wait_for_dev(session, dev_path, max_seconds): + for i in range(0, max_seconds): + if os.path.exists(dev_path): + return dev_path + time.sleep(1) + + return "" + + +def make_partition(session, dev, partition_start, partition_end): + dev_path = utils.make_dev_path(dev) + + if partition_end != "-": + raise pluginlib.PluginError("Can only create unbounded partitions") + + utils.run_command(['sfdisk', '-uS', dev_path], + '%s,;\n' % (partition_start)) + + +def _mkfs(fs, path, label): + """Format a file or block device + + :param fs: Filesystem type (only 'swap', 'ext3' supported) + :param path: Path to file or block device to format + :param label: Volume label to use + """ + if fs == 'swap': + args = ['mkswap'] + elif fs == 'ext3': + args = ['mkfs', '-t', fs] + # add -F to force no interactive execute on non-block device. + args.extend(['-F']) + if label: + args.extend(['-L', label]) + else: + raise pluginlib.PluginError("Partition type %s not supported" % fs) + args.append(path) + utils.run_command(args) + + +def mkfs(session, dev, partnum, fs_type, fs_label): + dev_path = utils.make_dev_path(dev) + + out = utils.run_command(['kpartx', '-avspp', dev_path]) + try: + logging.info('kpartx output: %s' % out) + mapperdir = os.path.join('/dev', 'mapper') + dev_base = os.path.basename(dev) + partition_path = os.path.join(mapperdir, "%sp%s" % (dev_base, partnum)) + _mkfs(fs_type, partition_path, fs_label) + finally: + # Always remove partitions otherwise we can't unplug the VBD + utils.run_command(['kpartx', '-dvspp', dev_path]) + +if __name__ == "__main__": + utils.register_plugin_calls(wait_for_dev, + make_partition, + mkfs) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/utils.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/utils.py index 73ffa14cbec4..08c190c6a6fa 100644 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/utils.py +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/utils.py @@ -486,6 +486,21 @@ def extract_tarball(fileobj, path, callback=None): finish_subprocess(tar_proc, tar_cmd) +def make_dev_path(dev, partition=None, base='/dev'): + """Return a path to a particular device. + + >>> make_dev_path('xvdc') + /dev/xvdc + + >>> make_dev_path('xvdc', 1) + /dev/xvdc1 + """ + path = os.path.join(base, dev) + if partition: + path += str(partition) + return path + + def _handle_serialization(func): def wrapped(session, params): params = pickle.loads(params['params'])