Repository: ansible/ansible

Issue:
vmware_guest option to move an existing vm to a different folder
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and devel branch are affected too.
Always add information AFTER of these html comments. -->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Feature Idea


##### COMPONENT NAME
<!--- Insert, BELOW THIS COMMENT, the name of the module, plugin, task or feature.
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path-->
vmware_guest

##### ANSIBLE VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "ansible --version" between quotes below -->
```
2.5
```

##### CONFIGURATION
<!--- If using Ansible 2.4 or above, paste, BELOW THIS COMMENT, the results of "ansible-config dump --only-changed"
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).-->

##### OS / ENVIRONMENT
<!--- Mention, BELOW THIS COMMENT, the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.-->

##### SUMMARY
<!--- Explain the problem briefly -->

i wasnt able to find a way to use the vmware_guest module to move a VM to a different folder  
that would be quite useful for lifecycle management ,and the feature is supported in pyvomi

##### STEPS TO REPRODUCE
<!--- For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used. -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```

```



Discussion:
Files identified in the description:
* [lib/ansible/modules/cloud/vmware/vmware_guest.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/vmware/vmware_guest.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @Akasurde @akasurde @dav1x @jctanner @nerzhul @pdellaert
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
-label needs_triage
@saargrin Thanks for suggesting this feature. Currently, vmware_guest is overburdened with lot of features, so I feel creating a new module will be the best approach here. Let me know if you have any suggestion or idea.
needs_info
Hi
Thanks for your reply

I actually think it would be beneficial to have this feature as part of the
general guest module since this operation doesn't seem to justify a
separate module
I can implement this in python myself, how would I go about contributing
this either as separate module or part of vmware_guest?


Thanks

On Thu, 26 Apr 2018, 09:54 Abhijeet Kasurde, <notifications@github.com>
wrote:

> @saargrin <https://github.com/saargrin> Thanks for suggesting this
> feature. Currently, vmware_guest is overburdened with lot of features, so I
> feel creating a new module will be the best approach here. Let me know if
> you have any suggestion or idea.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/ansible/ansible/issues/39165#issuecomment-384533081>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AGNGa_VwsiUe5pVwPMu53_E4EqXqubfYks5tsW8egaJpZM4TgCMh>
> .
>

@saargrin I think a separate module will give justification to this feature. Honestly, right now, vmware_guest is overpacked with features and adding one more will increase chance of new issues,  complexity and maintainability of the module. So to keep things the UNIX way "do one thing and do it well" +1 for the new module.  

Open Source Contribution is always welcomed. You can start looking into this document - http://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html#how-to-develop-a-module 
You can follow VMware_* naming convention as "vmware_vm_move" or "vmware_guest_move". Let me know if you need any help on this process. Thanks for feature request and your willingness to contribute.
cc @warthog9
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest_move.py b/lib/ansible/modules/cloud/vmware/vmware_guest_move.py
new file mode 100644
--- /dev/null
+++ b/lib/ansible/modules/cloud/vmware/vmware_guest_move.py
@@ -0,0 +1,218 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Jose Angel Munoz <josea.munoz () gmail.com>
+# Copyright: (c) 2018, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+    'metadata_version': '1.1',
+    'status': ['preview'],
+    'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: vmware_guest_move
+short_description: Moves virtual machines in vCenter
+description:
+    - This module can be used to move virtual machines between folders.
+version_added: '2.7'
+author:
+    - Jose Angel Munoz (@imjoseangel)
+notes:
+    - Tested on vSphere 5.5 and vSphere 6.5
+requirements:
+    - python >= 2.6
+    - PyVmomi
+options:
+   name:
+        description:
+            - Name of the existing virtual machine to move.
+            - This is required if C(UUID) is not supplied.
+   uuid:
+        description:
+            - UUID of the virtual machine to manage if known, this is VMware's unique identifier.
+            - This is required if C(name) is not supplied.
+   name_match:
+        description:
+            - If multiple virtual machines matching the name, use the first or last found.
+        default: 'first'
+        choices: [ first, last ]
+   dest_folder:
+        description:
+            - Absolute path to move an existing guest
+            - The dest_folder should include the datacenter. ESX's datacenter is ha-datacenter.
+            - This parameter is case sensitive.
+            - 'Examples:'
+            - '   dest_folder: /ha-datacenter/vm'
+            - '   dest_folder: ha-datacenter/vm'
+            - '   dest_folder: /datacenter1/vm'
+            - '   dest_folder: datacenter1/vm'
+            - '   dest_folder: /datacenter1/vm/folder1'
+            - '   dest_folder: datacenter1/vm/folder1'
+            - '   dest_folder: /folder1/datacenter1/vm'
+            - '   dest_folder: folder1/datacenter1/vm'
+            - '   dest_folder: /folder1/datacenter1/vm/folder2'
+        required: True
+   datacenter:
+        description:
+            - Destination datacenter for the move operation
+        required: True
+extends_documentation_fragment: vmware.documentation
+'''
+
+EXAMPLES = r'''
+- name: Move Virtual Machine
+  vmware_guest_move:
+    hostname: "{{ vcenter_ip }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    datacenter: datacenter
+    validate_certs: False
+    name: testvm-1
+    dest_folder: datacenter/vm/prodvms
+
+- name: Get VM UUID
+  vmware_guest_facts:
+    hostname: "{{ vcenter_ip }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    validate_certs: no
+    datacenter: "{{ datacenter }}"
+    folder: "/{{datacenter}}/vm"
+    name: "{{ vm_name }}"
+  register: vm_facts
+
+- name: Get UUID from previous task and pass it to this task
+  vmware_guest_move:
+    hostname: "{{ vcenter_ip }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    validate_certs: no
+    uuid: "{{ vm_facts.instance.hw_product_uuid }}"
+    dest_folder: "/DataCenter/vm/path/to/new/folder/where/we/want"
+  datacenter: "{{ datacenter }}"
+  delegate_to: localhost
+  register: facts
+'''
+
+RETURN = """
+instance:
+    description: metadata about the virtual machine
+    returned: always
+    type: dict
+    sample: {
+        "annotation": null,
+        "current_snapshot": null,
+        "customvalues": {},
+        "guest_consolidation_needed": false,
+        "guest_question": null,
+        "guest_tools_status": null,
+        "guest_tools_version": "0",
+        "hw_cores_per_socket": 1,
+        "hw_datastores": [
+            "LocalDS_0"
+        ],
+        "hw_esxi_host": "DC0_H0",
+        "hw_eth0": {
+            "addresstype": "generated",
+            "ipaddresses": null,
+            "label": "ethernet-0",
+            "macaddress": "00:0c:29:6b:34:2c",
+            "macaddress_dash": "00-0c-29-6b-34-2c",
+            "summary": "DVSwitch: 43cdd1db-1ef7-4016-9bbe-d96395616199"
+        },
+        "hw_files": [
+            "[LocalDS_0] DC0_H0_VM0/DC0_H0_VM0.vmx"
+        ],
+        "hw_folder": "/F0/DC0/vm/F0",
+        "hw_guest_full_name": null,
+        "hw_guest_ha_state": null,
+        "hw_guest_id": "otherGuest",
+        "hw_interfaces": [
+            "eth0"
+        ],
+        "hw_is_template": false,
+        "hw_memtotal_mb": 32,
+        "hw_name": "DC0_H0_VM0",
+        "hw_power_status": "poweredOn",
+        "hw_processor_count": 1,
+        "hw_product_uuid": "581c2808-64fb-45ee-871f-6a745525cb29",
+        "instance_uuid": "8bcb0b6e-3a7d-4513-bf6a-051d15344352",
+        "ipv4": null,
+        "ipv6": null,
+        "module_hw": true,
+        "snapshots": []
+    }
+"""
+
+try:
+    import pyVmomi
+    from pyVmomi import vim
+except ImportError:
+    pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+from ansible.module_utils.vmware import PyVmomi, vmware_argument_spec, connect_to_api, wait_for_task
+
+
+class PyVmomiHelper(PyVmomi):
+    def __init__(self, module):
+        super(PyVmomiHelper, self).__init__(module)
+
+
+def main():
+    argument_spec = vmware_argument_spec()
+    argument_spec.update(
+        name=dict(type='str'),
+        name_match=dict(
+            type='str', choices=['first', 'last'], default='first'),
+        uuid=dict(type='str'),
+        dest_folder=dict(type='str', required=True),
+        datacenter=dict(type='str', required=True),
+    )
+    module = AnsibleModule(
+        argument_spec=argument_spec, required_one_of=[['name', 'uuid']])
+
+    # FindByInventoryPath() does not require an absolute path
+    # so we should leave the input folder path unmodified
+    module.params['dest_folder'] = module.params['dest_folder'].rstrip('/')
+
+    pyv = PyVmomiHelper(module)
+    search_index = pyv.content.searchIndex
+
+    # Check if the VM exists before continuing
+    vm = pyv.get_vm()
+
+    # VM exists
+    if vm:
+        try:
+            vm_path = pyv.get_vm_path(pyv.content, vm).lstrip('/')
+            vm_full = vm_path + '/' + module.params['name']
+            folder = search_index.FindByInventoryPath(
+                module.params['dest_folder'])
+            vm_to_move = search_index.FindByInventoryPath(vm_full)
+            if vm_path != module.params['dest_folder'].lstrip('/'):
+                move_task = folder.MoveInto([vm_to_move])
+                changed, err = wait_for_task(move_task)
+                if changed:
+                    module.exit_json(
+                        changed=True, instance=pyv.gather_facts(vm))
+            else:
+                module.exit_json(instance=pyv.gather_facts(vm))
+        except Exception as exc:
+            module.fail_json(msg="Failed to move VM with exception %s" %
+                             to_native(exc))
+    else:
+        module.fail_json(msg="Unable to find VM %s to move to %s" % (
+            (module.params.get('uuid') or module.params.get('name')),
+            module.params.get('dest_folder')))
+
+
+if __name__ == '__main__':
+    main()


Test patch:
diff --git a/test/integration/targets/vmware_guest_move/aliases b/test/integration/targets/vmware_guest_move/aliases
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/vmware_guest_move/aliases
@@ -0,0 +1,2 @@
+shippable/vcenter/group1
+cloud/vcenter
diff --git a/test/integration/targets/vmware_guest_move/tasks/main.yml b/test/integration/targets/vmware_guest_move/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/vmware_guest_move/tasks/main.yml
@@ -0,0 +1,78 @@
+# Test code for the vmware_guest_move module
+# Copyright: (c) 2018, Jose Angel Munoz <josea.munoz@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- name: store the vcenter container ip
+  set_fact:
+    vcsim: "{{ lookup('env', 'vcenter_host') }}"
+- debug: var=vcsim
+
+- name: Wait for Flask controller to come up online
+  wait_for:
+    host: "{{ vcsim }}"
+    port: 5000
+    state: started
+
+- name: kill vcsim
+  uri:
+    url: http://{{ vcsim }}:5000/killall
+
+- name: start vcsim
+  uri:
+    url: http://{{ vcsim }}:5000/spawn?folder=2&dc=2
+  register: vcsim_instance
+
+- name: Wait for Flask controller to come up online
+  wait_for:
+    host: "{{ vcsim }}"
+    port: 443
+    state: started
+
+- debug: var=vcsim_instance
+
+- name: get a list of virtual machines from vcsim
+  uri:
+    url: http://{{ vcsim }}:5000/govc_find?filter=VM
+  register: vms
+
+- set_fact: vm1="{{ vms['json'][0] }}"
+
+# Testcase 0001: Move vm and get changed status
+- name: Move VM (Changed)
+  vmware_guest_move:
+    validate_certs: false
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    datacenter: "{{ (vm1|basename).split('_')[0] }}"
+    name: "{{ vm1|basename }}"
+    dest_folder: F1/DC1/vm/F1
+  register: vm_facts_0001
+
+
+# Testcase 0002: Move vm and get OK status (Already Moved)
+- name: Move VM (OK)
+  vmware_guest_move:
+    validate_certs: false
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    datacenter: "{{ (vm1|basename).split('_')[0] }}"
+    name: "{{ vm1|basename }}"
+    dest_folder: F1/DC1/vm/F1
+  register: vm_facts_0002
+
+- debug:
+    msg: "{{ vm_facts_0001 }}"
+
+- debug:
+    msg: "{{ vm_facts_0002 }}"
+
+- name: get all VMs
+  uri:
+    url: http://{{ vcsim }}:5000/govc_find?filter=VM
+  register: vms_diff
+
+- name: Difference
+  debug:
+    var: vms_diff.json | difference(vms.json)