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