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