diff --git a/diskimage_builder/block_device/level1/lvm.py b/diskimage_builder/block_device/level1/lvm.py index 84fcef016..1b6bfa297 100644 --- a/diskimage_builder/block_device/level1/lvm.py +++ b/diskimage_builder/block_device/level1/lvm.py @@ -23,6 +23,7 @@ from diskimage_builder.block_device.utils import parse_abs_size_spec from diskimage_builder.block_device.utils import remove_device PHYSICAL_EXTENT_BYTES = parse_abs_size_spec('4MiB') +LVS_TYPES = ['thin', 'thin-pool'] logger = logging.getLogger(__name__) @@ -173,7 +174,8 @@ class VgsNode(NodeBase): class LvsNode(NodeBase): - def __init__(self, name, state, base, options, size, extents): + def __init__(self, name, state, base, options, size, extents, segtype, + thin_pool): """Logical Volume This is a placeholder node for a logical volume @@ -186,21 +188,31 @@ class LvsNode(NodeBase): :param size: size of the LV, using the supported unit types (MB, MiB, etc) :param extents: size of the LV in extents + :param segtype: value passed to segment type, supports thin and + thin-pool + :param thin_pool: name of the thin pool to create this volume from """ super(LvsNode, self).__init__(name, state) self.base = base self.options = options self.size = size self.extents = extents + self.type = segtype + self.thin_pool = thin_pool def _create(self): cmd = ["lvcreate", ] cmd.extend(['--name', self.name]) + if self.type: + cmd.extend(['--type', self.type]) + if self.thin_pool: + cmd.extend(['--thin-pool', self.thin_pool]) if self.size: size = parse_abs_size_spec(self.size) # ensuire size aligns with physical extents size = size - size % PHYSICAL_EXTENT_BYTES - cmd.extend(['-L', '%dB' % size]) + size_arg = '-V' if self.type == 'thin' else '-L' + cmd.extend([size_arg, '%dB' % size]) elif self.extents: cmd.extend(['-l', self.extents]) if self.options: @@ -391,10 +403,18 @@ class LVMPlugin(PluginBase): self._config_error("base:%s in lvs does not match a valid vg" % lvs_cfg['base']) + if 'type' in lvs_cfg: + if lvs_cfg['type'] not in LVS_TYPES: + self._config_error( + "Unsupported type:%s, supported types: %s" % + (lvs_cfg['type'], ', '.join(LVS_TYPES))) + lvs_item = LvsNode(lvs_cfg['name'], state, lvs_cfg['base'], lvs_cfg.get('options', None), lvs_cfg.get('size', None), - lvs_cfg.get('extents', None)) + lvs_cfg.get('extents', None), + lvs_cfg.get('type', None), + lvs_cfg.get('thin-pool', None)) self.lvs.append(lvs_item) # create the "driver" node diff --git a/diskimage_builder/block_device/tests/config/lvm_tree_thin_provision.yaml b/diskimage_builder/block_device/tests/config/lvm_tree_thin_provision.yaml new file mode 100644 index 000000000..0ab93f13b --- /dev/null +++ b/diskimage_builder/block_device/tests/config/lvm_tree_thin_provision.yaml @@ -0,0 +1,122 @@ +- local_loop: + name: image0 + +- partitioning: + base: image0 + name: mbr + label: mbr + partitions: + - name: root + base: image0 + flags: [ boot,primary ] + size: 3G + +- lvm: + base: mbr + pvs: + - name: pv + options: ["--force"] + base: root + + vgs: + - name: vg + base: ["pv"] + options: ["--force"] + + lvs: + - name: lv_thinpool + type: thin-pool + base: vg + size: 2800MiB + + - name: lv_root + type: thin + thin-pool: lv_thinpool + base: vg + size: 1800MiB + + - name: lv_tmp + type: thin + thin-pool: lv_thinpool + base: vg + size: 100MiB + + - name: lv_var + type: thin + thin-pool: lv_thinpool + base: vg + size: 500MiB + + - name: lv_log + type: thin + thin-pool: lv_thinpool + base: vg + size: 100MiB + + - name: lv_audit + type: thin + thin-pool: lv_thinpool + base: vg + size: 100MiB + + - name: lv_home + type: thin + thin-pool: lv_thinpool + base: vg + size: 200MiB + +- mkfs: + name: fs_root + base: lv_root + label: "img-rootfs" + type: "xfs" + mount: + mount_point: / + fstab: + options: "rw,relatime" + fsck-passno: 1 + +- mkfs: + name: fs_var + base: lv_var + type: "xfs" + mount: + mount_point: /var + fstab: + options: "rw,relatime" + +- mkfs: + name: fs_log + base: lv_log + type: "xfs" + mount: + mount_point: /var/log + fstab: + options: "rw,relatime" + +- mkfs: + name: fs_audit + base: lv_audit + type: "xfs" + mount: + mount_point: /var/log/audit + fstab: + options: "rw,relatime" + +- mkfs: + name: fs_tmp + base: lv_tmp + type: "xfs" + mount: + mount_point: /tmp + fstab: + options: "rw,nosuid,nodev,noexec,relatime" + +- mkfs: + name: fs_home + base: lv_home + type: "xfs" + mount: + mount_point: /home + fstab: + options: "rw,nodev,relatime" diff --git a/diskimage_builder/block_device/tests/test_lvm.py b/diskimage_builder/block_device/tests/test_lvm.py index f39ba03d3..692f1a1eb 100644 --- a/diskimage_builder/block_device/tests/test_lvm.py +++ b/diskimage_builder/block_device/tests/test_lvm.py @@ -582,3 +582,129 @@ class TestLVM(tc.TestGraphGeneration): ] manager.assert_has_calls(cmd_sequence) + + @mock.patch('diskimage_builder.block_device.level1.lvm.exec_sudo') + def test_lvm_thin_provision(self, mock_exec_sudo): + # Test the command-sequence for a more complicated LVM setup + tree = self.load_config_file('lvm_tree_thin_provision.yaml') + config = config_tree_to_graph(tree) + + state = BlockDeviceState() + + graph, call_order = create_graph(config, self.fake_default_config, + state) + + # Fake state for the two PV's specified by this config + state['blockdev'] = {} + state['blockdev']['root'] = {} + state['blockdev']['root']['device'] = '/dev/fake/root' + + for node in call_order: + # XXX: This has not mocked out the "lower" layers of + # creating the devices, which we're assuming works OK, nor + # the upper layers. + if isinstance(node, (LVMNode, PvsNode, + VgsNode, LvsNode)): + # only the LVMNode actually does anything here... + node.create() + + # ensure the sequence of calls correctly setup the devices + cmd_sequence = [ + # create the pv's on the faked out block devices + mock.call(['pvcreate', '/dev/fake/root', '--force']), + # create a volume called "vg" out of these two pv's + mock.call(['vgcreate', 'vg', '/dev/fake/root', '--force']), + mock.call(['lvcreate', '--name', 'lv_thinpool', + '--type', 'thin-pool', '-L', '2936012800B', 'vg']), + # create a bunch of lv's on vg using the pool + mock.call(['lvcreate', '--name', 'lv_root', '--type', 'thin', + '--thin-pool', 'lv_thinpool', '-V', '1887436800B', + 'vg']), + mock.call(['lvcreate', '--name', 'lv_tmp', '--type', 'thin', + '--thin-pool', 'lv_thinpool', '-V', '104857600B', 'vg']), + mock.call(['lvcreate', '--name', 'lv_var', '--type', 'thin', + '--thin-pool', 'lv_thinpool', '-V', '524288000B', 'vg']), + mock.call(['lvcreate', '--name', 'lv_log', '--type', 'thin', + '--thin-pool', 'lv_thinpool', '-V', '104857600B', 'vg']), + mock.call(['lvcreate', '--name', 'lv_audit', '--type', 'thin', + '--thin-pool', 'lv_thinpool', '-V', '104857600B', 'vg']), + mock.call(['lvcreate', '--name', 'lv_home', '--type', 'thin', + '--thin-pool', 'lv_thinpool', '-V', '209715200B', 'vg'])] + + self.assertEqual(mock_exec_sudo.call_count, len(cmd_sequence)) + mock_exec_sudo.assert_has_calls(cmd_sequence) + + # Ensure the correct LVM state was preserved + blockdev_state = { + 'root': {'device': '/dev/fake/root'}, + 'lv_thinpool': { + 'vgs': 'vg', + 'size': '2800MiB', + 'extents': None, + 'opts': None, + 'device': '/dev/mapper/vg-lv_thinpool' + }, + 'lv_root': { + 'vgs': 'vg', + 'size': '1800MiB', + 'extents': None, + 'opts': None, + 'device': '/dev/mapper/vg-lv_root' + }, + 'lv_tmp': { + 'vgs': 'vg', + 'size': '100MiB', + 'extents': None, + 'opts': None, + 'device': '/dev/mapper/vg-lv_tmp' + }, + 'lv_var': { + 'vgs': 'vg', + 'size': '500MiB', + 'extents': None, + 'opts': None, + 'device': '/dev/mapper/vg-lv_var' + }, + 'lv_log': { + 'vgs': 'vg', + 'size': '100MiB', + 'extents': None, + 'opts': None, + 'device': '/dev/mapper/vg-lv_log' + }, + 'lv_audit': { + 'vgs': 'vg', + 'size': '100MiB', + 'extents': None, + 'opts': None, + 'device': '/dev/mapper/vg-lv_audit' + }, + 'lv_home': { + 'vgs': 'vg', + 'size': '200MiB', + 'extents': None, + 'opts': None, + 'device': '/dev/mapper/vg-lv_home' + } + } + + self.assertDictEqual(state['blockdev'], blockdev_state) + + @mock.patch('diskimage_builder.block_device.level1.lvm.exec_sudo') + def test_validate_lvs_type(self, mock_exec_sudo): + # Test the command-sequence for a more complicated LVM setup + tree = self.load_config_file('lvm_tree_thin_provision.yaml') + print(tree) + tree[2]['lvm']['lvs'][0]['type'] = 'thin-pol' + + config = config_tree_to_graph(tree) + + state = BlockDeviceState() + + self.assertRaisesRegex( + BlockDeviceSetupException, + "Unsupported type:thin-pol, supported types: thin, thin-pool", + create_graph, + config, + self.fake_default_config, + state) diff --git a/doc/source/user_guide/building_an_image.rst b/doc/source/user_guide/building_an_image.rst index e7d2ae8ba..97c9d2fbd 100644 --- a/doc/source/user_guide/building_an_image.rst +++ b/doc/source/user_guide/building_an_image.rst @@ -435,6 +435,14 @@ options (optional) List of options for the logical volume. It can contain any option supported by the `lvcreate` command. +type + (optional) When set to `thin-pool` a thin pool volume will be created. When + set to `thin` the thin volume will be backed by the thin pool named with the + `thin-pool` key. + +thin-pool + (optional) Name of the thin pool to use for this thin volume. + Example: .. code-block:: yaml diff --git a/releasenotes/notes/thin-provision-c57db8003acec386.yaml b/releasenotes/notes/thin-provision-c57db8003acec386.yaml new file mode 100644 index 000000000..095938eeb --- /dev/null +++ b/releasenotes/notes/thin-provision-c57db8003acec386.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + LVM thin provisioning is now supported in the block device `lvs` node. Thin + pools can be defined and thin volumes associated with those pools.