Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for creating and managing LVM cache volumes #235

Merged
merged 3 commits into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,26 @@ will take on the device. Virtual size of VDO volume is set by `size` parameter.
e.g.: "30g", "50GiB".
Default value is equal to the size of the volume.

#### `cached`
This specifies whether the volume should be cached or not.
This is currently supported only for LVM volumes where dm-cache
is used.
__NOTE__: Only creating new cached volumes and removing cache from
an existing volume is currently supported. Enabling cache
for an existing volume is not yet supported.

#### `cache_size`
Size of the cache. `cache_size` format is intended to be human-readable,
e.g.: "30g", "50GiB".

#### `cache_mode`
Mode for the cache. Supported values include `writethrough` (default) and `writeback`.

#### `cache_devices`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PVs could be partitions or md as well as whole disks. Their names may be difficult to predict. Is it the case that, as of now, we cannot operate on non-existent PVs/VGs because blivet cannot yet create cache pools? If it is possible, is there a way to specify the cache devices in terms of which disks they are on in these cases?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I think we can allow specifying list of disks and look for PVs in their children. I'll update the PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added new commit that adds support for using disks for cache_devices instead of PVs.

List of devices that will be used for the cache. These should be either physical volumes or
drives these physical volumes are allocated on. Generally you want to select fast devices like
SSD or NVMe drives for cache.

#### `storage_safe_mode`
When true (the default), an error will occur instead of automatically removing existing devices and/or formatting.

Expand Down
5 changes: 5 additions & 0 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,8 @@ storage_volume_defaults:
compression: null
deduplication: null
vdo_pool_size: null

cached: false
cache_size: 0
cache_mode: null
cache_devices: []
67 changes: 66 additions & 1 deletion library/blivet.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ def _destroy(self):
def _manage_encryption(self):
self._device = self._manage_one_encryption(self._device)

def _manage_cache(self):
pass

def _resize(self):
""" Schedule actions as needed to ensure the device has the desired size. """
size = self._get_size()
Expand Down Expand Up @@ -552,6 +555,7 @@ def manage(self):
raise BlivetAnsibleError("failed to look up or create device '%s'" % self._volume['name'])

self._manage_encryption()
self._manage_cache()

# schedule reformat if appropriate
if self._device.raw_device.exists:
Expand Down Expand Up @@ -622,6 +626,10 @@ def _get_device_id(self):
def _resize(self):
pass

def _manage_cache(self):
if self._volume['cached']:
raise BlivetAnsibleError("caching is not supported for partition volumes")

def _create(self):
if self._device:
return
Expand Down Expand Up @@ -734,6 +742,55 @@ def _get_params_lvmraid(self):

return dict(seg_type=self._volume['raid_level'], pvs=pvs)

def _detach_cache(self):
""" Detach cache from the volume and remove the unused cache pool """
try:
cpool_name = self._device.cache.detach()
except Exception as e:
raise BlivetAnsibleError("failed to detach cache from volume '%s': %s" % (self._device.name, str(e)))

# full reset is needed for the cache pool to be added to the devicetree so we can remove it
self._blivet.reset()

cpool_name = cpool_name.rstrip("_cpool")
cpool_device = self._blivet.devicetree.resolve_device("%s-%s" % (self._device.vg.name, cpool_name))

self._blivet.destroy_device(cpool_device)

def _attach_cache(self):
""" Create a new cache pool and attach it to the volume """
raise BlivetAnsibleError("adding cache to an existing volume is currently not supported")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blivet unfortunately doesn't support creating a new cache pool. We can attach an existing cache pool to the LV, but we can't create it. I plan to add support for creating cache pools to blivet and I'll update this when the support is available.


def _manage_cache(self):
if not self._device:
# cache for newly created LVs is managed in _create
return

if self._volume['cached'] and not self._device.raw_device.cached:
self._attach_cache()
if not self._volume['cached'] and self._device.raw_device.cached:
self._detach_cache()

def _get_params_lvmcache(self):
parent = self._blivet_pool._device
fast_pvs = []
for cache_spec in self._volume['cache_devices']:
cache_device = self._blivet.devicetree.resolve_device(cache_spec)
if cache_device is None:
raise BlivetAnsibleError("cache device '%s' not found" % cache_spec)

pv_device = next((pv for pv in parent.pvs if cache_device.name in [an.name for an in pv.ancestors]),
None)
if pv_device is None:
raise BlivetAnsibleError("cache device '%s' doesn't seems to be a physical volume or its parent" % cache_spec)
fast_pvs.append(pv_device)

cache_request = devices.lvm.LVMCacheRequest(size=Size(self._volume['cache_size']),
mode=self._volume['cache_mode'],
pvs=fast_pvs)

return dict(cache_request=cache_request)

def _create(self):
if self._device:
return
Expand Down Expand Up @@ -765,6 +822,10 @@ def _create(self):
lvmraid_arguments = self._get_params_lvmraid()
newlv_arguments.update(lvmraid_arguments)

if self._volume['cached']:
lvmcache_arguments = self._get_params_lvmcache()
newlv_arguments.update(lvmcache_arguments)

try:
device = self._blivet.new_lv(**newlv_arguments)
except Exception as e:
Expand Down Expand Up @@ -1456,7 +1517,11 @@ def run_module():
state=dict(type='str', default='present', choices=['present', 'absent']),
type=dict(type='str'),
volumes=dict(type='list', elements='dict', default=list(),
options=dict(compression=dict(type='bool'),
options=dict(cached=dict(type='bool'),
cache_devices=dict(type='list', elements='str', default=list()),
cache_mode=dict(type='str'),
cache_size=dict(type='str'),
compression=dict(type='bool'),
deduplication=dict(type='bool'),
encryption=dict(type='bool'),
encryption_cipher=dict(type='str'),
Expand Down
45 changes: 45 additions & 0 deletions tests/test-verify-volume-cache.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---

- name: check cache options
block:

- name: Get information about the LV
command: >
lvs --noheadings --nameprefixes --units=b --nosuffix --unquoted
-o name,attr,cache_total_blocks,chunk_size,segtype
{{ storage_test_pool.name }}/{{ storage_test_volume.name }}
register: lvs
changed_when: false

- set_fact:
storage_test_lv_segtype: "{{ lvs.stdout|regex_search('LVM2_SEGTYPE=(\\S+)', '\\1') }}"

- name: check segment type
assert:
that: storage_test_volume.cached | ternary(storage_test_lv_segtype[0] == 'cache', storage_test_lv_segtype[0] != 'cache')
msg: Unexpected segtype {{ storage_test_lv_segtype }} for {{ storage_test_volume.name }}

- set_fact:
storage_test_lv_cache_size: "{{ lvs.stdout|regex_search('LVM2_CACHE_TOTAL_BLOCKS=([0-9]*) LVM2_CHUNK_SIZE=([0-9]+)', '\\1', '\\2') }}"
when: storage_test_volume.cached|bool

- name: parse the requested cache size
bsize:
size: "{{ storage_test_volume.cache_size }}"
register: storage_test_requested_cache_size
when: storage_test_volume.cached|bool

- set_fact:
storage_test_expected_cache_size: "{{ storage_test_requested_cache_size.bytes }}"
when: storage_test_volume.cached|bool

- name: Check cache size
assert:
that: (storage_test_expected_cache_size|int -
storage_test_lv_cache_size[0]|int * storage_test_lv_cache_size[1]|int)|abs / storage_test_expected_cache_size|int < 0.01
msg: >
Unexpected cache size, expected {{ storage_test_expected_cache_size }},
got {{ storage_test_lv_cache_size[0]|int * storage_test_lv_cache_size[1]|int }}
when: storage_test_volume.cached|bool

when: storage_test_volume.type == 'lvm' and _storage_test_volume_present
2 changes: 1 addition & 1 deletion tests/test-verify-volume.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
- set_fact:
_storage_volume_tests: ['mount', 'fstab', 'fs', 'device', 'encryption', 'md', 'size'] # fs: type, label device: name, type, disks
_storage_volume_tests: ['mount', 'fstab', 'fs', 'device', 'encryption', 'md', 'size', 'cache'] # fs: type, label device: name, type, disks
# future:
# device:
# compression
Expand Down
72 changes: 72 additions & 0 deletions tests/tests_create_lvm_cache_then_remove.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
- hosts: all
become: true
vars:
storage_safe_mode: false
storage_use_partitions: true
volume_group_size: '10g'
volume_size: '5g'
cache_size: '4g'
vojtechtrefny marked this conversation as resolved.
Show resolved Hide resolved
tags:
- tests::lvm

tasks:
- include_role:
name: linux-system-roles.storage

- name: Mark tasks to be skipped
set_fact:
storage_skip_checks:
- blivet_available
- packages_installed
- service_facts

- include_tasks: get_unused_disk.yml
vars:
min_size: "{{ volume_group_size }}"
max_return: 2
disks_needed: 2

- name: Create a cached LVM logical volume under volume group 'foo'
include_role:
name: linux-system-roles.storage
vars:
storage_pools:
- name: foo
disks: "{{ unused_disks }}"
volumes:
- name: test
size: "{{ volume_size }}"
cached: true
cache_size: "{{ cache_size }}"
cache_devices: "{{ [unused_disks[1]] }}"

- include_tasks: verify-role-results.yml

- name: Remove (detach) cache from the 'test' LV created above
include_role:
name: linux-system-roles.storage
vars:
storage_pools:
- name: foo
disks: "{{ unused_disks }}"
volumes:
- name: test
size: "{{ volume_size }}"
cached: false

- include_tasks: verify-role-results.yml

- name: Clean up
include_role:
name: linux-system-roles.storage
vars:
storage_pools:
- name: foo
disks: "{{ unused_disks }}"
state: "absent"
volumes:
- name: test
size: "{{ volume_size }}"

- include_tasks: verify-role-results.yml
79 changes: 79 additions & 0 deletions tests/tests_fatals_cache_volume.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
- hosts: all
become: true
vars:
storage_safe_mode: false
volume_group_size: '10g'
volume_size: '5g'
cache_size: '4g'
vojtechtrefny marked this conversation as resolved.
Show resolved Hide resolved
tags:
- tests::lvm

tasks:
- include_role:
name: linux-system-roles.storage

- name: Mark tasks to be skipped
set_fact:
storage_skip_checks:
- blivet_available
- packages_installed
- service_facts

- include_tasks: get_unused_disk.yml
vars:
max_return: 2
disks_needed: 2

- name: Verify that creating a cached partition volume fails
block:
- name: Create cached partition
include_role:
name: linux-system-roles.storage
vars:
storage_pools:
- name: "{{ unused_disks[0] }}"
type: partition
disks: "{{ unused_disks }}"
volumes:
- name: test1
type: partition
cached: true

- name: unreachable task
fail:
msg: UNREACH

rescue:
- name: Check that we failed in the role
assert:
that:
- ansible_failed_result.msg != 'UNREACH'
msg: "Role has not failed when it should have"

- name: Verify that creating cache on unused disk fails
block:
- name: Create cached volume
include_role:
name: linux-system-roles.storage
vars:
storage_pools:
- name: "foo"
disks: "{{ [unused_disks[0]] }}"
volumes:
- name: test1
size: "{{ volume_size }}"
cached: true
cache_size: "{{ cache_size }}"
cache_devices: "{{ [unused_disks[1]] }}"

- name: unreachable task
fail:
msg: UNREACH

rescue:
- name: Check that we failed in the role
assert:
that:
- ansible_failed_result.msg != 'UNREACH'
msg: "Role has not failed when it should have"