diff --git a/plugins/modules/files/iso_create.py b/plugins/modules/files/iso_create.py new file mode 100644 index 0000000000..bac2f8e4e1 --- /dev/null +++ b/plugins/modules/files/iso_create.py @@ -0,0 +1,275 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Ansible Project +# Copyright: (c) 2020, VMware, Inc. All Rights Reserved. +# 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 = ''' +--- +module: iso_create +short_description: Generate ISO file with specified files or folders +description: + - This module is used to generate ISO file with specified path of files. +author: + - Diane Wang(@Tomorrow9) +requirements: ["pycdlib", "python >= 2.7"] + +options: + src_files: + description: + - This is a list of absolute paths of source files or folders which will be contained in the new generated ISO file. + - Will fail if specified file or folder in C(src_files) does not exist on local machine. + - 'Note: With all ISO9660 levels from 1 to 3, all file names are restricted to uppercase letters, numbers and + underscores (_). File names are limited to 31 characters, directory nesting is limited to 8 levels, and path + names are limited to 255 characters.' + type: list + required: yes + elements: path + dest_iso: + description: + - The absolute path with file name of the new generated ISO file on local machine. + - Will create intermediate folders when they does not exist. + type: path + required: yes + interchange_level: + description: + - The ISO9660 interchange level to use, it dictates the rules on the names of files. + - Levels and valid values C(1), C(2), C(3), C(4) are supported. + - The default value is level C(1), which is the most conservative, level C(3) is recommended. + - ISO9660 file names at interchange level C(1) cannot have more than 8 characters or 3 characters in the extension. + type: int + default: 1 + choices: [1, 2, 3, 4] + vol_ident: + description: + - The volume identification string to use on the new generated ISO image. + type: str + rock_ridge: + description: + - Whether to make this ISO have the Rock Ridge extensions or not. + - 'Valid values are C(1.09), C(1.10) or C(1.12), means adding the specified Rock Ridge version to the ISO. If + unsure, set C(1.09) to ensure maximum compatibility.' + - If not specified, then not add Rock Ridge extension to the ISO. + type: str + choices: ['1.09', '1.10', '1.12'] + joliet: + description: + - Support levels and valid values are C(1), C(2), or C(3). Level C(3) is by far the most common. + - If not specified, then no Joliet support is added. + type: int + choices: [1, 2, 3] + udf: + description: + - Whether to add UDF support to this ISO. If set to C(True), then version 2.60 of the UDF spec is used. + - If not specified or set to C(False), then no UDF support is added. + type: bool + default: False +''' + +EXAMPLES = r''' +- name: Create an ISO file + iso_create: + src_files: + - /root/testfile.yml + - /root/testfolder + dest_iso: /tmp/test.iso + interchange_level: 3 + +- name: Create an ISO file with Rock Ridge extension + iso_create: + src_files: + - /root/testfile.yml + - /root/testfolder + dest_iso: /tmp/test.iso + rock_ridge: 1.09 +''' + +RETURN = r''' +source_file: + description: Configured source files or directories list. + returned: on success + type: list + elements: path + sample: ["/path/to/file.txt", "/path/to/folder"] +created_iso: + description: Created iso file path. + returned: on success + type: str + sample: "/path/to/test.iso" +interchange_level: + description: Configured interchange level. + returned: on success + type: int + sample: 3 +vol_ident: + description: Configured volume identification string. + returned: on success + type: str + sample: "OEMDRV" +joliet: + description: Configured Joliet support level. + returned: on success + type: int + sample: 3 +rock_ridge: + description: Configured Rock Ridge version. + returned: on success + type: str + sample: "1.09" +udf: + description: Configured UDF support. + returned: on success + type: bool + sample: False +''' + +import os +HAS_PYCDLIB = False +try: + import pycdlib + HAS_PYCDLIB = True +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + + +def add_file(module, iso_file=None, src_file=None, file_path=None, rock_ridge=None, use_joliet=None, use_udf=None): + rr_name = None + joliet_path = None + udf_path = None + # In standard ISO interchange level 1, file names have a maximum of 8 characters, followed by a required dot, + # followed by a maximum 3 character extension, followed by a semicolon and a version + file_name = os.path.basename(file_path) + if '.' not in file_name: + file_in_iso_path = file_path.upper() + '.;1' + else: + file_in_iso_path = file_path.upper() + ';1' + if rock_ridge: + rr_name = file_name + if use_joliet: + joliet_path = file_path + if use_udf: + udf_path = file_path + try: + iso_file.add_file(src_file, iso_path=file_in_iso_path, rr_name=rr_name, joliet_path=joliet_path, udf_path=udf_path) + except Exception as err: + module.fail_json(msg="Add file %s to ISO file failed due to %s" % (src_file, to_native(err))) + + +def add_directory(module, iso_file=None, dir_path=None, rock_ridge=None, use_joliet=None, use_udf=None): + rr_name = None + joliet_path = None + udf_path = None + iso_dir_path = dir_path.upper() + if rock_ridge: + rr_name = os.path.basename(dir_path) + if use_joliet: + joliet_path = iso_dir_path + if use_udf: + udf_path = iso_dir_path + try: + iso_file.add_directory(iso_path=iso_dir_path, rr_name=rr_name, joliet_path=joliet_path, udf_path=udf_path) + except Exception as err: + module.fail_json(msg="Add directory %s to ISO file failed due to %s" % (dir_path, to_native(err))) + + +def main(): + argument_spec = dict( + src_files=dict(type='list', required=True, elements='path'), + dest_iso=dict(type='path', required=True), + interchange_level=dict(type='int', choices=[1, 2, 3, 4], default=1), + vol_ident=dict(type='str'), + rock_ridge=dict(type='str', choices=['1.09', '1.10', '1.12']), + joliet=dict(type='int', choices=[1, 2, 3]), + udf=dict(type='bool', default=False), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + src_file_list = module.params.get('src_files') + if src_file_list and len(src_file_list) == 0: + module.fail_json(msg='Please specify source file and/or directory list using src_files parameter.') + for src_file in src_file_list: + if not os.path.exists(src_file): + module.fail_json(msg="Specified source file/directory path does not exist on local machine, %s" % src_file) + + dest_iso = module.params.get('dest_iso') + if dest_iso and len(dest_iso) == 0: + module.fail_json(msg='Please specify the absolute path of the new created ISO file using dest_iso parameter.') + + dest_iso_dir = os.path.dirname(dest_iso) + if dest_iso_dir and not os.path.exists(dest_iso_dir): + # will create intermediate dir for new ISO file + try: + os.makedirs(dest_iso_dir) + except OSError as err: + module.fail_json(msg='Exception caught when creating folder %s, with error %s' % (dest_iso_dir, to_native(err))) + + volume_id = module.params.get('vol_ident') + if volume_id is None: + volume_id = '' + inter_level = module.params.get('interchange_level') + rock_ridge = module.params.get('rock_ridge') + use_joliet = module.params.get('joliet') + use_udf = None + if module.params['udf']: + use_udf = '2.60' + + result = dict( + changed=False, + source_file=src_file_list, + created_iso=dest_iso, + interchange_level=inter_level, + vol_ident=volume_id, + rock_ridge=rock_ridge, + joliet=use_joliet, + udf=use_udf + ) + if not module.check_mode: + iso_file = pycdlib.PyCdlib() + iso_file.new(interchange_level=inter_level, vol_ident=volume_id, rock_ridge=rock_ridge, joliet=use_joliet, udf=use_udf) + + for src_file in src_file_list: + # if specify a dir then go through the dir to add files and dirs + if os.path.isdir(src_file): + dir_list = [] + file_list = [] + src_file = src_file.rstrip('/') + dir_name = os.path.basename(src_file) + add_directory(module, iso_file=iso_file, dir_path='/' + dir_name, rock_ridge=rock_ridge, + use_joliet=use_joliet, use_udf=use_udf) + + # get dir list and file list + for path, dirs, files in os.walk(src_file): + for filename in files: + file_list.append(os.path.join(path, filename)) + for dir in dirs: + dir_list.append(os.path.join(path, dir)) + for new_dir in dir_list: + add_directory(module, iso_file=iso_file, dir_path=new_dir.split(os.path.dirname(src_file))[1], + rock_ridge=rock_ridge, use_joliet=use_joliet, use_udf=use_udf) + for new_file in file_list: + add_file(module, iso_file=iso_file, src_file=new_file, + file_path=new_file.split(os.path.dirname(src_file))[1], rock_ridge=rock_ridge, + use_joliet=use_joliet, use_udf=use_udf) + # if specify a file then add this file directly to the '/' path in ISO + else: + add_file(module, iso_file=iso_file, src_file=src_file, file_path='/' + os.path.basename(src_file), + rock_ridge=rock_ridge, use_joliet=use_joliet, use_udf=use_udf) + + iso_file.write(dest_iso) + iso_file.close() + + result['changed'] = True + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/iso_create.py b/plugins/modules/iso_create.py new file mode 120000 index 0000000000..897a8d99be --- /dev/null +++ b/plugins/modules/iso_create.py @@ -0,0 +1 @@ +./files/iso_create.py \ No newline at end of file diff --git a/tests/integration/targets/iso_create/aliases b/tests/integration/targets/iso_create/aliases new file mode 100644 index 0000000000..9d694de923 --- /dev/null +++ b/tests/integration/targets/iso_create/aliases @@ -0,0 +1,4 @@ +shippable/posix/group1 +destructive +skip/aix +skip/python2.6 diff --git a/tests/integration/targets/iso_create/files/test1.cfg b/tests/integration/targets/iso_create/files/test1.cfg new file mode 100644 index 0000000000..1c6d0d0d86 --- /dev/null +++ b/tests/integration/targets/iso_create/files/test1.cfg @@ -0,0 +1,56 @@ +#version=DEVEL +# System authorization information +auth --enableshadow --passalgo=sha512 +# Use CDROM installation media +cdrom +# Use graphical install +graphical +# Run the Setup Agent on first boot +firstboot --enable +ignoredisk --only-use=sda +# Keyboard layouts +keyboard --vckeymap=us --xlayouts='us' +# System language +lang en_US.UTF-8 +# Network information +network --bootproto=dhcp --device=ens192 --ipv6=auto --no-activate +network --hostname=localhost.localdomain +# System services +services --enabled="chronyd" +# System timezone +timezone America/New_York --isUtc +# X Window System configuration information +xconfig --startxonboot +# System bootloader configuration +bootloader --append=" crashkernel=auto" --location=mbr --boot-drive=sda +autopart --type=lvm +# Partition clearing information +clearpart --none --initlabel +#firewall --disable +services --disabled=firewalld +eula --agreed +# Reboot when the install is finished. +reboot + +%packages +@^graphical-server-environment +@base +@core +@desktop-debugging +@dial-up +@fonts +@gnome-desktop +@guest-agents +@guest-desktop-agents +@hardware-monitoring +@input-methods +@internet-browser +@multimedia +@print-client +@x11 +chrony +kexec-tools +open-vm-tools-desktop +%end +%addon com_redhat_kdump --enable --reserve-mb='auto' +%end diff --git a/tests/integration/targets/iso_create/files/test_dir/test2.cfg b/tests/integration/targets/iso_create/files/test_dir/test2.cfg new file mode 100644 index 0000000000..1c6d0d0d86 --- /dev/null +++ b/tests/integration/targets/iso_create/files/test_dir/test2.cfg @@ -0,0 +1,56 @@ +#version=DEVEL +# System authorization information +auth --enableshadow --passalgo=sha512 +# Use CDROM installation media +cdrom +# Use graphical install +graphical +# Run the Setup Agent on first boot +firstboot --enable +ignoredisk --only-use=sda +# Keyboard layouts +keyboard --vckeymap=us --xlayouts='us' +# System language +lang en_US.UTF-8 +# Network information +network --bootproto=dhcp --device=ens192 --ipv6=auto --no-activate +network --hostname=localhost.localdomain +# System services +services --enabled="chronyd" +# System timezone +timezone America/New_York --isUtc +# X Window System configuration information +xconfig --startxonboot +# System bootloader configuration +bootloader --append=" crashkernel=auto" --location=mbr --boot-drive=sda +autopart --type=lvm +# Partition clearing information +clearpart --none --initlabel +#firewall --disable +services --disabled=firewalld +eula --agreed +# Reboot when the install is finished. +reboot + +%packages +@^graphical-server-environment +@base +@core +@desktop-debugging +@dial-up +@fonts +@gnome-desktop +@guest-agents +@guest-desktop-agents +@hardware-monitoring +@input-methods +@internet-browser +@multimedia +@print-client +@x11 +chrony +kexec-tools +open-vm-tools-desktop +%end +%addon com_redhat_kdump --enable --reserve-mb='auto' +%end diff --git a/tests/integration/targets/iso_create/meta/main.yml b/tests/integration/targets/iso_create/meta/main.yml new file mode 100644 index 0000000000..ca521ab1ef --- /dev/null +++ b/tests/integration/targets/iso_create/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_pkg_mgr + - prepare_tests diff --git a/tests/integration/targets/iso_create/tasks/main.yml b/tests/integration/targets/iso_create/tasks/main.yml new file mode 100644 index 0000000000..20e42fb4d3 --- /dev/null +++ b/tests/integration/targets/iso_create/tasks/main.yml @@ -0,0 +1,149 @@ +# Test code for iso_create module +# Copyright: (c) 2020, Diane Wang (Tomorrow9) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +- name: install pycdlib + pip: + name: pycdlib + # state: latest + register: install_pycdlib +- debug: var=install_pycdlib + +- set_fact: + output_dir_test: '{{ output_dir }}/test_iso_create' + +# - include_tasks: prepare_dest_dir.yml + +- name: Test check mode + iso_create: + src_files: + - "{{ role_path }}/files/test1.cfg" + dest_iso: "{{ output_dir_test }}/test.iso" + interchange_level: 3 + register: iso_result + check_mode: yes +- debug: var=iso_result + +- name: Check if iso file created + stat: + path: "{{ output_dir_test }}/test.iso" + register: iso_file +- debug: var=iso_file +- assert: + that: + - iso_result.changed == True + - iso_file.stat.exists == False + +- name: Create iso file with a specified file + iso_create: + src_files: + - "{{ role_path }}/files/test1.cfg" + dest_iso: "{{ output_dir_test }}/test.iso" + interchange_level: 3 + register: iso_result +- debug: var=iso_result + +- name: Check if iso file created + stat: + path: "{{ output_dir_test }}/test.iso" + register: iso_file + +- assert: + that: + - iso_result.changed == True + - iso_file.stat.exists == True + +- name: Create iso file with a specified file and folder + iso_create: + src_files: + - "{{ role_path }}/files/test1.cfg" + - "{{ role_path }}/files/test_dir" + dest_iso: "{{ output_dir_test }}/test1.iso" + interchange_level: 3 + register: iso_result +- debug: var=iso_result + +- name: Check if iso file created + stat: + path: "{{ output_dir_test }}/test1.iso" + register: iso_file + +- assert: + that: + - iso_result.changed == True + - iso_file.stat.exists == True + +- name: Create iso file with volume identification string + iso_create: + src_files: + - "{{ role_path }}/files/test1.cfg" + dest_iso: "{{ output_dir_test }}/test2.iso" + vol_ident: "OEMDRV" + register: iso_result +- debug: var=iso_result + +- name: Check if iso file created + stat: + path: "{{ output_dir_test }}/test2.iso" + register: iso_file + +- assert: + that: + - iso_result.changed == True + - iso_file.stat.exists == True + +- name: Create iso file with Rock Ridge extention + iso_create: + src_files: + - "{{ role_path }}/files/test1.cfg" + dest_iso: "{{ output_dir_test }}/test3.iso" + rock_ridge: "1.09" + register: iso_result +- debug: var=iso_result + +- name: Check if iso file created + stat: + path: "{{ output_dir_test }}/test3.iso" + register: iso_file + +- assert: + that: + - iso_result.changed == True + - iso_file.stat.exists == True + +- name: Create iso file with Joliet extention + iso_create: + src_files: + - "{{ role_path }}/files/test1.cfg" + dest_iso: "{{ output_dir_test }}/test4.iso" + joliet: 3 + register: iso_result +- debug: var=iso_result + +- name: Check if iso file created + stat: + path: "{{ output_dir_test }}/test4.iso" + register: iso_file + +- assert: + that: + - iso_result.changed == True + - iso_file.stat.exists == True + +- name: Create iso file with UDF enabled + iso_create: + src_files: + - "{{ role_path }}/files/test1.cfg" + dest_iso: "{{ output_dir_test }}/test5.iso" + udf: True + register: iso_result +- debug: var=iso_result + +- name: Check if iso file created + stat: + path: "{{ output_dir_test }}/test5.iso" + register: iso_file + +- assert: + that: + - iso_result.changed == True + - iso_file.stat.exists == True diff --git a/tests/integration/targets/iso_create/tasks/prepare_dest_dir.yml b/tests/integration/targets/iso_create/tasks/prepare_dest_dir.yml new file mode 100644 index 0000000000..94c529d52a --- /dev/null +++ b/tests/integration/targets/iso_create/tasks/prepare_dest_dir.yml @@ -0,0 +1,12 @@ +# Test code for iso_create module +# Copyright: (c) 2020, Diane Wang (Tomorrow9) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +- name: Make sure our testing sub-directory does not exist + file: + path: '{{ output_dir_test }}' + state: absent + +- name: Create our testing sub-directory + file: + path: '{{ output_dir_test }}' + state: directory