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