diff --git a/cloudbaseinit/plugins/windows/extendvolumes.py b/cloudbaseinit/plugins/windows/extendvolumes.py index 11f61d0f..e8ccad0d 100644 --- a/cloudbaseinit/plugins/windows/extendvolumes.py +++ b/cloudbaseinit/plugins/windows/extendvolumes.py @@ -12,18 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. -import ctypes -import re - from oslo_config import cfg -from oslo_log import log as oslo_logging from cloudbaseinit.plugins.common import base -from cloudbaseinit.utils.windows import vds - -ole32 = ctypes.windll.ole32 -ole32.CoTaskMemFree.restype = None -ole32.CoTaskMemFree.argtypes = [ctypes.c_void_p] +from cloudbaseinit.utils.windows.storage import factory as storage_factory opts = [ cfg.ListOpt('volumes_to_extend', @@ -38,134 +30,16 @@ opts = [ CONF = cfg.CONF CONF.register_opts(opts) -LOG = oslo_logging.getLogger(__name__) - class ExtendVolumesPlugin(base.BasePlugin): - - def _extend_volumes(self, pack, volume_idxs=None): - enum = pack.QueryVolumes() - while True: - (unk, c) = enum.Next(1) - if not c: - break - volume = unk.QueryInterface(vds.IVdsVolume) - volume_prop = volume.GetProperties() - try: - extend_volume = True - if volume_idxs is not None: - volume_name = ctypes.wstring_at(volume_prop.pwszName) - volume_idx = self._get_volume_index(volume_name) - if volume_idx not in volume_idxs: - extend_volume = False - - if extend_volume: - self._extend_volume(pack, volume, volume_prop) - finally: - ole32.CoTaskMemFree(volume_prop.pwszName) - - def _get_volume_index(self, volume_name): - m = re.match(r"[^0-9]+([0-9]+)$", volume_name) - if m: - return int(m.group(1)) - - def _extend_volume(self, pack, volume, volume_prop): - volume_extents = self._get_volume_extents_to_resize(pack, - volume_prop.id) - input_disks = [] - - for (volume_extent, volume_extend_size) in volume_extents: - input_disk = vds.VDS_INPUT_DISK() - input_disks.append(input_disk) - - input_disk.diskId = volume_extent.diskId - input_disk.memberIdx = volume_extent.memberIdx - input_disk.plexId = volume_extent.plexId - input_disk.ullSize = volume_extend_size - - if input_disks: - extend_size = sum([i.ullSize for i in input_disks]) - volume_name = ctypes.wstring_at(volume_prop.pwszName) - LOG.info('Extending volume "%s" with %s bytes' % - (volume_name, extend_size)) - - input_disks_ar = (vds.VDS_INPUT_DISK * - len(input_disks))(*input_disks) - async = volume.Extend(input_disks_ar, len(input_disks)) - async.Wait() - - def _get_volume_extents_to_resize(self, pack, volume_id): - volume_extents = [] - - enum = pack.QueryDisks() - while True: - (unk, c) = enum.Next(1) - if not c: - break - disk = unk.QueryInterface(vds.IVdsDisk) - - (extents_p, num_extents) = disk.QueryExtents() - try: - extents_array_type = vds.VDS_DISK_EXTENT * num_extents - extents_array = extents_array_type.from_address( - ctypes.addressof(extents_p.contents)) - - volume_extent_extend_size = None - - for extent in extents_array: - if extent.volumeId == volume_id: - # Copy the extent in order to return it safely - # after the source is deallocated - extent_copy = vds.VDS_DISK_EXTENT() - ctypes.pointer(extent_copy)[0] = extent - - volume_extent_extend_size = [extent_copy, 0] - volume_extents.append(volume_extent_extend_size) - elif (volume_extent_extend_size and - extent.type == vds.VDS_DET_FREE): - volume_extent_extend_size[1] += extent.ullSize - else: - volume_extent_extend_size = None - finally: - ole32.CoTaskMemFree(extents_p) - - # Return only the extents that need to be resized - return [ve for ve in volume_extents if ve[1] > 0] - - def _query_providers(self, svc): - providers = [] - enum = svc.QueryProviders(vds.VDS_QUERY_SOFTWARE_PROVIDERS) - while True: - (unk, c) = enum.Next(1) - if not c: - break - providers.append(unk.QueryInterface(vds.IVdsSwProvider)) - return providers - - def _query_packs(self, provider): - packs = [] - enum = provider.QueryPacks() - while True: - (unk, c) = enum.Next(1) - if not c: - break - packs.append(unk.QueryInterface(vds.IVdsPack)) - return packs - def _get_volumes_to_extend(self): if CONF.volumes_to_extend is not None: return list(map(int, CONF.volumes_to_extend)) def execute(self, service, shared_data): - svc = vds.load_vds_service() - providers = self._query_providers(svc) - - volumes_to_extend = self._get_volumes_to_extend() - - for provider in providers: - packs = self._query_packs(provider) - for pack in packs: - self._extend_volumes(pack, volumes_to_extend) + volumes_indexes = self._get_volumes_to_extend() + storage_manager = storage_factory.get_storage_manager() + storage_manager.extend_volumes(volumes_indexes) return base.PLUGIN_EXECUTE_ON_NEXT_BOOT, False diff --git a/cloudbaseinit/tests/plugins/windows/test_extendvolumes.py b/cloudbaseinit/tests/plugins/windows/test_extendvolumes.py index 3a6fd98a..7d23e57a 100644 --- a/cloudbaseinit/tests/plugins/windows/test_extendvolumes.py +++ b/cloudbaseinit/tests/plugins/windows/test_extendvolumes.py @@ -12,8 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. + import importlib -import re import unittest try: @@ -21,10 +21,12 @@ try: except ImportError: import mock +from cloudbaseinit.plugins.common import base from cloudbaseinit.tests import testutils -class ExtendVolumesPluginTests(unittest.TestCase): +class TestExtendVolumesPlugin(unittest.TestCase): + def setUp(self): self._ctypes_mock = mock.MagicMock() self._comtypes_mock = mock.MagicMock() @@ -35,182 +37,33 @@ class ExtendVolumesPluginTests(unittest.TestCase): 'ctypes': self._ctypes_mock}) self._module_patcher.start() + self.addCleanup(self._module_patcher.stop) extendvolumes = importlib.import_module('cloudbaseinit.plugins.' 'windows.extendvolumes') self._extend_volumes = extendvolumes.ExtendVolumesPlugin() - def tearDown(self): - self._module_patcher.stop() - - @mock.patch('cloudbaseinit.plugins.windows.extendvolumes' - '.ExtendVolumesPlugin._get_volume_index') - @mock.patch('cloudbaseinit.plugins.windows.extendvolumes' - '.ExtendVolumesPlugin._extend_volume') - @mock.patch('cloudbaseinit.utils.windows.vds.IVdsVolume') - def test_extend_volumes(self, _vds_mock, mock_extend_volume, - mock_get_volume_index): - mock_pack = mock.MagicMock() - mock_volume_idxs = mock.MagicMock() - mock_enum = mock.MagicMock() - mock_unk = mock.MagicMock() - mock_c = mock.MagicMock() - mock_volume = mock.MagicMock() - mock_properties = mock.MagicMock() - mock_pack.QueryVolumes.return_value = mock_enum - mock_enum.Next.side_effect = [(mock_unk, mock_c), (None, None)] - mock_unk.QueryInterface.return_value = mock_volume - mock_volume.GetProperties.return_value = mock_properties - self._ctypes_mock.wstring_at.return_value = 'fake name' - mock_get_volume_index.return_value = mock_volume_idxs - self._extend_volumes._extend_volumes(mock_pack, [mock_volume_idxs]) - mock_pack.QueryVolumes.assert_called_once_with() - mock_enum.Next.assert_called_with(1) - mock_unk.QueryInterface.assert_called_once_with(_vds_mock) - mock_volume.GetProperties.assert_called_once_with() - self._ctypes_mock.wstring_at.assert_called_with( - mock_properties.pwszName) - mock_get_volume_index.assert_called_once_with('fake name') - mock_extend_volume.assert_called_once_with(mock_pack, mock_volume, - mock_properties) - self._ctypes_mock.windll.ole32.CoTaskMemFree.assert_called_once_with( - mock_properties.pwszName) - - def test_get_volume_index(self): - mock_value = mock.MagicMock() - re.match = mock.MagicMock(return_value=mock_value) - mock_value.group.return_value = '9999' - response = self._extend_volumes._get_volume_index('$2') - mock_value.group.assert_called_once_with(1) - self.assertTrue(response == 9999) - - @mock.patch('cloudbaseinit.plugins.windows.extendvolumes' - '.ExtendVolumesPlugin._get_volume_extents_to_resize') - @mock.patch('cloudbaseinit.utils.windows.vds.VDS_INPUT_DISK') - def test_extend_volume(self, mock_VDS_INPUT_DISK, - mock_get_volume_extents_to_resize): - mock_disk = mock.MagicMock() - mock_pack = mock.MagicMock() - mock_volume = mock.MagicMock() - mock_properties = mock.MagicMock() - mock_volume_extent = mock.MagicMock() - mock_async = mock.MagicMock() - mock_get_volume_extents_to_resize.return_value = [(mock_volume_extent, - 9999)] - mock_VDS_INPUT_DISK.return_value = mock_disk - mock_volume.Extend.return_value = mock_async - - self._extend_volumes._extend_volume(mock_pack, mock_volume, - mock_properties) - - mock_get_volume_extents_to_resize.assert_called_once_with( - mock_pack, mock_properties.id) - self._ctypes_mock.wstring_at.assert_called_with( - mock_properties.pwszName) - mock_volume.Extend.assert_called_once_with( - mock_VDS_INPUT_DISK.__mul__()(), 1) - mock_async.Wait.assert_called_once_with() - - @mock.patch('cloudbaseinit.utils.windows.vds.IVdsDisk') - @mock.patch('cloudbaseinit.utils.windows.vds.VDS_DISK_EXTENT') - def test_get_volume_extents_to_resize(self, mock_VDS_DISK_EXTENT, - mock_IVdsDisk): - mock_pack = mock.MagicMock() - mock_extents_p = mock.MagicMock() - mock_unk = mock.MagicMock() - mock_c = mock.MagicMock() - mock_disk = mock.MagicMock() - mock_enum = mock.MagicMock() - fake_volume_id = '$1' - mock_array = mock.MagicMock() - mock_array.volumeId = fake_volume_id - mock_pack.QueryDisks.return_value = mock_enum - mock_enum.Next.side_effect = [(mock_unk, mock_c), (None, None)] - mock_unk.QueryInterface.return_value = mock_disk - mock_disk.QueryExtents.return_value = (mock_extents_p, - 1) - mock_VDS_DISK_EXTENT.__mul__().from_address.return_value = [mock_array] - - response = self._extend_volumes._get_volume_extents_to_resize( - mock_pack, fake_volume_id) - - mock_pack.QueryDisks.assert_called_once_with() - mock_enum.Next.assert_called_with(1) - mock_unk.QueryInterface.assert_called_once_with(mock_IVdsDisk) - self._ctypes_mock.addressof.assert_called_with(mock_extents_p.contents) - mock_VDS_DISK_EXTENT.__mul__().from_address.assert_called_with( - self._ctypes_mock.addressof(mock_extents_p.contents)) - - self._ctypes_mock.pointer.assert_called_once_with( - mock_VDS_DISK_EXTENT()) - self.assertEqual([], response) - - self._ctypes_mock.windll.ole32.CoTaskMemFree.assert_called_with( - mock_extents_p) - - @mock.patch('cloudbaseinit.utils.windows.vds.' - 'VDS_QUERY_SOFTWARE_PROVIDERS') - @mock.patch('cloudbaseinit.utils.windows.vds.IVdsSwProvider') - def test_query_providers(self, mock_IVdsSwProvider, - mock_VDS_QUERY_SOFTWARE_PROVIDERS): - mock_svc = mock.MagicMock() - mock_enum = mock.MagicMock() - mock_unk = mock.MagicMock() - mock_c = mock.MagicMock() - mock_svc.QueryProviders.return_value = mock_enum - mock_enum.Next.side_effect = [(mock_unk, mock_c), (None, None)] - mock_unk.QueryInterface.return_value = 'fake providers' - - response = self._extend_volumes._query_providers(mock_svc) - mock_svc.QueryProviders.assert_called_once_with( - mock_VDS_QUERY_SOFTWARE_PROVIDERS) - mock_enum.Next.assert_called_with(1) - mock_unk.QueryInterface.assert_called_once_with(mock_IVdsSwProvider) - self.assertEqual(['fake providers'], response) - - @mock.patch('cloudbaseinit.utils.windows.vds.IVdsPack') - def test_query_packs(self, mock_IVdsPack): - mock_provider = mock.MagicMock() - mock_enum = mock.MagicMock() - mock_unk = mock.MagicMock() - mock_c = mock.MagicMock() - mock_provider.QueryPacks.return_value = mock_enum - mock_enum.Next.side_effect = [(mock_unk, mock_c), (None, None)] - mock_unk.QueryInterface.return_value = 'fake packs' - - response = self._extend_volumes._query_packs(mock_provider) - - mock_provider.QueryPacks.assert_called_once_with() - mock_enum.Next.assert_called_with(1) - mock_unk.QueryInterface.assert_called_once_with(mock_IVdsPack) - self.assertEqual(['fake packs'], response) - def test_get_volumes_to_extend(self): with testutils.ConfPatcher('volumes_to_extend', '1'): response = self._extend_volumes._get_volumes_to_extend() self.assertEqual([1], response) - @mock.patch('cloudbaseinit.utils.windows.vds.load_vds_service') - @mock.patch('cloudbaseinit.plugins.windows.extendvolumes.' - 'ExtendVolumesPlugin._query_providers') - @mock.patch('cloudbaseinit.plugins.windows.extendvolumes.' - 'ExtendVolumesPlugin._query_packs') - @mock.patch('cloudbaseinit.plugins.windows.extendvolumes.' - 'ExtendVolumesPlugin._extend_volumes') - def test_execute(self, mock_extend_volumes, mock_query_packs, - mock_query_providers, mock_load_vds_service): - mock_svc = mock.MagicMock() - fake_providers = ['fake providers'] - fake_packs = ['fake packs'] - mock_service = mock.MagicMock() - fake_data = 'fake data' - mock_load_vds_service.return_value = mock_svc - mock_query_providers.return_value = fake_providers - mock_query_packs.return_value = fake_packs + @mock.patch("cloudbaseinit.utils.windows.storage.factory" + ".get_storage_manager") + @mock.patch("cloudbaseinit.plugins.windows.extendvolumes" + ".ExtendVolumesPlugin._get_volumes_to_extend") + def test_execute(self, mock_get_volumes_to_extend, + mock_get_storage_manager): + volumes_indexes = [1, 3] + mock_get_volumes_to_extend.return_value = volumes_indexes + storage_manager = mock.Mock() + mock_get_storage_manager.return_value = storage_manager - with testutils.ConfPatcher('volumes_to_extend', '1'): - self._extend_volumes.execute(mock_service, fake_data) + response = self._extend_volumes.execute(mock.Mock(), mock.Mock()) - mock_query_providers.assert_called_once_with(mock_svc) - mock_query_packs.assert_called_once_with('fake providers') - mock_extend_volumes.assert_called_with('fake packs', [1]) + mock_get_volumes_to_extend.assert_called_once_with() + mock_get_storage_manager.assert_called_once_with() + storage_manager.extend_volumes.assert_called_once_with( + volumes_indexes) + self.assertEqual((base.PLUGIN_EXECUTE_ON_NEXT_BOOT, False), + response) diff --git a/cloudbaseinit/tests/utils/windows/storage/__init__.py b/cloudbaseinit/tests/utils/windows/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cloudbaseinit/tests/utils/windows/storage/test_factory.py b/cloudbaseinit/tests/utils/windows/storage/test_factory.py new file mode 100644 index 00000000..987d21db --- /dev/null +++ b/cloudbaseinit/tests/utils/windows/storage/test_factory.py @@ -0,0 +1,70 @@ +# Copyright 2015 Cloudbase Solutions Srl +# +# 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 ctypes as _ # noqa +import importlib +import unittest + +import mock + + +class TestStorageManager(unittest.TestCase): + + def setUp(self): + self.mock_os = mock.MagicMock() + patcher = mock.patch.dict( + "sys.modules", + { + "os": self.mock_os + } + ) + patcher.start() + + self.factory = importlib.import_module( + "cloudbaseinit.utils.windows.storage.factory") + self.addCleanup(patcher.stop) + + @mock.patch("cloudbaseinit.utils.classloader.ClassLoader") + @mock.patch("cloudbaseinit.osutils.factory.get_os_utils") + def _test_get_storage_manager(self, mock_get_os_utils, mock_class_loader, + nano=False, fail=False): + if fail: + self.mock_os.name = "linux" + with self.assertRaises(NotImplementedError): + self.factory.get_storage_manager() + return + + self.mock_os.name = "nt" + mock_get_os_utils.return_value.check_os_version.return_value = nano + mock_load_class = mock_class_loader.return_value.load_class + response = self.factory.get_storage_manager() + if nano: + class_path = ("cloudbaseinit.utils.windows.storage." + "wsm_storage_manager.WSMStorageManager") + else: + class_path = ("cloudbaseinit.utils.windows.storage." + "vds_storage_manager.VDSStorageManager") + mock_load_class.assert_called_once_with(class_path) + self.assertEqual(mock_load_class.return_value.return_value, + response) + + def test_get_storage_manager_fail(self): + self._test_get_storage_manager(fail=True) + + def test_get_storage_manager_nano(self): + self._test_get_storage_manager(nano=True) + + def test_get_storage_manager(self): + self._test_get_storage_manager() diff --git a/cloudbaseinit/tests/utils/windows/storage/test_vds_storage_manager.py b/cloudbaseinit/tests/utils/windows/storage/test_vds_storage_manager.py new file mode 100644 index 00000000..442e1412 --- /dev/null +++ b/cloudbaseinit/tests/utils/windows/storage/test_vds_storage_manager.py @@ -0,0 +1,217 @@ +# Copyright 2015 Cloudbase Solutions Srl +# +# 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 ctypes.util +import importlib +import re +import unittest + +try: + import unittest.mock as mock +except ImportError: + import mock + + +class TestVDSStorageManager(unittest.TestCase): + + def setUp(self): + self._ctypes_mock = mock.MagicMock() + self._comtypes_mock = mock.MagicMock() + self._ctypes_mock.util = ctypes.util + + self._module_patcher = mock.patch.dict( + 'sys.modules', + {'comtypes': self._comtypes_mock, + 'ctypes': self._ctypes_mock}) + + self._module_patcher.start() + + vds_store = importlib.import_module( + "cloudbaseinit.utils.windows.storage.vds_storage_manager") + self._vds_storage_manager = vds_store.VDSStorageManager() + + self.addCleanup(self._module_patcher.stop) + + @mock.patch("cloudbaseinit.utils.windows.storage.vds_storage_manager" + ".VDSStorageManager._get_volume_index") + @mock.patch("cloudbaseinit.utils.windows.storage.vds_storage_manager" + ".VDSStorageManager._extend_volume") + @mock.patch('cloudbaseinit.utils.windows.vds.IVdsVolume') + def test__extend_volumes(self, _vds_mock, mock_extend_volume, + mock_get_volume_index): + mock_pack = mock.MagicMock() + mock_volume_idxs = mock.MagicMock() + mock_enum = mock.MagicMock() + mock_unk = mock.MagicMock() + mock_c = mock.MagicMock() + mock_volume = mock.MagicMock() + mock_properties = mock.MagicMock() + mock_pack.QueryVolumes.return_value = mock_enum + mock_enum.Next.side_effect = [(mock_unk, mock_c), (None, None)] + mock_unk.QueryInterface.return_value = mock_volume + mock_volume.GetProperties.return_value = mock_properties + self._ctypes_mock.wstring_at.return_value = 'fake name' + mock_get_volume_index.return_value = mock_volume_idxs + self._vds_storage_manager._extend_volumes(mock_pack, + [mock_volume_idxs]) + mock_pack.QueryVolumes.assert_called_once_with() + mock_enum.Next.assert_called_with(1) + mock_unk.QueryInterface.assert_called_once_with(_vds_mock) + mock_volume.GetProperties.assert_called_once_with() + self._ctypes_mock.wstring_at.assert_called_with( + mock_properties.pwszName) + mock_get_volume_index.assert_called_once_with('fake name') + mock_extend_volume.assert_called_once_with(mock_pack, mock_volume, + mock_properties) + self._ctypes_mock.windll.ole32.CoTaskMemFree.assert_called_once_with( + mock_properties.pwszName) + + def test_get_volume_index(self): + mock_value = mock.MagicMock() + re.match = mock.MagicMock(return_value=mock_value) + mock_value.group.return_value = '9999' + response = self._vds_storage_manager._get_volume_index('$2') + mock_value.group.assert_called_once_with(1) + self.assertTrue(response == 9999) + + @mock.patch("cloudbaseinit.utils.windows.storage.vds_storage_manager" + ".VDSStorageManager._get_volume_extents_to_resize") + @mock.patch('cloudbaseinit.utils.windows.vds.VDS_INPUT_DISK') + def test_extend_volume(self, mock_VDS_INPUT_DISK, + mock_get_volume_extents_to_resize): + mock_disk = mock.MagicMock() + mock_pack = mock.MagicMock() + mock_volume = mock.MagicMock() + mock_properties = mock.MagicMock() + mock_volume_extent = mock.MagicMock() + mock_async = mock.MagicMock() + mock_get_volume_extents_to_resize.return_value = [(mock_volume_extent, + 9999)] + mock_VDS_INPUT_DISK.return_value = mock_disk + mock_volume.Extend.return_value = mock_async + + self._vds_storage_manager._extend_volume(mock_pack, mock_volume, + mock_properties) + + mock_get_volume_extents_to_resize.assert_called_once_with( + mock_pack, mock_properties.id) + self._ctypes_mock.wstring_at.assert_called_with( + mock_properties.pwszName) + mock_volume.Extend.assert_called_once_with( + mock_VDS_INPUT_DISK.__mul__()(), 1) + mock_async.Wait.assert_called_once_with() + + @mock.patch('cloudbaseinit.utils.windows.vds.IVdsDisk') + @mock.patch('cloudbaseinit.utils.windows.vds.VDS_DISK_EXTENT') + def test_get_volume_extents_to_resize(self, mock_VDS_DISK_EXTENT, + mock_IVdsDisk): + mock_pack = mock.MagicMock() + mock_extents_p = mock.MagicMock() + mock_unk = mock.MagicMock() + mock_c = mock.MagicMock() + mock_disk = mock.MagicMock() + mock_enum = mock.MagicMock() + fake_volume_id = '$1' + mock_array = mock.MagicMock() + mock_array.volumeId = fake_volume_id + mock_pack.QueryDisks.return_value = mock_enum + mock_enum.Next.side_effect = [(mock_unk, mock_c), (None, None)] + mock_unk.QueryInterface.return_value = mock_disk + mock_disk.QueryExtents.return_value = (mock_extents_p, + 1) + mock_VDS_DISK_EXTENT.__mul__().from_address.return_value = [mock_array] + + response = self._vds_storage_manager._get_volume_extents_to_resize( + mock_pack, fake_volume_id) + + mock_pack.QueryDisks.assert_called_once_with() + mock_enum.Next.assert_called_with(1) + mock_unk.QueryInterface.assert_called_once_with(mock_IVdsDisk) + self._ctypes_mock.addressof.assert_called_with(mock_extents_p.contents) + mock_VDS_DISK_EXTENT.__mul__().from_address.assert_called_with( + self._ctypes_mock.addressof(mock_extents_p.contents)) + + self._ctypes_mock.pointer.assert_called_once_with( + mock_VDS_DISK_EXTENT()) + self.assertEqual([], response) + + self._ctypes_mock.windll.ole32.CoTaskMemFree.assert_called_with( + mock_extents_p) + + @mock.patch('cloudbaseinit.utils.windows.vds.' + 'VDS_QUERY_SOFTWARE_PROVIDERS') + @mock.patch('cloudbaseinit.utils.windows.vds.IVdsSwProvider') + def test_query_providers(self, mock_IVdsSwProvider, + mock_VDS_QUERY_SOFTWARE_PROVIDERS): + mock_svc = mock.MagicMock() + mock_enum = mock.MagicMock() + mock_unk = mock.MagicMock() + mock_c = mock.MagicMock() + mock_svc.QueryProviders.return_value = mock_enum + mock_enum.Next.side_effect = [(mock_unk, mock_c), (None, None)] + mock_unk.QueryInterface.return_value = 'fake providers' + + response = self._vds_storage_manager._query_providers(mock_svc) + mock_svc.QueryProviders.assert_called_once_with( + mock_VDS_QUERY_SOFTWARE_PROVIDERS) + mock_enum.Next.assert_called_with(1) + mock_unk.QueryInterface.assert_called_once_with(mock_IVdsSwProvider) + self.assertEqual(['fake providers'], response) + + @mock.patch('cloudbaseinit.utils.windows.vds.IVdsPack') + def test_query_packs(self, mock_IVdsPack): + mock_provider = mock.MagicMock() + mock_enum = mock.MagicMock() + mock_unk = mock.MagicMock() + mock_c = mock.MagicMock() + mock_provider.QueryPacks.return_value = mock_enum + mock_enum.Next.side_effect = [(mock_unk, mock_c), (None, None)] + mock_unk.QueryInterface.return_value = 'fake packs' + + response = self._vds_storage_manager._query_packs(mock_provider) + + mock_provider.QueryPacks.assert_called_once_with() + mock_enum.Next.assert_called_with(1) + mock_unk.QueryInterface.assert_called_once_with(mock_IVdsPack) + self.assertEqual(['fake packs'], response) + + @mock.patch("cloudbaseinit.utils.windows.storage.vds_storage_manager" + ".VDSStorageManager._extend_volumes") + @mock.patch("cloudbaseinit.utils.windows.storage.vds_storage_manager" + ".VDSStorageManager._query_packs") + @mock.patch("cloudbaseinit.utils.windows.storage.vds_storage_manager" + ".VDSStorageManager._query_providers") + @mock.patch("cloudbaseinit.utils.windows.vds.load_vds_service") + def test_extend_volumes(self, mock_load_vds_service, mock_query_providers, + mock_query_packs, mock_extend_volumes): + mock_svc = mock.Mock() + providers = [mock.Mock()] * 5 + packs = [mock.Mock()] * 3 + volume_indexes = mock.Mock() + + mock_load_vds_service.return_value = mock_svc + mock_query_providers.return_value = providers + mock_query_packs.return_value = packs + + self._vds_storage_manager.extend_volumes( + volume_indexes=volume_indexes) + mock_load_vds_service.assert_called_once_with() + mock_query_providers.assert_called_once_with( + mock_svc) + mock_query_packs.assert_has_calls( + [mock.call(provider) for provider in providers]) + mock_extend_volumes.assert_has_calls( + [mock.call(pack, volume_indexes) for pack in packs] * + len(providers)) diff --git a/cloudbaseinit/tests/utils/windows/storage/test_wsm_storage_manager.py b/cloudbaseinit/tests/utils/windows/storage/test_wsm_storage_manager.py new file mode 100644 index 00000000..2a75cf8c --- /dev/null +++ b/cloudbaseinit/tests/utils/windows/storage/test_wsm_storage_manager.py @@ -0,0 +1,106 @@ +# Copyright 2015 Cloudbase Solutions Srl +# +# 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 importlib +import unittest + +try: + import unittest.mock as mock +except ImportError: + import mock + +from cloudbaseinit import exception + + +class TestWSMStorageManager(unittest.TestCase): + + def setUp(self): + self.mock_wmi = mock.MagicMock() + + patcher = mock.patch.dict( + "sys.modules", + { + "wmi": self.mock_wmi + } + ) + patcher.start() + self.addCleanup(patcher.stop) + wsm_store = importlib.import_module( + "cloudbaseinit.utils.windows.storage.wsm_storage_manager") + self.wsm = wsm_store.WSMStorageManager() + + def test_init(self): + self.mock_wmi.WMI.assert_called_once_with( + moniker='//./Root/Microsoft/Windows/Storage') + + def _test_extend_volumes(self, extend=True, fail=False, + size_ret=0, resize_ret=0): + volume_indexes = [1, 3] + volumes = [mock.Mock(), mock.Mock(), mock.Mock()] + partitions = [mock.Mock()] + for volume in volumes: + volume.associators.return_value = partitions + for partition in partitions: + size_max = partition.Size = 100 + if extend: + size_max = partition.Size + 10 + partition.GetSupportedSize.return_value = [ + size_ret, + mock.Mock(), + size_max, + mock.Mock()] + partition.Resize.return_value = [ + resize_ret, + mock.Mock()] + + conn = self.mock_wmi.WMI.return_value + conn.MSFT_Volume.return_value = volumes + + if fail: + if size_ret or extend: + with self.assertRaises(exception.CloudbaseInitException): + self.wsm.extend_volumes(volume_indexes=volume_indexes) + return + self.wsm.extend_volumes(volume_indexes=volume_indexes) + + conn.MSFT_Volume.assert_called_once_with() + for idx in volume_indexes: + volumes[idx - 1].associators.assert_called_once_with( + wmi_result_class='MSFT_Partition') + volumes[1].associators.assert_not_called() + for partition in partitions: + calls = [mock.call()] * len(volume_indexes) + partition.GetSupportedSize.assert_has_calls(calls) + + if not extend: + for partition in partitions: + partition.Resize.assert_not_called() + return + for partition in partitions: + size_max = partition.GetSupportedSize.return_value[2] + calls = [mock.call(size_max)] * len(volume_indexes) + partition.Resize.assert_has_calls(calls) + + def test_extend_volumes_fail_size(self): + self._test_extend_volumes(fail=True, size_ret=1) + + def test_extend_volumes_fail_resize(self): + self._test_extend_volumes(fail=True, resize_ret=1) + + def test_extend_volumes_no_extend(self): + self._test_extend_volumes(extend=False) + + def test_extend_volumes(self): + self._test_extend_volumes() diff --git a/cloudbaseinit/utils/windows/storage/__init__.py b/cloudbaseinit/utils/windows/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cloudbaseinit/utils/windows/storage/base.py b/cloudbaseinit/utils/windows/storage/base.py new file mode 100644 index 00000000..44bd6121 --- /dev/null +++ b/cloudbaseinit/utils/windows/storage/base.py @@ -0,0 +1,25 @@ +# Copyright 2015 Cloudbase Solutions Srl +# +# 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 abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class BaseStorageManager(object): + + @abc.abstractmethod + def extend_volumes(self, volume_indexes=None): + pass diff --git a/cloudbaseinit/utils/windows/storage/factory.py b/cloudbaseinit/utils/windows/storage/factory.py new file mode 100644 index 00000000..a2d176ed --- /dev/null +++ b/cloudbaseinit/utils/windows/storage/factory.py @@ -0,0 +1,40 @@ +# Copyright 2015 Cloudbase Solutions Srl +# +# 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 os + +from cloudbaseinit.osutils import factory as osutils_factory +from cloudbaseinit.utils import classloader + + +def get_storage_manager(): + class_paths = { + "VDS": "cloudbaseinit.utils.windows.storage.vds_storage_manager." + "VDSStorageManager", + "WSM": "cloudbaseinit.utils.windows.storage.wsm_storage_manager." + "WSMStorageManager", + } + + osutils = osutils_factory.get_os_utils() + cl = classloader.ClassLoader() + + if os.name == "nt": + if osutils.check_os_version(10, 0): + # VDS is not available on Nano Server + # WSM supersedes VDS since Windows Server 2012 / Windows 8 + return cl.load_class(class_paths["WSM"])() + else: + return cl.load_class(class_paths["VDS"])() + + raise NotImplementedError("No storage manager available for this platform") diff --git a/cloudbaseinit/utils/windows/storage/vds_storage_manager.py b/cloudbaseinit/utils/windows/storage/vds_storage_manager.py new file mode 100644 index 00000000..29865fd4 --- /dev/null +++ b/cloudbaseinit/utils/windows/storage/vds_storage_manager.py @@ -0,0 +1,147 @@ +# Copyright 2013 Cloudbase Solutions Srl +# +# 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 ctypes +import re + +from oslo_log import log as oslo_logging + +from cloudbaseinit.utils.windows.storage import base +from cloudbaseinit.utils.windows import vds + +LOG = oslo_logging.getLogger(__name__) + +ole32 = ctypes.windll.ole32 +ole32.CoTaskMemFree.restype = None +ole32.CoTaskMemFree.argtypes = [ctypes.c_void_p] + + +class VDSStorageManager(base.BaseStorageManager): + def _extend_volumes(self, pack, volume_indexes): + enum = pack.QueryVolumes() + while True: + (unk, c) = enum.Next(1) + if not c: + break + volume = unk.QueryInterface(vds.IVdsVolume) + volume_prop = volume.GetProperties() + try: + extend_volume = True + if volume_indexes: + volume_name = ctypes.wstring_at(volume_prop.pwszName) + volume_idx = self._get_volume_index(volume_name) + if volume_idx not in volume_indexes: + extend_volume = False + + if extend_volume: + self._extend_volume(pack, volume, volume_prop) + finally: + ole32.CoTaskMemFree(volume_prop.pwszName) + + def _get_volume_index(self, volume_name): + m = re.match(r"[^0-9]+([0-9]+)$", volume_name) + if m: + return int(m.group(1)) + + def _extend_volume(self, pack, volume, volume_prop): + volume_extents = self._get_volume_extents_to_resize(pack, + volume_prop.id) + input_disks = [] + + for (volume_extent, volume_extend_size) in volume_extents: + input_disk = vds.VDS_INPUT_DISK() + input_disks.append(input_disk) + + input_disk.diskId = volume_extent.diskId + input_disk.memberIdx = volume_extent.memberIdx + input_disk.plexId = volume_extent.plexId + input_disk.ullSize = volume_extend_size + + if input_disks: + extend_size = sum([i.ullSize for i in input_disks]) + volume_name = ctypes.wstring_at(volume_prop.pwszName) + LOG.info('Extending volume "%s" with %s bytes' % + (volume_name, extend_size)) + + input_disks_ar = (vds.VDS_INPUT_DISK * + len(input_disks))(*input_disks) + async = volume.Extend(input_disks_ar, len(input_disks)) + async.Wait() + + def _get_volume_extents_to_resize(self, pack, volume_id): + volume_extents = [] + + enum = pack.QueryDisks() + while True: + (unk, c) = enum.Next(1) + if not c: + break + disk = unk.QueryInterface(vds.IVdsDisk) + + (extents_p, num_extents) = disk.QueryExtents() + try: + extents_array_type = vds.VDS_DISK_EXTENT * num_extents + extents_array = extents_array_type.from_address( + ctypes.addressof(extents_p.contents)) + + volume_extent_extend_size = None + + for extent in extents_array: + if extent.volumeId == volume_id: + # Copy the extent in order to return it safely + # after the source is deallocated + extent_copy = vds.VDS_DISK_EXTENT() + ctypes.pointer(extent_copy)[0] = extent + + volume_extent_extend_size = [extent_copy, 0] + volume_extents.append(volume_extent_extend_size) + elif (volume_extent_extend_size and + extent.type == vds.VDS_DET_FREE): + volume_extent_extend_size[1] += extent.ullSize + else: + volume_extent_extend_size = None + finally: + ole32.CoTaskMemFree(extents_p) + + # Return only the extents that need to be resized + return [ve for ve in volume_extents if ve[1] > 0] + + def _query_providers(self, svc): + providers = [] + enum = svc.QueryProviders(vds.VDS_QUERY_SOFTWARE_PROVIDERS) + while True: + (unk, c) = enum.Next(1) + if not c: + break + providers.append(unk.QueryInterface(vds.IVdsSwProvider)) + return providers + + def _query_packs(self, provider): + packs = [] + enum = provider.QueryPacks() + while True: + (unk, c) = enum.Next(1) + if not c: + break + packs.append(unk.QueryInterface(vds.IVdsPack)) + return packs + + def extend_volumes(self, volume_indexes=None): + svc = vds.load_vds_service() + providers = self._query_providers(svc) + + for provider in providers: + packs = self._query_packs(provider) + for pack in packs: + self._extend_volumes(pack, volume_indexes) diff --git a/cloudbaseinit/utils/windows/storage/wsm_storage_manager.py b/cloudbaseinit/utils/windows/storage/wsm_storage_manager.py new file mode 100644 index 00000000..01ea5125 --- /dev/null +++ b/cloudbaseinit/utils/windows/storage/wsm_storage_manager.py @@ -0,0 +1,52 @@ +# Copyright 2013 Cloudbase Solutions Srl +# +# 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 wmi + +from oslo_log import log as oslo_logging + +from cloudbaseinit import exception +from cloudbaseinit.utils.windows.storage import base + +LOG = oslo_logging.getLogger(__name__) + + +class WSMStorageManager(base.BaseStorageManager): + def __init__(self): + self._conn = wmi.WMI(moniker='//./Root/Microsoft/Windows/Storage') + + def extend_volumes(self, volume_indexes=None): + volumes = self._conn.MSFT_Volume() + + for idx, volume in enumerate(volumes, 1): + # TODO(alexpilotti): don't rely on the volumes WMI query order + if volume_indexes and idx not in volume_indexes: + continue + + partitions = volume.associators(wmi_result_class='MSFT_Partition') + for partition in partitions: + (ret_val, _, size_max, _) = partition.GetSupportedSize() + if ret_val: + raise exception.CloudbaseInitException( + "GetSupportedSize failed with error: %s" % ret_val) + + if size_max > partition.Size: + LOG.info('Extending partition "%(partition_number)s" ' + 'to %(size)s bytes' % + {'partition_number': partition.PartitionNumber, + 'size': size_max}) + (ret_val, _) = partition.Resize(size_max) + if ret_val: + raise exception.CloudbaseInitException( + "Resize failed with error: %s" % ret_val)