1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2017, Stéphane Travassac <stravassac@gmail.com>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11DOCUMENTATION = r'''
12---
13module: vmware_guest_file_operation
14short_description: Files operation in a VMware guest operating system without network
15description:
16    - Module to copy a file to a VM, fetch a file from a VM and create or delete a directory in the guest OS.
17author:
18  - Stéphane Travassac (@stravassac)
19notes:
20    - Tested on vSphere 6
21    - Only the first match against vm_id is used, even if there are multiple matches
22requirements:
23    - "python >= 2.6"
24    - PyVmomi
25    - requests
26options:
27    datacenter:
28        description:
29            - The datacenter hosting the virtual machine.
30            - If set, it will help to speed up virtual machine search.
31        type: str
32    cluster:
33        description:
34            - The cluster hosting the virtual machine.
35            - If set, it will help to speed up virtual machine search.
36        type: str
37    folder:
38        description:
39            - Destination folder, absolute path to find an existing guest or create the new guest.
40            - The folder should include the datacenter. ESX's datacenter is ha-datacenter
41            - Used only if C(vm_id_type) is C(inventory_path).
42            - 'Examples:'
43            - '   folder: /ha-datacenter/vm'
44            - '   folder: ha-datacenter/vm'
45            - '   folder: /datacenter1/vm'
46            - '   folder: datacenter1/vm'
47            - '   folder: /datacenter1/vm/folder1'
48            - '   folder: datacenter1/vm/folder1'
49            - '   folder: /folder1/datacenter1/vm'
50            - '   folder: folder1/datacenter1/vm'
51            - '   folder: /folder1/datacenter1/vm/folder2'
52            - '   folder: vm/folder2'
53            - '   folder: folder2'
54        type: str
55    vm_id:
56        description:
57            - Name of the virtual machine to work with.
58        required: True
59        type: str
60    vm_id_type:
61        description:
62            - The VMware identification method by which the virtual machine will be identified.
63        default: vm_name
64        choices:
65            - 'uuid'
66            - 'instance_uuid'
67            - 'dns_name'
68            - 'inventory_path'
69            - 'vm_name'
70        type: str
71    vm_username:
72        description:
73            - The user to login in to the virtual machine.
74        required: True
75        type: str
76    vm_password:
77        description:
78            - The password used to login-in to the virtual machine.
79        required: True
80        type: str
81    directory:
82        description:
83            - Create or delete a directory.
84            - Can be used to create temp directory inside guest using mktemp operation.
85            - mktemp sets variable C(dir) in the result with the name of the new directory.
86            - mktemp operation option is added in version 2.8.
87        suboptions:
88            operation:
89                description:
90                - Operation to perform.
91                type: str
92                required: True
93                choices: [ 'create', 'delete', 'mktemp' ]
94            path:
95                type: str
96                description:
97                - Directory path.
98                - Required for C(create) or C(remove).
99            prefix:
100                description:
101                - Temporary directory prefix.
102                - Required for C(mktemp).
103                type: str
104            suffix:
105                type: str
106                description:
107                - Temporary directory suffix.
108                - Required for C(mktemp).
109            recurse:
110                type: bool
111                description:
112                - Not required.
113                default: False
114        required: False
115        type: dict
116    copy:
117        description:
118            - Copy file to vm without requiring network.
119        suboptions:
120            src:
121                description:
122                - File source absolute or relative.
123                required: True
124                type: str
125            dest:
126                description:
127                - File destination, path must be exist.
128                required: True
129                type: str
130            overwrite:
131                description:
132                - Overwrite or not.
133                type: bool
134                default: False
135        required: False
136        type: dict
137    fetch:
138        description:
139            - Get file from virtual machine without requiring network.
140        suboptions:
141            src:
142                description:
143                - The file on the remote system to fetch.
144                - This I(must) be a file, not a directory.
145                required: True
146                type: str
147            dest:
148                description:
149                - File destination on localhost, path must be exist.
150                required: True
151                type: str
152        required: False
153        type: dict
154
155extends_documentation_fragment:
156- community.vmware.vmware.documentation
157
158'''
159
160EXAMPLES = r'''
161- name: Create directory inside a vm
162  community.vmware.vmware_guest_file_operation:
163    hostname: "{{ vcenter_hostname }}"
164    username: "{{ vcenter_username }}"
165    password: "{{ vcenter_password }}"
166    datacenter: "{{ datacenter_name }}"
167    vm_id: "{{ guest_name }}"
168    vm_username: "{{ guest_username }}"
169    vm_password: "{{ guest_userpassword }}"
170    directory:
171      path: "/test"
172      operation: create
173      recurse: no
174  delegate_to: localhost
175
176- name: copy file to vm
177  community.vmware.vmware_guest_file_operation:
178    hostname: "{{ vcenter_hostname }}"
179    username: "{{ vcenter_username }}"
180    password: "{{ vcenter_password }}"
181    datacenter: "{{ datacenter_name }}"
182    vm_id: "{{ guest_name }}"
183    vm_username: "{{ guest_username }}"
184    vm_password: "{{ guest_userpassword }}"
185    copy:
186        src: "files/test.zip"
187        dest: "/root/test.zip"
188        overwrite: False
189  delegate_to: localhost
190
191- name: fetch file from vm
192  community.vmware.vmware_guest_file_operation:
193    hostname: "{{ vcenter_hostname }}"
194    username: "{{ vcenter_username }}"
195    password: "{{ vcenter_password }}"
196    datacenter: "{{ datacenter_name }}"
197    vm_id: "{{ guest_name }}"
198    vm_username: "{{ guest_username }}"
199    vm_password: "{{ guest_userpassword }}"
200    fetch:
201        src: "/root/test.zip"
202        dest: "files/test.zip"
203  delegate_to: localhost
204'''
205
206RETURN = r'''
207'''
208
209try:
210    from pyVmomi import vim, vmodl
211except ImportError:
212    pass
213
214import os
215from ansible.module_utils.basic import AnsibleModule
216from ansible.module_utils import urls
217from ansible.module_utils._text import to_bytes, to_native
218from ansible_collections.community.vmware.plugins.module_utils.vmware import (
219    PyVmomi, find_cluster_by_name, find_datacenter_by_name,
220    find_vm_by_id, vmware_argument_spec)
221
222
223class VmwareGuestFileManager(PyVmomi):
224    def __init__(self, module):
225        super(VmwareGuestFileManager, self).__init__(module)
226        datacenter_name = module.params['datacenter']
227        cluster_name = module.params['cluster']
228        folder = module.params['folder']
229
230        datacenter = None
231        if datacenter_name:
232            datacenter = find_datacenter_by_name(self.content, datacenter_name)
233            if not datacenter:
234                module.fail_json(msg="Unable to find %(datacenter)s datacenter" % module.params)
235
236        cluster = None
237        if cluster_name:
238            cluster = find_cluster_by_name(self.content, cluster_name, datacenter)
239            if not cluster:
240                module.fail_json(msg="Unable to find %(cluster)s cluster" % module.params)
241
242        if module.params['vm_id_type'] == 'inventory_path':
243            vm = find_vm_by_id(self.content, vm_id=module.params['vm_id'], vm_id_type="inventory_path", folder=folder)
244        else:
245            vm = find_vm_by_id(self.content,
246                               vm_id=module.params['vm_id'],
247                               vm_id_type=module.params['vm_id_type'],
248                               datacenter=datacenter,
249                               cluster=cluster)
250
251        if not vm:
252            module.fail_json(msg='Unable to find virtual machine.')
253
254        self.vm = vm
255        try:
256            result = dict(changed=False)
257            if module.params['directory']:
258                result = self.directory()
259            if module.params['copy']:
260                result = self.copy()
261            if module.params['fetch']:
262                result = self.fetch()
263            module.exit_json(**result)
264        except vmodl.RuntimeFault as runtime_fault:
265            module.fail_json(msg=to_native(runtime_fault.msg))
266        except vmodl.MethodFault as method_fault:
267            module.fail_json(msg=to_native(method_fault.msg))
268        except Exception as e:
269            module.fail_json(msg=to_native(e))
270
271    def directory(self):
272        result = dict(changed=True, uuid=self.vm.summary.config.uuid)
273        vm_username = self.module.params['vm_username']
274        vm_password = self.module.params['vm_password']
275
276        recurse = bool(self.module.params['directory']['recurse'])
277        operation = self.module.params['directory']['operation']
278        path = self.module.params['directory']['path']
279        prefix = self.module.params['directory']['prefix']
280        suffix = self.module.params['directory']['suffix']
281        creds = vim.vm.guest.NamePasswordAuthentication(username=vm_username, password=vm_password)
282        file_manager = self.content.guestOperationsManager.fileManager
283        if operation in ("create", "mktemp"):
284            try:
285                if operation == "create":
286                    file_manager.MakeDirectoryInGuest(vm=self.vm,
287                                                      auth=creds,
288                                                      directoryPath=path,
289                                                      createParentDirectories=recurse)
290                else:
291                    newdir = file_manager.CreateTemporaryDirectoryInGuest(vm=self.vm, auth=creds,
292                                                                          prefix=prefix, suffix=suffix)
293                    result['dir'] = newdir
294            except vim.fault.FileAlreadyExists as file_already_exists:
295                result['changed'] = False
296                result['msg'] = "Guest directory %s already exist: %s" % (path,
297                                                                          to_native(file_already_exists.msg))
298            except vim.fault.GuestPermissionDenied as permission_denied:
299                self.module.fail_json(msg="Permission denied for path %s : %s" % (path,
300                                                                                  to_native(permission_denied.msg)),
301                                      uuid=self.vm.summary.config.uuid)
302            except vim.fault.InvalidGuestLogin as invalid_guest_login:
303                self.module.fail_json(msg="Invalid guest login for user %s : %s" % (vm_username,
304                                                                                    to_native(invalid_guest_login.msg)),
305                                      uuid=self.vm.summary.config.uuid)
306            # other exceptions
307            except Exception as e:
308                self.module.fail_json(msg="Failed to Create directory into VM VMware exception : %s" % to_native(e),
309                                      uuid=self.vm.summary.config.uuid)
310
311        if operation == "delete":
312            try:
313                file_manager.DeleteDirectoryInGuest(vm=self.vm, auth=creds, directoryPath=path,
314                                                    recursive=recurse)
315            except vim.fault.FileNotFound as file_not_found:
316                result['changed'] = False
317                result['msg'] = "Guest directory %s not exists %s" % (path,
318                                                                      to_native(file_not_found.msg))
319            except vim.fault.FileFault as e:
320                self.module.fail_json(msg="FileFault : %s" % e.msg,
321                                      uuid=self.vm.summary.config.uuid)
322            except vim.fault.GuestPermissionDenied as permission_denied:
323                self.module.fail_json(msg="Permission denied for path %s : %s" % (path,
324                                                                                  to_native(permission_denied.msg)),
325                                      uuid=self.vm.summary.config.uuid)
326            except vim.fault.InvalidGuestLogin as invalid_guest_login:
327                self.module.fail_json(msg="Invalid guest login for user %s : %s" % (vm_username,
328                                                                                    to_native(invalid_guest_login.msg)),
329                                      uuid=self.vm.summary.config.uuid)
330            # other exceptions
331            except Exception as e:
332                self.module.fail_json(msg="Failed to Delete directory into Vm VMware exception : %s" % to_native(e),
333                                      uuid=self.vm.summary.config.uuid)
334
335        return result
336
337    def fetch(self):
338        result = dict(changed=True, uuid=self.vm.summary.config.uuid)
339        vm_username = self.module.params['vm_username']
340        vm_password = self.module.params['vm_password']
341        hostname = self.module.params['hostname']
342        dest = self.module.params["fetch"]['dest']
343        src = self.module.params['fetch']['src']
344        creds = vim.vm.guest.NamePasswordAuthentication(username=vm_username, password=vm_password)
345        file_manager = self.content.guestOperationsManager.fileManager
346
347        try:
348            fileTransferInfo = file_manager.InitiateFileTransferFromGuest(vm=self.vm, auth=creds,
349                                                                          guestFilePath=src)
350            url = fileTransferInfo.url
351            url = url.replace("*", hostname)
352            resp, info = urls.fetch_url(self.module, url, method="GET")
353            if info.get('status') != 200 or not resp:
354                self.module.fail_json(msg="Failed to fetch file : %s" % info.get('msg', ''), body=info.get('body', ''))
355            try:
356                with open(dest, "wb") as local_file:
357                    local_file.write(resp.read())
358            except Exception as e:
359                self.module.fail_json(msg="local file write exception : %s" % to_native(e),
360                                      uuid=self.vm.summary.config.uuid)
361        except vim.fault.FileNotFound as file_not_found:
362            self.module.fail_json(msg="Guest file %s does not exist : %s" % (src, to_native(file_not_found.msg)),
363                                  uuid=self.vm.summary.config.uuid)
364        except vim.fault.FileFault as e:
365            self.module.fail_json(msg="FileFault : %s" % to_native(e.msg),
366                                  uuid=self.vm.summary.config.uuid)
367        except vim.fault.GuestPermissionDenied:
368            self.module.fail_json(msg="Permission denied to fetch file %s" % src,
369                                  uuid=self.vm.summary.config.uuid)
370        except vim.fault.InvalidGuestLogin:
371            self.module.fail_json(msg="Invalid guest login for user %s" % vm_username,
372                                  uuid=self.vm.summary.config.uuid)
373        # other exceptions
374        except Exception as e:
375            self.module.fail_json(msg="Failed to Fetch file from Vm VMware exception : %s" % to_native(e),
376                                  uuid=self.vm.summary.config.uuid)
377
378        return result
379
380    def copy(self):
381        result = dict(changed=True, uuid=self.vm.summary.config.uuid)
382        vm_username = self.module.params['vm_username']
383        vm_password = self.module.params['vm_password']
384        hostname = self.module.params['hostname']
385        overwrite = self.module.params["copy"]["overwrite"]
386        dest = self.module.params["copy"]['dest']
387        src = self.module.params['copy']['src']
388        b_src = to_bytes(src, errors='surrogate_or_strict')
389
390        if not os.path.exists(b_src):
391            self.module.fail_json(msg="Source %s not found" % src)
392        if not os.access(b_src, os.R_OK):
393            self.module.fail_json(msg="Source %s not readable" % src)
394        if os.path.isdir(b_src):
395            self.module.fail_json(msg="copy does not support copy of directory: %s" % src)
396
397        data = None
398        with open(b_src, "rb") as local_file:
399            data = local_file.read()
400        file_size = os.path.getsize(b_src)
401
402        creds = vim.vm.guest.NamePasswordAuthentication(username=vm_username, password=vm_password)
403        file_attributes = vim.vm.guest.FileManager.FileAttributes()
404        file_manager = self.content.guestOperationsManager.fileManager
405        try:
406            url = file_manager.InitiateFileTransferToGuest(vm=self.vm, auth=creds, guestFilePath=dest,
407                                                           fileAttributes=file_attributes, overwrite=overwrite,
408                                                           fileSize=file_size)
409            url = url.replace("*", hostname)
410            resp, info = urls.fetch_url(self.module, url, data=data, method="PUT")
411
412            status_code = info["status"]
413            if status_code != 200:
414                self.module.fail_json(msg='problem during file transfer, http message:%s' % info,
415                                      uuid=self.vm.summary.config.uuid)
416        except vim.fault.FileAlreadyExists:
417            result['changed'] = False
418            result['msg'] = "Guest file %s already exists" % dest
419            return result
420        except vim.fault.FileFault as e:
421            self.module.fail_json(msg="FileFault:%s" % to_native(e.msg),
422                                  uuid=self.vm.summary.config.uuid)
423        except vim.fault.GuestPermissionDenied as permission_denied:
424            self.module.fail_json(msg="Permission denied to copy file into "
425                                      "destination %s : %s" % (dest, to_native(permission_denied.msg)),
426                                  uuid=self.vm.summary.config.uuid)
427        except vim.fault.InvalidGuestLogin as invalid_guest_login:
428            self.module.fail_json(msg="Invalid guest login for user"
429                                      " %s : %s" % (vm_username, to_native(invalid_guest_login.msg)))
430        # other exceptions
431        except Exception as e:
432            self.module.fail_json(msg="Failed to Copy file to Vm VMware exception : %s" % to_native(e),
433                                  uuid=self.vm.summary.config.uuid)
434        return result
435
436
437def main():
438    argument_spec = vmware_argument_spec()
439    argument_spec.update(dict(
440        datacenter=dict(type='str'),
441        cluster=dict(type='str'),
442        folder=dict(type='str'),
443        vm_id=dict(type='str', required=True),
444        vm_id_type=dict(
445            default='vm_name',
446            type='str',
447            choices=['inventory_path', 'uuid', 'instance_uuid', 'dns_name', 'vm_name']),
448        vm_username=dict(type='str', required=True),
449        vm_password=dict(type='str', no_log=True, required=True),
450        directory=dict(
451            type='dict',
452            default=None,
453            options=dict(
454                operation=dict(required=True, type='str', choices=['create', 'delete', 'mktemp']),
455                path=dict(required=False, type='str'),
456                prefix=dict(required=False, type='str'),
457                suffix=dict(required=False, type='str'),
458                recurse=dict(required=False, type='bool', default=False)
459            )
460        ),
461        copy=dict(
462            type='dict',
463            default=None,
464            options=dict(
465                src=dict(required=True, type='str'),
466                dest=dict(required=True, type='str'),
467                overwrite=dict(required=False, type='bool', default=False)
468            )
469        ),
470        fetch=dict(
471            type='dict',
472            default=None,
473            options=dict(
474                src=dict(required=True, type='str'),
475                dest=dict(required=True, type='str'),
476            )
477        )
478    )
479    )
480
481    module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False,
482                           required_if=[['vm_id_type', 'inventory_path', ['folder']]],
483                           mutually_exclusive=[['directory', 'copy', 'fetch']],
484                           required_one_of=[['directory', 'copy', 'fetch']],
485                           )
486
487    if module.params['directory']:
488        if module.params['directory']['operation'] in ('create', 'delete') and not module.params['directory']['path']:
489            module.fail_json(msg='directory.path is required when operation is "create" or "delete"')
490        if module.params['directory']['operation'] == 'mktemp' and not (module.params['directory']['prefix'] and module.params['directory']['suffix']):
491            module.fail_json(msg='directory.prefix and directory.suffix are required when operation is "mktemp"')
492
493    if module.params['vm_id_type'] == 'inventory_path' and not module.params['folder']:
494        module.fail_json(msg='Folder is required parameter when vm_id_type is inventory_path')
495
496    VmwareGuestFileManager(module)
497
498
499if __name__ == '__main__':
500    main()
501