1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2020, Ansible Project 5# Copyright: (c) 2020, VMware, Inc. All Rights Reserved. 6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 7 8from __future__ import absolute_import, division, print_function 9__metaclass__ = type 10 11DOCUMENTATION = r''' 12--- 13module: iso_create 14short_description: Generate ISO file with specified files or folders 15description: 16 - This module is used to generate ISO file with specified path of files. 17author: 18 - Diane Wang (@Tomorrow9) <dianew@vmware.com> 19requirements: 20- "pycdlib" 21- "python >= 2.7" 22version_added: '0.2.0' 23 24options: 25 src_files: 26 description: 27 - This is a list of absolute paths of source files or folders which will be contained in the new generated ISO file. 28 - Will fail if specified file or folder in C(src_files) does not exist on local machine. 29 - 'Note: With all ISO9660 levels from 1 to 3, all file names are restricted to uppercase letters, numbers and 30 underscores (_). File names are limited to 31 characters, directory nesting is limited to 8 levels, and path 31 names are limited to 255 characters.' 32 type: list 33 required: yes 34 elements: path 35 dest_iso: 36 description: 37 - The absolute path with file name of the new generated ISO file on local machine. 38 - Will create intermediate folders when they does not exist. 39 type: path 40 required: yes 41 interchange_level: 42 description: 43 - The ISO9660 interchange level to use, it dictates the rules on the names of files. 44 - Levels and valid values C(1), C(2), C(3), C(4) are supported. 45 - The default value is level C(1), which is the most conservative, level C(3) is recommended. 46 - ISO9660 file names at interchange level C(1) cannot have more than 8 characters or 3 characters in the extension. 47 type: int 48 default: 1 49 choices: [1, 2, 3, 4] 50 vol_ident: 51 description: 52 - The volume identification string to use on the new generated ISO image. 53 type: str 54 rock_ridge: 55 description: 56 - Whether to make this ISO have the Rock Ridge extensions or not. 57 - Valid values are C(1.09), C(1.10) or C(1.12), means adding the specified Rock Ridge version to the ISO. 58 - If unsure, set C(1.09) to ensure maximum compatibility. 59 - If not specified, then not add Rock Ridge extension to the ISO. 60 type: str 61 choices: ['1.09', '1.10', '1.12'] 62 joliet: 63 description: 64 - Support levels and valid values are C(1), C(2), or C(3). 65 - Level C(3) is by far the most common. 66 - If not specified, then no Joliet support is added. 67 type: int 68 choices: [1, 2, 3] 69 udf: 70 description: 71 - Whether to add UDF support to this ISO. 72 - If set to C(True), then version 2.60 of the UDF spec is used. 73 - If not specified or set to C(False), then no UDF support is added. 74 type: bool 75 default: False 76''' 77 78EXAMPLES = r''' 79- name: Create an ISO file 80 community.general.iso_create: 81 src_files: 82 - /root/testfile.yml 83 - /root/testfolder 84 dest_iso: /tmp/test.iso 85 interchange_level: 3 86 87- name: Create an ISO file with Rock Ridge extension 88 community.general.iso_create: 89 src_files: 90 - /root/testfile.yml 91 - /root/testfolder 92 dest_iso: /tmp/test.iso 93 rock_ridge: 1.09 94 95- name: Create an ISO file with Joliet support 96 community.general.iso_create: 97 src_files: 98 - ./windows_config/Autounattend.xml 99 dest_iso: ./test.iso 100 interchange_level: 3 101 joliet: 3 102 vol_ident: WIN_AUTOINSTALL 103''' 104 105RETURN = r''' 106source_file: 107 description: Configured source files or directories list. 108 returned: on success 109 type: list 110 elements: path 111 sample: ["/path/to/file.txt", "/path/to/folder"] 112created_iso: 113 description: Created iso file path. 114 returned: on success 115 type: str 116 sample: "/path/to/test.iso" 117interchange_level: 118 description: Configured interchange level. 119 returned: on success 120 type: int 121 sample: 3 122vol_ident: 123 description: Configured volume identification string. 124 returned: on success 125 type: str 126 sample: "OEMDRV" 127joliet: 128 description: Configured Joliet support level. 129 returned: on success 130 type: int 131 sample: 3 132rock_ridge: 133 description: Configured Rock Ridge version. 134 returned: on success 135 type: str 136 sample: "1.09" 137udf: 138 description: Configured UDF support. 139 returned: on success 140 type: bool 141 sample: False 142''' 143 144import os 145import traceback 146 147PYCDLIB_IMP_ERR = None 148try: 149 import pycdlib 150 HAS_PYCDLIB = True 151except ImportError: 152 PYCDLIB_IMP_ERR = traceback.format_exc() 153 HAS_PYCDLIB = False 154 155from ansible.module_utils.basic import AnsibleModule, missing_required_lib 156from ansible.module_utils.common.text.converters import to_native 157 158 159def add_file(module, iso_file=None, src_file=None, file_path=None, rock_ridge=None, use_joliet=None, use_udf=None): 160 rr_name = None 161 joliet_path = None 162 udf_path = None 163 # In standard ISO interchange level 1, file names have a maximum of 8 characters, followed by a required dot, 164 # followed by a maximum 3 character extension, followed by a semicolon and a version 165 file_name = os.path.basename(file_path) 166 if '.' not in file_name: 167 file_in_iso_path = file_path.upper() + '.;1' 168 else: 169 file_in_iso_path = file_path.upper() + ';1' 170 if rock_ridge: 171 rr_name = file_name 172 if use_joliet: 173 joliet_path = file_path 174 if use_udf: 175 udf_path = file_path 176 try: 177 iso_file.add_file(src_file, iso_path=file_in_iso_path, rr_name=rr_name, joliet_path=joliet_path, udf_path=udf_path) 178 except Exception as err: 179 module.fail_json(msg="Failed to add file %s to ISO file due to %s" % (src_file, to_native(err))) 180 181 182def add_directory(module, iso_file=None, dir_path=None, rock_ridge=None, use_joliet=None, use_udf=None): 183 rr_name = None 184 joliet_path = None 185 udf_path = None 186 iso_dir_path = dir_path.upper() 187 if rock_ridge: 188 rr_name = os.path.basename(dir_path) 189 if use_joliet: 190 joliet_path = iso_dir_path 191 if use_udf: 192 udf_path = iso_dir_path 193 try: 194 iso_file.add_directory(iso_path=iso_dir_path, rr_name=rr_name, joliet_path=joliet_path, udf_path=udf_path) 195 except Exception as err: 196 module.fail_json(msg="Failed to directory %s to ISO file due to %s" % (dir_path, to_native(err))) 197 198 199def main(): 200 argument_spec = dict( 201 src_files=dict(type='list', required=True, elements='path'), 202 dest_iso=dict(type='path', required=True), 203 interchange_level=dict(type='int', choices=[1, 2, 3, 4], default=1), 204 vol_ident=dict(type='str'), 205 rock_ridge=dict(type='str', choices=['1.09', '1.10', '1.12']), 206 joliet=dict(type='int', choices=[1, 2, 3]), 207 udf=dict(type='bool', default=False), 208 ) 209 module = AnsibleModule( 210 argument_spec=argument_spec, 211 supports_check_mode=True, 212 ) 213 if not HAS_PYCDLIB: 214 module.fail_json(missing_required_lib('pycdlib'), exception=PYCDLIB_IMP_ERR) 215 216 src_file_list = module.params.get('src_files') 217 if src_file_list and len(src_file_list) == 0: 218 module.fail_json(msg='Please specify source file and/or directory list using src_files parameter.') 219 for src_file in src_file_list: 220 if not os.path.exists(src_file): 221 module.fail_json(msg="Specified source file/directory path does not exist on local machine, %s" % src_file) 222 223 dest_iso = module.params.get('dest_iso') 224 if dest_iso and len(dest_iso) == 0: 225 module.fail_json(msg='Please specify the absolute path of the new created ISO file using dest_iso parameter.') 226 227 dest_iso_dir = os.path.dirname(dest_iso) 228 if dest_iso_dir and not os.path.exists(dest_iso_dir): 229 # will create intermediate dir for new ISO file 230 try: 231 os.makedirs(dest_iso_dir) 232 except OSError as err: 233 module.fail_json(msg='Exception caught when creating folder %s, with error %s' % (dest_iso_dir, to_native(err))) 234 235 volume_id = module.params.get('vol_ident') 236 if volume_id is None: 237 volume_id = '' 238 inter_level = module.params.get('interchange_level') 239 rock_ridge = module.params.get('rock_ridge') 240 use_joliet = module.params.get('joliet') 241 use_udf = None 242 if module.params['udf']: 243 use_udf = '2.60' 244 245 result = dict( 246 changed=False, 247 source_file=src_file_list, 248 created_iso=dest_iso, 249 interchange_level=inter_level, 250 vol_ident=volume_id, 251 rock_ridge=rock_ridge, 252 joliet=use_joliet, 253 udf=use_udf 254 ) 255 if not module.check_mode: 256 iso_file = pycdlib.PyCdlib() 257 iso_file.new(interchange_level=inter_level, vol_ident=volume_id, rock_ridge=rock_ridge, joliet=use_joliet, udf=use_udf) 258 259 for src_file in src_file_list: 260 # if specify a dir then go through the dir to add files and dirs 261 if os.path.isdir(src_file): 262 dir_list = [] 263 file_list = [] 264 src_file = src_file.rstrip('/') 265 dir_name = os.path.basename(src_file) 266 add_directory(module, iso_file=iso_file, dir_path='/' + dir_name, rock_ridge=rock_ridge, 267 use_joliet=use_joliet, use_udf=use_udf) 268 269 # get dir list and file list 270 for path, dirs, files in os.walk(src_file): 271 for filename in files: 272 file_list.append(os.path.join(path, filename)) 273 for dir in dirs: 274 dir_list.append(os.path.join(path, dir)) 275 for new_dir in dir_list: 276 add_directory(module, iso_file=iso_file, dir_path=new_dir.split(os.path.dirname(src_file))[1], 277 rock_ridge=rock_ridge, use_joliet=use_joliet, use_udf=use_udf) 278 for new_file in file_list: 279 add_file(module, iso_file=iso_file, src_file=new_file, 280 file_path=new_file.split(os.path.dirname(src_file))[1], rock_ridge=rock_ridge, 281 use_joliet=use_joliet, use_udf=use_udf) 282 # if specify a file then add this file directly to the '/' path in ISO 283 else: 284 add_file(module, iso_file=iso_file, src_file=src_file, file_path='/' + os.path.basename(src_file), 285 rock_ridge=rock_ridge, use_joliet=use_joliet, use_udf=use_udf) 286 287 iso_file.write(dest_iso) 288 iso_file.close() 289 290 result['changed'] = True 291 module.exit_json(**result) 292 293 294if __name__ == '__main__': 295 main() 296