diff --git a/plugins/modules/uki.py b/plugins/modules/uki.py new file mode 100644 index 0000000..823d146 --- /dev/null +++ b/plugins/modules/uki.py @@ -0,0 +1,182 @@ +#!/usr/bin/python + +# Copyright: (c) 2024, Christopher Palmer-Richez (tofu.ansible@chorky.net) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: uki + +short_description: Configure unified kernel images + +version_added: "1.0.0" + +description: Configure kernel-install to make unified kernel images. + +options: + signing_key: + description: | + The path to a RSA private key to use for UKI signing. + Specifying this option enables UKI signing for secure boot. + The `signing_cert` option must be provided as well. + type: path + + signing_cert: + description: | + The path to the x509 certificate used for UKI singing. + Specifying this option enables UKI signing for secure boot. + The `signing_key` option must be provided as well. + type: path + + initrd_generator: + description: | + The tool to use to make the UKI's initramfs section. + You must use a tool supported by kernel-install. If an unknown or + unsupported value is provided, the UKI build will fail. + type: str + default: dracut + +# Specify this value according to your collection +# in format of namespace.collection.doc_fragment_name +# extends_documentation_fragment: +# - my_namespace.my_collection.my_doc_fragment_name + +author: + - Christopher Palmer-Richez @crichez +''' + +EXAMPLES = r''' +# Build and boot from a UKI without secure boot support +- name: Use UKIs + crichez.secureboot.uki: + +# Build and boot from a signed UKI using a custom MOK +- name: Use signed UKIs + crichez.secureboot.uki: + signing_key: /etc/kernel/MOK.priv + signing_cert: /etc/kernel/MOK.pem + +# Build and boot from a signed UKI using a custom initrd generator +- name: Use signed UKIs with mkinitcpio + crichez.secureboot.uki: + signing_key: /etc/kernel/DB.priv + signing_cert: /etc/kernel/DB.pem + initrd_generator: mkinitcpio +''' + +RETURN = r''' +uki_path: + description: The path to the generated UKI. + type: path + returned: success + sample: /boot/efi/EFI/Linux/6f51ea06-4933-4666-937e-f83391673562-6.9.9-f40-x86_64.efi +kernel_install_output: + description: The output of kernel-install + type: str + returned: always + sample: '' +''' + +from ansible.module_utils.basic import AnsibleModule +import re + +def run_module(): + # define available arguments/parameters a user can pass to the module + module_args = dict( + signing_key=dict(type='path', required=False), + signing_cert=dict(type='path', required=False), + initrd_generator=dict(type='str', required=False, default='dracut') + ) + + # seed the result dict in the object + # we primarily care about changed and state + # changed is if this module effectively modified the target + # state will include any data that you want your module to pass back + # for consumption, for example, in a subsequent task + result = dict( + changed=False, + uki_path='', + kernel_install_output='' + ) + + # the AnsibleModule object will be our abstraction working with Ansible + # this includes instantiation, a couple of common attr would be the + # args/params passed to the execution, as well as if the module + # supports check mode + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=False + ) + + + + # if the user is working with this module in only check mode we do not + # want to make any changes to the environment, just return the current + # state with no modifications + if module.check_mode: + module.exit_json(**result) + + # manipulate or modify the state as needed (this is going to be the + # part where your module will do what it needs to do) + change_list = dict( + install_conf_path=[], + ukify_conf_path=[]) + + # Set the install.conf file to the desired state + install_conf_path = '/etc/kernel/install.conf' + install_conf = None + try: + with open(install_conf_path, mode='r', encoding='utf-8') as f: + install_conf = f.read() + except FileNotFoundError: + install_conf = '' + + # Set install.conf layout + layout_re = re.compile(r'^layout=([a-z]*)$') + layout_match = layout_re.search(install_conf) + if layout_match: + layout = layout_match.group(1) + if layout != 'uki': + install_conf = layout_re.sub('layout=uki', install_conf) + change = f'changed layout from {layout} to uki' + change_list[install_conf_path].append(change) + else: + layout_line = 'layout=uki\n' + if install_conf != '' and install_conf[-1] != '\n': + layout_line = '\n'.join(layout_line) + install_conf = install_conf.join(layout_line) + change = 'change layout from default to uki' + change_list[install_conf_path].append(change) + + # Set install.conf initrd_generator + initrd_re = re.compile(r'^initrd_generator=([a-z]*)$') + initrd_match = initrd_re.search(install_conf) + if initrd_match: + initrd_generator = initrd_match.group(1) + new_initrd_gen = module.params['initrd_generator'] + if initrd_generator != new_initrd_gen: + replacement = f"initrd_generator={new_initrd_gen}" + install_conf = initrd_re.sub(replacement, install_conf) + + # use whatever logic you need to determine whether or not this module + # made any modifications to your target + + # during the execution of the module, if there is an exception or a + # conditional state that effectively causes a failure, run + # AnsibleModule.fail_json() to pass in the message and the result + if module.params['name'] == 'fail me': + module.fail_json(msg='You requested this to fail', **result) + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main()