From 6acad1150085a0e87d35466f1869124b722153ee Mon Sep 17 00:00:00 2001 From: David Lehman Date: Fri, 5 Jun 2020 14:21:27 -0400 Subject: [PATCH 1/8] Add basic support for managing LUKS volumes. --- README.md | 21 ++ defaults/main.yml | 7 + library/blivet.py | 44 ++++ tests/test-verify-volume-encryption.yml | 35 +++ tests/test-verify-volume.yml | 3 +- tests/tests_luks.yml | 287 ++++++++++++++++++++++++ vars/CentOS-7.yml | 1 + vars/CentOS-8.yml | 1 + vars/Fedora.yml | 1 + vars/RedHat-7.yml | 1 + vars/RedHat-8.yml | 1 + 11 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 tests/test-verify-volume-encryption.yml create mode 100644 tests/tests_luks.yml diff --git a/README.md b/README.md index f808adcd..efc882d4 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,27 @@ The `mount_point` specifies the directory on which the file system will be mount ##### `mount_options` The `mount_options` specifies custom mount options as a string, e.g.: 'ro'. +##### `encryption` +This specifies whether or not the volume will be encrypted using LUKS. +__WARNING__: Toggling encryption for a volume is a destructive operation, meaning + all data on that volume will be removed as part of the process of + adding/removing the encryption layer. + +##### `encryption_passphrase` +This string specifies a passphrase used to unlock/open the LUKS volume. + +##### `encryption_key_file` +This string specifies the full path to the key file used to unlock the LUKS volume. + +##### `encryption_cipher` +This string specifies a non-default cipher to be used by LUKS. + +##### `encryption_key_size` +This integer specifies the LUKS key size (in bits). + +##### `encryption_luks_version` +This integer specifies the LUKS version to use. + #### `storage_safe_mode` When true (the default), an error will occur instead of automatically removing existing devices and/or formatting. diff --git a/defaults/main.yml b/defaults/main.yml index 476616b9..5fe229fb 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -24,3 +24,10 @@ storage_volume_defaults: mount_check: 0 mount_passno: 0 mount_device_identifier: "uuid" # uuid|label|path + + encryption: false + encryption_passphrase: null + encryption_key_file: null + encryption_cipher: null + encryption_key_size: null + encryption_luks_version: null diff --git a/library/blivet.py b/library/blivet.py index 358ad9f9..e17b260a 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -224,6 +224,48 @@ def _destroy(self): # schedule removal of this device and any descendant devices self._blivet.devicetree.recursive_remove(self._device) + def _manage_encryption(self): + # Make sure to handle adjusting both existing stacks and future stacks. + if self._device == self._device.raw_device and self._volume['encryption']: + # add luks + luks_name = "luks-%s" % self._device._name + if not self._device.format.exists: + fmt = self._device.format + else: + fmt = get_format(None) + + self._blivet.format_device(self._device, + get_format("luks", + name=luks_name, + cipher=self._volume.get('encryption_cipher'), + key_size=self._volume.get('encryption_key_size'), + luks_version=self._volume.get('encryption_luks_version'), + passphrase=self._volume.get('encryption_passphrase') or None, + key_file=self._volume.get('encryption_key_file') or None)) + + if not self._device.format.has_key: + raise BlivetAnsibleError("encrypted volume '%s' missing key/passphrase" % self._volume['name']) + + luks_device = devices.LUKSDevice(luks_name, + fmt=fmt, + parents=[self._device]) + self._blivet.create_device(luks_device) + self._device = luks_device + elif self._device != self._device.raw_device and not self._volume['encryption']: + # remove luks + if not self._device.format.exists: + fmt = self._device.format + else: + fmt = get_format(None) + + self._device = self._device.raw_device + self._blivet.destroy_device(self._device.children[0]) + if fmt.type is not None: + self._blivet.format_device(self._device, fmt) + + # XXX: blivet has to store cipher, key_size, luks_version for existing before we + # can support re-encrypting based on changes to those parameters + def _resize(self): """ Schedule actions as needed to ensure the device has the desired size. """ try: @@ -275,6 +317,8 @@ def manage(self): if self._device is None: raise BlivetAnsibleError("failed to look up or create device '%s'" % self._volume['name']) + self._manage_encryption() + # schedule reformat if appropriate if self._device.exists: self._reformat() diff --git a/tests/test-verify-volume-encryption.yml b/tests/test-verify-volume-encryption.yml new file mode 100644 index 00000000..5418ab57 --- /dev/null +++ b/tests/test-verify-volume-encryption.yml @@ -0,0 +1,35 @@ +--- +# name/path +- name: Stat the LUKS device, if encrypted + stat: + path: "{{ storage_test_volume._device }}" + follow: yes + register: storage_test_luks_dev + when: storage_test_volume.encryption + +- name: Verify the presence/absence of the LUKS device node + assert: + that: "{{ storage_test_luks_dev.stat.exists and storage_test_luks_dev.stat.isblk + if _storage_test_volume_present or storage_test_volume.type == 'disk' + else + not storage_test_luks_dev.stat.exists }}" + msg: "Incorrect device node presence for volume {{ storage_test_volume.name }}" + when: storage_test_volume.encryption + +- name: Verify that the raw device is the same as the device if not encrypted + assert: + that: "{{ (storage_test_volume._device != storage_test_volume._raw_device)|bool == (storage_test_volume.encryption|bool) }}" + msg: "Encryption not managed correctly for volume {{ storage_test_volume.name }}: {{ (storage_test_volume._device != storage_test_volume._raw_device) }} {{ storage_test_volume.encryption|bool }}" + when: _storage_test_volume_present + +- name: Make sure we got info about the LUKS volume if encrypted + assert: + that: "{{ storage_test_volume._device in storage_test_blkinfo.info }}" + msg: "Failed to gather info about volume '{{ storage_test_volume.name }}'" + when: _storage_test_volume_present and storage_test_volume.encryption + +- name: Verify the LUKS volume's device type if encrypted + assert: + that: "{{ storage_test_blkinfo.info[storage_test_volume._device].type == 'crypt' }}" + when: _storage_test_volume_present and storage_test_volume.encryption + diff --git a/tests/test-verify-volume.yml b/tests/test-verify-volume.yml index 389367dc..d1dbd5f5 100644 --- a/tests/test-verify-volume.yml +++ b/tests/test-verify-volume.yml @@ -1,9 +1,8 @@ --- - set_fact: - _storage_volume_tests: ['mount', 'fstab', 'fs', 'device'] # fs: type, label device: name, size, type, disks + _storage_volume_tests: ['mount', 'fstab', 'fs', 'device', 'encryption'] # fs: type, label device: name, size, type, disks # future: # device: - # encryption # raid # compression # deduplication diff --git a/tests/tests_luks.yml b/tests/tests_luks.yml new file mode 100644 index 00000000..6fec3f99 --- /dev/null +++ b/tests/tests_luks.yml @@ -0,0 +1,287 @@ +--- +- hosts: all + become: true + vars: + storage_safe_mode: false + mount_location: '/opt/test1' + volume_size: '5g' + + tasks: + - include_role: + name: storage + + - include_tasks: get_unused_disk.yml + vars: + min_size: "{{ volume_size }}" + max_return: 1 + + ## + ## Disk + ## + - name: Test for correct handling of new encrypted volume w/ no key + block: + - name: Create an encrypted disk volume w/ default fs + include_role: + name: storage + vars: + storage_volumes: + - name: foo + type: disk + disks: "{{ unused_disks }}" + mount_point: "{{ mount_location }}" + encryption: 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 the output of the keyless luks test + assert: + that: "blivet_output.failed and + blivet_output.msg|regex_search('encrypted volume.*missing key') and + not blivet_output.changed" + msg: "Unexpected behavior w/ encrypted pool w/ no key" + + # encrypted disk volume + - name: Create an encrypted disk volume w/ default fs + include_role: + name: storage + vars: + storage_volumes: + - name: foo + type: disk + disks: "{{ unused_disks }}" + mount_point: "{{ mount_location }}" + encryption: true + encryption_passphrase: 'yabbadabbadoo' + + - include_tasks: verify-role-results.yml + + - name: Remove the encryption layer + include_role: + name: storage + vars: + storage_volumes: + - name: foo + type: disk + disks: "{{ unused_disks }}" + mount_point: "{{ mount_location }}" + encryption: false + encryption_passphrase: 'yabbadabbadoo' + + - include_tasks: verify-role-results.yml + + - name: Add encryption to the volume + include_role: + name: storage + vars: + storage_volumes: + - name: foo + type: disk + disks: "{{ unused_disks }}" + mount_point: "{{ mount_location }}" + encryption: true + encryption_passphrase: 'yabbadabbadoo' + + - include_tasks: verify-role-results.yml + + ## + ## Partition + ## + + - name: Test for correct handling of new encrypted volume w/ no key + block: + - name: Create an encrypted partition volume w/ default fs + include_role: + name: storage + vars: + storage_pools: + - name: foo + type: partition + disks: "{{ unused_disks }}" + volumes: + - name: test1 + type: partition + mount_point: "{{ mount_location }}" + size: 4g + encryption: 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 the output of the keyless luks test + assert: + that: "blivet_output.failed and + blivet_output.msg|regex_search('encrypted volume.*missing key') and + not blivet_output.changed" + msg: "Unexpected behavior w/ encrypted pool w/ no key" + + - name: Create an encrypted partition volume w/ default fs + include_role: + name: storage + vars: + storage_pools: + - name: foo + type: partition + disks: "{{ unused_disks }}" + volumes: + - name: test1 + type: partition + mount_point: "{{ mount_location }}" + size: 4g + encryption: true + encryption_passphrase: 'yabbadabbadoo' + + - include_tasks: verify-role-results.yml + + - name: Remove the encryption layer + include_role: + name: storage + vars: + storage_pools: + - name: foo + type: partition + disks: "{{ unused_disks }}" + volumes: + - name: test1 + type: partition + mount_point: "{{ mount_location }}" + size: 4g + encryption: false + encryption_passphrase: 'yabbadabbadoo' + + - include_tasks: verify-role-results.yml + + - name: Add encryption to the volume + include_role: + name: storage + vars: + storage_pools: + - name: foo + type: partition + disks: "{{ unused_disks }}" + volumes: + - name: test1 + type: partition + mount_point: "{{ mount_location }}" + size: 4g + encryption: true + encryption_passphrase: 'yabbadabbadoo' + + - include_tasks: verify-role-results.yml + + ## + ## LVM + ## + + - name: Test for correct handling of new encrypted volume w/ no key + block: + - name: Create an encrypted lvm volume w/ default fs + include_role: + name: storage + vars: + storage_pools: + - name: foo + type: lvm + disks: "{{ unused_disks }}" + volumes: + - name: test1 + mount_point: "{{ mount_location }}" + size: 4g + encryption: 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 the output of the keyless luks test + assert: + that: "blivet_output.failed and + blivet_output.msg|regex_search('encrypted volume.*missing key') and + not blivet_output.changed" + msg: "Unexpected behavior w/ encrypted pool w/ no key" + + - name: Create an encrypted lvm volume w/ default fs + include_role: + name: storage + vars: + storage_pools: + - name: foo + type: lvm + disks: "{{ unused_disks }}" + volumes: + - name: test1 + mount_point: "{{ mount_location }}" + size: 4g + encryption: true + encryption_passphrase: 'yabbadabbadoo' + + - include_tasks: verify-role-results.yml + + - name: Remove the encryption layer + include_role: + name: storage + vars: + storage_pools: + - name: foo + type: lvm + disks: "{{ unused_disks }}" + volumes: + - name: test1 + mount_point: "{{ mount_location }}" + size: 4g + encryption: false + encryption_passphrase: 'yabbadabbadoo' + + - include_tasks: verify-role-results.yml + + - name: Add encryption to the volume + include_role: + name: storage + vars: + storage_pools: + - name: foo + type: lvm + disks: "{{ unused_disks }}" + volumes: + - name: test1 + mount_point: "{{ mount_location }}" + size: 4g + encryption: true + encryption_passphrase: 'yabbadabbadoo' + + - include_tasks: verify-role-results.yml + + - name: Clean up + include_role: + name: storage + vars: + storage_volumes: + - name: foo + type: disk + disks: "{{ unused_disks }}" + state: absent + + - include_tasks: verify-role-results.yml diff --git a/vars/CentOS-7.yml b/vars/CentOS-7.yml index fa7249fa..712a31f3 100644 --- a/vars/CentOS-7.yml +++ b/vars/CentOS-7.yml @@ -1,6 +1,7 @@ blivet_package_list: - python-enum34 - python-blivet3 + - libblockdev-crypto - libblockdev-dm - libblockdev-lvm - libblockdev-swap diff --git a/vars/CentOS-8.yml b/vars/CentOS-8.yml index 6d3577b2..f935723d 100644 --- a/vars/CentOS-8.yml +++ b/vars/CentOS-8.yml @@ -1,5 +1,6 @@ blivet_package_list: - python3-blivet + - libblockdev-crypto - libblockdev-dm - libblockdev-lvm - libblockdev-swap diff --git a/vars/Fedora.yml b/vars/Fedora.yml index 6d3577b2..f935723d 100644 --- a/vars/Fedora.yml +++ b/vars/Fedora.yml @@ -1,5 +1,6 @@ blivet_package_list: - python3-blivet + - libblockdev-crypto - libblockdev-dm - libblockdev-lvm - libblockdev-swap diff --git a/vars/RedHat-7.yml b/vars/RedHat-7.yml index fa7249fa..712a31f3 100644 --- a/vars/RedHat-7.yml +++ b/vars/RedHat-7.yml @@ -1,6 +1,7 @@ blivet_package_list: - python-enum34 - python-blivet3 + - libblockdev-crypto - libblockdev-dm - libblockdev-lvm - libblockdev-swap diff --git a/vars/RedHat-8.yml b/vars/RedHat-8.yml index 6d3577b2..f935723d 100644 --- a/vars/RedHat-8.yml +++ b/vars/RedHat-8.yml @@ -1,5 +1,6 @@ blivet_package_list: - python3-blivet + - libblockdev-crypto - libblockdev-dm - libblockdev-lvm - libblockdev-swap From b0753d2ba5b593dfff7a44594657c93535004ea4 Mon Sep 17 00:00:00 2001 From: David Lehman Date: Fri, 5 Jun 2020 14:23:17 -0400 Subject: [PATCH 2/8] Add logic required to associate existing LUKS devices w/ volumes. --- library/blivet.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/library/blivet.py b/library/blivet.py index e17b260a..3377a315 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -190,6 +190,20 @@ def _look_up_device(self): if device is None: return + if device.format.type == 'luks': + # XXX If we have no key we will always re-encrypt. + device.format._key_file = self._volume.get('encryption_key_file') + device.format.passphrase = self._volume.get('encryption_passphrase') + + # set up the original format as well since it'll get used for processing + device.original_format._key_file = self._volume.get('encryption_key_file') + device.original_format.passphrase = self._volume.get('encryption_passphrase') + if device.isleaf: + self._blivet.populate() + + if not device.isleaf: + device = device.children[0] + self._device = device # check that the type is correct, raising an exception if there is a name conflict From 236b12b25e2ef1228e7ff6388c9075a7a0af7644 Mon Sep 17 00:00:00 2001 From: David Lehman Date: Fri, 5 Jun 2020 14:26:24 -0400 Subject: [PATCH 3/8] Add some special handling for volumes' LUKS layer. This probably merits an explanation. In blivet, the backing device will have a format of type 'luks'. This is the encrypted, or backing, device. The next layer out is a LUKSDevice, which represents the (decrypted/open) device-mapper device. Because the LUKS layer is optional and can be effectively toggled, there are many occasions on which it is convenient to look past the LUKS layer directly to the backing device. For this purpose, all of blivet's StorageDevice classes have a 'raw_device' property. For unexcrypted leaf devices, the raw device is the same as the actual device. For encrypted leaf devices, the raw device points to the backing device. In other words, adding _raw_device establishes a line between the raw/backing/encrypted device and the decrypted/mapped/open device which makes test validation quite a bit easier. --- library/blivet.py | 22 +++++++++++++++++----- tests/test-verify-volume-device.yml | 6 +++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/library/blivet.py b/library/blivet.py index 3377a315..428068e6 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -233,10 +233,11 @@ def _destroy(self): # save device identifiers for use by the role self._volume['_device'] = self._device.path + self._volume['_raw_device'] = self._device.raw_device.path self._volume['_mount_id'] = self._device.fstab_spec # schedule removal of this device and any descendant devices - self._blivet.devicetree.recursive_remove(self._device) + self._blivet.devicetree.recursive_remove(self._device.raw_device) def _manage_encryption(self): # Make sure to handle adjusting both existing stacks and future stacks. @@ -334,15 +335,16 @@ def manage(self): self._manage_encryption() # schedule reformat if appropriate - if self._device.exists: + if self._device.raw_device.exists: self._reformat() # schedule resize if appropriate - if self._device.exists and self._volume['size']: + if self._device.raw_device.exists and self._volume['size']: self._resize() # save device identifiers for use by the role self._volume['_device'] = self._device.path + self._volume['_raw_device'] = self._device.raw_device.path self._volume['_mount_id'] = self._device.fstab_spec @@ -353,7 +355,7 @@ def _get_device_id(self): return self._volume['disks'][0] def _type_check(self): - return self._device.is_disk + return self._device.raw_device.is_disk def _look_up_device(self): super(BlivetDiskVolume, self)._look_up_device() @@ -371,7 +373,7 @@ class BlivetPartitionVolume(BlivetVolume): blivet_device_class = devices.PartitionDevice def _type_check(self): - return self._device.type == 'partition' + return self._device.raw_device.type == 'partition' def _get_device_id(self): return self._blivet_pool._disks[0].name + '1' @@ -664,6 +666,7 @@ def manage_volume(b, volume): bvolume = _get_blivet_volume(b, volume) bvolume.manage() volume['_device'] = bvolume._volume.get('_device', '') + volume['_raw_device'] = bvolume._volume.get('_raw_device', '') volume['_mount_id'] = bvolume._volume.get('_mount_id', '') @@ -673,6 +676,7 @@ def manage_pool(b, pool): bpool.manage() for (volume, bvolume) in zip(pool['volumes'], bpool._blivet_volumes): volume['_device'] = bvolume._volume.get('_device', '') + volume['_raw_device'] = bvolume._volume.get('_raw_device', '') volume['_mount_id'] = bvolume._volume.get('_mount_id', '') @@ -809,6 +813,14 @@ def update_fstab_identifiers(b, pools, volumes): for volume in all_volumes: if volume['state'] == 'present': device = b.devicetree.resolve_device(volume['_mount_id']) + if device is None and volume['encryption']: + device = b.devicetree.resolve_device(volume['_raw_device']) + if device is not None and not device.isleaf: + device = device.children[0] + volume['_device'] = device.path + + if device is None: + raise BlivetAnsibleError("failed to look up device for volume %s (%s/%s)" % (volume['name'], volume['_device'], volume['_mount_id'])) volume['_mount_id'] = device.fstab_spec if device.format.type == 'swap': device.format.setup() diff --git a/tests/test-verify-volume-device.yml b/tests/test-verify-volume-device.yml index 6b7d2f2d..ba23b564 100644 --- a/tests/test-verify-volume-device.yml +++ b/tests/test-verify-volume-device.yml @@ -2,7 +2,7 @@ # name/path - name: See whether the device node is present stat: - path: "{{ storage_test_volume._device }}" + path: "{{ storage_test_volume._raw_device }}" follow: yes register: storage_test_dev @@ -16,13 +16,13 @@ - name: Make sure we got info about this volume assert: - that: "{{ storage_test_volume._device in storage_test_blkinfo.info }}" + that: "{{ storage_test_volume._raw_device in storage_test_blkinfo.info }}" msg: "Failed to gather info about volume '{{ storage_test_volume.name }}'" when: _storage_test_volume_present - name: Verify the volume's device type assert: - that: "{{ storage_test_blkinfo.info[storage_test_volume._device].type == storage_test_volume.type }}" + that: "{{ storage_test_blkinfo.info[storage_test_volume._raw_device].type == storage_test_volume.type }}" when: _storage_test_volume_present # disks From 4208f47858ae9b93c2af7799312b3327ff9fd20b Mon Sep 17 00:00:00 2001 From: David Lehman Date: Fri, 5 Jun 2020 14:37:03 -0400 Subject: [PATCH 4/8] Only deactivate mounted fs and swap in preparing the action queue. Doing so for all formats can trigger deactivation of device stacks that needlessly complicates things. --- library/blivet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/blivet.py b/library/blivet.py index 428068e6..85abe007 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -311,7 +311,7 @@ def _reformat(self): if safe_mode and (self._device.format.type is not None or self._device.format.name != get_format(None).name): raise BlivetAnsibleError("cannot remove existing formatting on volume '%s' in safe mode" % self._volume['name']) - if self._device.format.status: + if self._device.format.status and (self._device.format.mountable or self._device.format.type == "swap"): self._device.format.teardown() self._blivet.format_device(self._device, fmt) @@ -934,7 +934,8 @@ def action_dict(action): result['packages'] = b.packages[:] for action in scheduled: - if action.is_destroy and action.is_format and action.format.exists: + if action.is_destroy and action.is_format and action.format.exists and \ + (action.format.mountable or action.format.type == "swap"): action.format.teardown() if scheduled: From c764dd857e1921f0c27aea5e5f53533a06681125 Mon Sep 17 00:00:00 2001 From: David Lehman Date: Fri, 5 Jun 2020 14:38:16 -0400 Subject: [PATCH 5/8] Add a _create method to BlivetDiskVolume. Relying on _reformat to create the formatting worked because a DiskDevice always exists. Now that self._device can be an optional, non-existent, LUKS layer on top of the disk we have to make the disk volume class behave more like the other volume classes -- namely, it has to create its format as part of _create since we will not always call _reformat (eg: when we've set up, but not yet created, a new LUKS layer on the disk). --- library/blivet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/blivet.py b/library/blivet.py index 85abe007..6fd13457 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -357,6 +357,9 @@ def _get_device_id(self): def _type_check(self): return self._device.raw_device.is_disk + def _create(self): + self._reformat() + def _look_up_device(self): super(BlivetDiskVolume, self)._look_up_device() if not self._get_device_id(): From 9ab01a80eea223a4598b2459c555bf6e994d884b Mon Sep 17 00:00:00 2001 From: David Lehman Date: Mon, 8 Jun 2020 12:36:30 -0400 Subject: [PATCH 6/8] Manage mounts in two stages to keep systemd cooperative. --- tasks/main-blivet.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tasks/main-blivet.yml b/tasks/main-blivet.yml index 3044e01b..48e6aecb 100644 --- a/tasks/main-blivet.yml +++ b/tasks/main-blivet.yml @@ -129,14 +129,34 @@ # changed options? (just add w/ new settings?) # add new mounts # -- name: manage mounts to match the specified state +# XXX Apparently we have to do the removals, then tell systemd to +# update its view, then set up the new mounts. Otherwise, +# systemd will forcibly prevent mounting a new volume to an +# existing mount point. +- name: remove obsolete mounts mount: src: "{{ mount_info['src']|default(omit) }}" path: "{{ mount_info['path'] }}" fstype: "{{ mount_info['fstype']|default(omit) }}" opts: "{{ mount_info['opts']|default(omit) }}" state: "{{ mount_info['state'] }}" - loop: "{{ blivet_output.mounts }}" + loop: "{{ blivet_output.mounts|json_query('[?state==`absent`]') }}" + loop_control: + loop_var: mount_info + +- name: tell systemd to refresh its view of /etc/fstab + systemd: + daemon_reload: yes + when: blivet_output['mounts'] + +- name: set up new/current mounts + mount: + src: "{{ mount_info['src']|default(omit) }}" + path: "{{ mount_info['path'] }}" + fstype: "{{ mount_info['fstype']|default(omit) }}" + opts: "{{ mount_info['opts']|default(omit) }}" + state: "{{ mount_info['state'] }}" + loop: "{{ blivet_output.mounts|json_query('[?state!=`absent`]') }}" loop_control: loop_var: mount_info From 485f10bb8071390ef3a452e11207f2027909c2db Mon Sep 17 00:00:00 2001 From: David Lehman Date: Wed, 10 Jun 2020 11:36:10 -0400 Subject: [PATCH 7/8] Manage /etc/crypttab entries for encrypted volumes. --- library/blivet.py | 19 +++++++++++++++ tasks/main-blivet.yml | 13 ++++++++++ tests/test-verify-volume-encryption.yml | 32 +++++++++++++++++++++++++ tests/verify-role-results.yml | 6 +++++ 4 files changed, 70 insertions(+) diff --git a/library/blivet.py b/library/blivet.py index 6fd13457..bf9731f7 100644 --- a/library/blivet.py +++ b/library/blivet.py @@ -72,6 +72,9 @@ mounts: description: list of dicts describing mounts to set up type: list of dict +crypts: + description: list of dicts describing crypttab entries to set up + type: list of dict pools: description: list of dicts describing the pools w/ device path for each volume type: list of dict @@ -785,6 +788,20 @@ def handle_new_mount(volume, fstab): return mount_info +def get_crypt_info(actions): + info = list() + for action in actions: + if not (action.is_format and action.format.type == 'luks'): + continue + + info.append(dict(backing_device=action.device.path, + name=action.format.map_name, + password=action.format.key_file or '-', + state='present' if action.is_create else 'absent')) + + return sorted(info, key=lambda e: e['state']) + + def get_required_packages(b, pools, volumes): packages = list() for pool in pools: @@ -861,6 +878,7 @@ def run_module(): actions=list(), leaves=list(), mounts=list(), + crypts=list(), pools=list(), volumes=list(), packages=list(), @@ -956,6 +974,7 @@ def action_dict(action): activate_swaps(b, module.params['pools'], module.params['volumes']) result['mounts'] = get_mount_info(module.params['pools'], module.params['volumes'], actions, fstab) + result['crypts'] = get_crypt_info(actions) result['leaves'] = [d.path for d in b.devicetree.leaves] result['pools'] = module.params['pools'] result['volumes'] = module.params['volumes'] diff --git a/tasks/main-blivet.yml b/tasks/main-blivet.yml index 48e6aecb..0dc27ccd 100644 --- a/tasks/main-blivet.yml +++ b/tasks/main-blivet.yml @@ -165,6 +165,19 @@ daemon_reload: yes when: blivet_output['mounts'] +# +# Manage /etc/crypttab +# +- name: Manage /etc/crypttab to account for changes we just made + crypttab: + name: "{{ entry.name }}" + backing_device: "{{ entry.backing_device }}" + password: "{{ entry.password }}" + state: "{{ entry.state }}" + loop: "{{ blivet_output.crypts }}" + loop_control: + loop_var: entry + # # Update facts since we may have changed system state. # diff --git a/tests/test-verify-volume-encryption.yml b/tests/test-verify-volume-encryption.yml index 5418ab57..60191638 100644 --- a/tests/test-verify-volume-encryption.yml +++ b/tests/test-verify-volume-encryption.yml @@ -33,3 +33,35 @@ that: "{{ storage_test_blkinfo.info[storage_test_volume._device].type == 'crypt' }}" when: _storage_test_volume_present and storage_test_volume.encryption +- set_fact: + _storage_test_expected_crypttab_entries: "{{ (storage_test_volume.encryption and _storage_test_volume_present)|ternary(1, 0) }}" + _storage_test_crypttab_entries: "{{ storage_test_crypttab.stdout_lines|map('regex_search', '^' + storage_test_volume._device|basename + ' .*$')|select('string')|list }}" + _storage_test_expected_crypttab_key_file: "{{ storage_test_volume.encryption_key_file or '-' }}" + +- name: Check for /etc/crypttab entry + assert: + that: "{{ _storage_test_crypttab_entries|length == _storage_test_expected_crypttab_entries|int }}" + msg: "Incorrect number of crypttab entries found for volume {{ storage_test_volume.name }}" + +- name: Validate the format of the crypttab entry + assert: + that: "{{ _storage_test_crypttab_entries[0].split()|length >= 3 }}" + msg: "Incorrectly formatted crypttab line for volume {{ storage_test_volume.name }}" + when: _storage_test_expected_crypttab_entries|int == 1 + +- name: Check backing device of crypttab entry + assert: + that: "{{ _storage_test_crypttab_entries[0].split()[1] == storage_test_volume._raw_device }}" + msg: "Incorrect backing device in crypttab entry for volume {{ storage_test_volume.name }}" + when: _storage_test_expected_crypttab_entries|int == 1 + +- name: Check key file of crypttab entry + assert: + that: "{{ _storage_test_crypttab_entries[0].split()[2] == _storage_test_expected_crypttab_key_file }}" + msg: "Incorrect key file in crypttab entry for volume {{ storage_test_volume.name }}" + when: _storage_test_expected_crypttab_entries|int == 1 + +- set_fact: + _storage_test_expected_crypttab_entries: null + _storage_test_crypttab_entries: null + _storage_test_expected_crypttab_key_file: null diff --git a/tests/verify-role-results.yml b/tests/verify-role-results.yml index ed85687d..06f96f4a 100644 --- a/tests/verify-role-results.yml +++ b/tests/verify-role-results.yml @@ -21,6 +21,11 @@ register: storage_test_fstab changed_when: false +- name: Read the /etc/crypttab file + command: cat /etc/crypttab + register: storage_test_crypttab + changed_when: false + # # Verify pools and the volumes they contain. # @@ -51,5 +56,6 @@ - name: Clean up variable namespace set_fact: storage_test_fstab: null + storage_test_crypttab: null storage_test_blkinfo: null storage_test_volume: null From 178c4d6ba7c76ab85272c3ac0493d8e415f0d1fb Mon Sep 17 00:00:00 2001 From: David Lehman Date: Wed, 10 Jun 2020 16:07:04 -0400 Subject: [PATCH 8/8] Call out jmespath requirement for molecule. --- molecule_extra_requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/molecule_extra_requirements.txt b/molecule_extra_requirements.txt index 5ff4857e..279202c6 100644 --- a/molecule_extra_requirements.txt +++ b/molecule_extra_requirements.txt @@ -1,3 +1,4 @@ # SPDX-License-Identifier: MIT # Write extra requirements for running molecule here: +jmespath