From def785a23d16e3965743bbb2a35ec5a3249a2fd0 Mon Sep 17 00:00:00 2001 From: Alexander Burluka Date: Wed, 21 Jan 2015 16:45:00 -0500 Subject: [PATCH] Add config drive support for Virtuozzo containers With this change, we're defining new setup methodology for container based virt (parallels now, and in future LXC too). In such environments the *host* container technology will be responsible for mounting the config drive image inside the guest filesystem. So when the guest image boots, it will see the config drive data immediately at the location defined by cloud-init. The only demand for existing images is having cloud-init turned on. To support config drive in Virtuozzo (former PCS) containers, we use ploop block device. Actions with ploop device are performed via ploop CLI tool and newly created device is mounted into /var/lib/cloud/seed/config_drive on container's filesystem. This destination recognised by cloud-init as a source of data. (Ploop device description: https://openvz.org/Ploop) Co-Authored-By: Maxim Nestratov Change-Id: Ib21ab954a08027fb1f2aec13c2d95b5f080eccce Implements: blueprint config-drive-support-for-virtuozzo --- nova/exception.py | 9 ++- nova/tests/unit/test_configdrive2.py | 38 ++++++++- .../tests/unit/virt/libvirt/test_blockinfo.py | 12 +++ nova/tests/unit/virt/libvirt/test_driver.py | 1 + nova/virt/configdrive.py | 77 +++++++++++++++++-- nova/virt/hyperv/vmops.py | 2 +- nova/virt/libvirt/blockinfo.py | 27 +++++-- nova/virt/libvirt/driver.py | 14 +++- nova/virt/vmwareapi/vmops.py | 2 +- 9 files changed, 162 insertions(+), 20 deletions(-) diff --git a/nova/exception.py b/nova/exception.py index 1e017a6015e1..2f605c37eb88 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1414,8 +1414,13 @@ class ConfigDriveMountFailed(NovaException): class ConfigDriveUnknownFormat(NovaException): - msg_fmt = _("Unknown config drive format %(format)s. Select one of " - "iso9660 or vfat.") + msg_fmt = _("Unknown config drive format %(format)s for %(os_type)s " + "os type. Valid options are: iso9660, vfat or None.") + + +class ConfigDriveUnsupportedFormat(NovaException): + msg_fmt = _("Unsupported config drive format %(format)s " + "for %(image_type)s image type. Image path: %(image_path)") class InterfaceAttachFailed(Invalid): diff --git a/nova/tests/unit/test_configdrive2.py b/nova/tests/unit/test_configdrive2.py index 4f74cbe2e425..5980ff83d343 100644 --- a/nova/tests/unit/test_configdrive2.py +++ b/nova/tests/unit/test_configdrive2.py @@ -14,10 +14,13 @@ # under the License. -import os -import tempfile - +import fixtures import mock +import os +import shutil +import tempfile +import uuid + from mox3 import mox from oslo_config import cfg @@ -93,6 +96,35 @@ class ConfigDriveTestCase(test.NoDBTestCase): if imagefile: fileutils.delete_if_exists(imagefile) + def _create_ext4_ploop(self, calls): + imagefile = None + with mock.patch.object(utils, 'execute', return_value=('', '')) as ex: + with configdrive.ConfigDriveBuilder(FakeInstanceMD()) as c: + imagefile = "/tmp/cd_ext4_" + str(uuid.uuid4())[:8] + c.make_drive(imagefile, image_type='ploop') + ex.assert_has_calls(calls) + return imagefile + + def test_create_configdrive_ext4_ploop(self): + self.flags(config_drive_format='ext4') + self.flags(virt_type='parallels', group='libvirt') + + def fake_trycmd(*args, **kwargs): + return None, None + self.useFixture(fixtures.MonkeyPatch('nova.utils.trycmd', fake_trycmd)) + calls = [ + mock.call('ploop', 'init', '-s', mock.ANY, + '-t', 'ext4', mock.ANY, + run_as_root=True, attempts=1), + mock.call('chown', '-R', mock.ANY, + mock.ANY, run_as_root=True), + mock.call('ploop', 'umount', + mock.ANY, run_as_root=True) + ] + imagefile = self._create_ext4_ploop(calls) + if imagefile: + shutil.rmtree(imagefile) + def test_config_drive_required_by_image_property(self): inst = fake_instance.fake_instance_obj(context.get_admin_context()) inst.config_drive = '' diff --git a/nova/tests/unit/virt/libvirt/test_blockinfo.py b/nova/tests/unit/virt/libvirt/test_blockinfo.py index a7a082fe8f31..c64cefdb275c 100644 --- a/nova/tests/unit/virt/libvirt/test_blockinfo.py +++ b/nova/tests/unit/virt/libvirt/test_blockinfo.py @@ -19,6 +19,7 @@ import mock from nova import block_device from nova.compute import arch +from nova.compute import vm_mode from nova import context from nova import exception from nova import objects @@ -679,6 +680,17 @@ class LibvirtBlockInfoTest(test.NoDBTestCase): config_drive_type = blockinfo.get_config_drive_type() self.assertEqual('disk', config_drive_type) + def test_get_config_drive_type_ploop(self): + self.flags(config_drive_format=None) + self.flags(virt_type='parallels', group='libvirt') + config_drive_type = blockinfo.get_config_drive_type(vm_mode.EXE) + self.assertEqual('fs', config_drive_type) + + def test_get_config_drive_type_improper_value(self): + self.flags(config_drive_format='test') + self.assertRaises(exception.ConfigDriveUnknownFormat, + blockinfo.get_config_drive_type) + def test_get_info_from_bdm(self): bdms = [{'device_name': '/dev/vds', 'device_type': 'disk', 'disk_bus': 'usb', 'swap_size': 4}, diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index ad4b449a3648..01f46967b38d 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -11336,6 +11336,7 @@ class LibvirtDriverTestCase(test.NoDBTestCase): inst['key_data'] = 'ABCDEFG' inst['system_metadata'] = {} inst['metadata'] = {} + inst['vm_mode'] = 'hvm' inst.update(params) diff --git a/nova/virt/configdrive.py b/nova/virt/configdrive.py index 23475c3a42ce..cf8f8c92b792 100644 --- a/nova/virt/configdrive.py +++ b/nova/virt/configdrive.py @@ -55,6 +55,12 @@ CONF.register_opts(configdrive_opts) # Config drives are 64mb, if we can't size to the exact size of the data CONFIGDRIVESIZE_BYTES = 64 * units.Mi +FS_FORMAT_VFAT = 'vfat' +FS_FORMAT_ISO9660 = 'iso9660' + +IMAGE_TYPE_RAW = 'raw' +IMAGE_TYPE_PLOOP = 'ploop' + class ConfigDriveBuilder(object): """Build config drives, optionally as a context manager.""" @@ -150,23 +156,80 @@ class ConfigDriveBuilder(object): if mounted: utils.execute('umount', mountdir, run_as_root=True) - def make_drive(self, path): + def _make_ext4_ploop(self, path, tmpdir): + """ploop is a disk loopback block device, that is used in + Parallels(OpenVZ) containers. It is similiar to Linux loop + device but prevents double caching of data in memory and + supports snapshots and some other effeciency benefits. Adding + ploop is a natural way to add disk device to VZ containers. + Ploop device has its own image format. It contains specific + partition table with one ext4 partition. + """ + os.mkdir(path) + utils.execute('ploop', + 'init', + '-s', CONFIGDRIVESIZE_BYTES, + '-t', 'ext4', + path + '/disk.config.hds', + attempts=1, + run_as_root=True) + with utils.tempdir() as mountdir: + mounted = False + try: + _, err = utils.trycmd( + 'ploop', 'mount', + '-m', mountdir, + '-t', 'ext4', + path + '/DiskDescriptor.xml', + run_as_root=True) + if os.path.exists(mountdir): + utils.execute('chown', '-R', + '%(u)d:%(g)d' % {'u': os.getuid(), + 'g': os.getgid()}, + mountdir, + run_as_root=True) + mounted = True + for ent in os.listdir(tmpdir): + shutil.copytree(os.path.join(tmpdir, ent), + os.path.join(mountdir, ent)) + finally: + if mounted: + utils.execute('ploop', 'umount', + path + '/disk.config.hds', run_as_root=True) + + def make_drive(self, path, image_type=IMAGE_TYPE_RAW): """Make the config drive. :param path: the path to place the config drive image at + :param image_type: host side image format :raises ProcessExecuteError if a helper process has failed. """ + fs_format = CONF.config_drive_format + if fs_format is None: + if image_type == IMAGE_TYPE_RAW: + fs_format = FS_FORMAT_ISO9660 + with utils.tempdir() as tmpdir: self._write_md_files(tmpdir) - if CONF.config_drive_format == 'iso9660': - self._make_iso9660(path, tmpdir) - elif CONF.config_drive_format == 'vfat': - self._make_vfat(path, tmpdir) + if image_type == IMAGE_TYPE_RAW: + if fs_format not in (FS_FORMAT_VFAT, FS_FORMAT_ISO9660): + raise exception.ConfigDriveUnsupportedFormat( + format=fs_format, + image_type=image_type, + image_path=path) + elif fs_format == FS_FORMAT_ISO9660: + self._make_iso9660(path, tmpdir) + elif fs_format == FS_FORMAT_VFAT: + self._make_vfat(path, tmpdir) + elif image_type == IMAGE_TYPE_PLOOP: + self._make_ext4_ploop(path, tmpdir) else: - raise exception.ConfigDriveUnknownFormat( - format=CONF.config_drive_format) + raise exception.ConfigDriveUnsupportedFormat( + format=fs_format, + image_type=image_type, + image_path=path) def cleanup(self): if self.imagefile: diff --git a/nova/virt/hyperv/vmops.py b/nova/virt/hyperv/vmops.py index b78131bc9963..bc5824107e7b 100644 --- a/nova/virt/hyperv/vmops.py +++ b/nova/virt/hyperv/vmops.py @@ -372,7 +372,7 @@ class VMOps(object): def _create_config_drive(self, instance, injected_files, admin_password, network_info): - if CONF.config_drive_format != 'iso9660': + if CONF.config_drive_format not in ('iso9660', None): raise vmutils.UnsupportedConfigDriveFormatException( _('Invalid config_drive_format "%s"') % CONF.config_drive_format) diff --git a/nova/virt/libvirt/blockinfo.py b/nova/virt/libvirt/blockinfo.py index 359e70f99815..697b533dc810 100644 --- a/nova/virt/libvirt/blockinfo.py +++ b/nova/virt/libvirt/blockinfo.py @@ -265,7 +265,7 @@ def get_disk_bus_for_device_type(virt_type, elif virt_type == "parallels": if device_type == "cdrom": return "ide" - elif device_type == "disk": + elif device_type in ("disk", "fs"): return "sata" else: # If virt-type not in list then it is unsupported @@ -341,11 +341,15 @@ def get_eph_disk(index): return 'disk.eph' + str(index) -def get_config_drive_type(): +def get_config_drive_type(os_type=None): """Determine the type of config drive. - If config_drive_format is set to iso9660 then the config drive will - be 'cdrom', otherwise 'disk'. + Config drive will be: + 'cdrom' in case of config_drive is set to iso9660; + 'disk' in case of config_drive is set to vfat; + 'fs' in case of os_type is EXE and virt_type is parallels; + Autodetected from (cdrom, disk, fs) in case of config_drive is None; + Otherwise, an exception of unknown format will be thrown. Returns a string indicating the config drive type. """ @@ -354,9 +358,22 @@ def get_config_drive_type(): config_drive_type = 'cdrom' elif CONF.config_drive_format == 'vfat': config_drive_type = 'disk' + elif CONF.config_drive_format is None: + if CONF.libvirt.virt_type == 'parallels': + if os_type == vm_mode.HVM: + config_drive_type = 'cdrom' + elif os_type == vm_mode.EXE: + config_drive_type = 'fs' + else: + raise exception.ConfigDriveUnknownFormat( + format=CONF.config_drive_format, + os_type=os_type) + else: + config_drive_type = 'cdrom' else: raise exception.ConfigDriveUnknownFormat( - format=CONF.config_drive_format) + format=CONF.config_drive_format, + os_type=os_type) return config_drive_type diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 5ef1b360626c..827524b5d054 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -2851,7 +2851,13 @@ class LibvirtDriver(driver.ComputeDriver): {'path': configdrive_path}, instance=instance) try: - cdb.make_drive(configdrive_path) + os_type = vm_mode.get_from_instance(instance) + if (os_type == vm_mode.EXE and + CONF.libvirt.virt_type == "parallels"): + cdb.make_drive(configdrive_path, + configdrive.IMAGE_TYPE_PLOOP) + else: + cdb.make_drive(configdrive_path) except processutils.ProcessExecutionError as e: with excutils.save_and_reraise_exception(): LOG.error(_LE('Creating config drive failed ' @@ -3164,6 +3170,12 @@ class LibvirtDriver(driver.ComputeDriver): elif os_type == vm_mode.EXE and CONF.libvirt.virt_type == "parallels": fs = self._get_guest_fs_config(instance, "disk") devices.append(fs) + if 'disk.config' in disk_mapping: + disk_config_image = self.image_backend.image(instance, + "disk.config", + "ploop") + devices.append(disk_config_image.libvirt_fs_info( + "/var/lib/cloud/seed/config_drive", "ploop")) else: if rescue: diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index cde9331ef358..dfb227ce862c 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -691,7 +691,7 @@ class VMwareVMOps(object): def _create_config_drive(self, instance, injected_files, admin_password, data_store_name, dc_name, upload_folder, cookies): - if CONF.config_drive_format != 'iso9660': + if CONF.config_drive_format not in ('iso9660', None): reason = (_('Invalid config_drive_format "%s"') % CONF.config_drive_format) raise exception.InstancePowerOnFailure(reason=reason)