diff --git a/changelogs/fragments/48-add_folder_template.yml b/changelogs/fragments/48-add_folder_template.yml new file mode 100644 index 00000000..996abb4c --- /dev/null +++ b/changelogs/fragments/48-add_folder_template.yml @@ -0,0 +1,2 @@ +minor_changes: + - folder_template_from_vm - add module and tests to create a template from an existing VM in vcenter and store the template in a folder diff --git a/plugins/doc_fragments/vmware.py b/plugins/doc_fragments/vmware.py index 18fe0197..7eaa942b 100644 --- a/plugins/doc_fragments/vmware.py +++ b/plugins/doc_fragments/vmware.py @@ -46,6 +46,16 @@ class ModuleDocFragment(object): - If the value is not specified in the task, the value of environment variable E(VMWARE_PORT) will be used instead. type: int default: 443 + datacenter: + description: + - The datacenter to use when connecting to a vCenter. + type: str + aliases: [ datacenter_name ] + cluster: + description: + - The cluster to use when connecting to a vCenter. + type: str + aliases: [ cluster_name ] proxy_host: description: - Address of a proxy that will receive all HTTPS requests and relay them. @@ -95,6 +105,16 @@ class ModuleDocFragment(object): - If the value is not specified in the task, the value of environment variable E(VMWARE_PORT) will be used instead. type: int default: 443 + datacenter: + description: + - The datacenter to use when connecting to a vCenter. + type: str + aliases: [ datacenter_name ] + cluster: + description: + - The cluster to use when connecting to a vCenter. + type: str + aliases: [ cluster_name ] proxy_host: description: - Address of a proxy that will receive all HTTPS requests and relay them. diff --git a/plugins/module_utils/vmware.py b/plugins/module_utils/vmware.py index 725e86f1..058cd8a6 100644 --- a/plugins/module_utils/vmware.py +++ b/plugins/module_utils/vmware.py @@ -58,6 +58,12 @@ def vmware_argument_spec(): required=False, no_log=True, fallback=(env_fallback, ['VMWARE_PASSWORD'])), + cluster=dict(type='str', + aliases=['cluster_name'], + required=False), + datacenter=dict(type='str', + aliases=['datacenter_name'], + required=False), port=dict(type='int', default=443, fallback=(env_fallback, ['VMWARE_PORT'])), @@ -225,7 +231,9 @@ def is_vcenter(self): def get_objs_by_name_or_moid(self, vimtype, name, return_all=False): """ - Get any vsphere object associated with a given text name and vim type. + Get any vsphere objects associated with a given text name or MOID and vim type. + Different objects have different unique-ness requirements for the name parameter, so + you may get one or more objects back. The MOID should always be unique Args: vimtype: The type of object to search for name: The name or the ID of the object to search for @@ -281,3 +289,117 @@ def get_datacenter_detailed(self, name): """ return self.get_objs_by_name_or_moid([vim.Datacenter], name) + + def get_vm_using_params( + self, name_param='name', uuid_param='uuid', moid_param='moid', fail_on_missing=False, + name_match_param='name_match', use_instance_uuid_param='use_instance_uuid'): + """ + TODO + Get the vms matching the common module params related to vm identification: name, uuid, or moid. Since + MOID and UUID are unique identifiers, they are tried first. If they are not set, a search by name is tried + which may give one or more vms. + This also supports the 'name_match' parameter and the 'use_instance_uuid' parameters. The VM identification + parameter keys can be changed if your module uses different keys, like vm_name instead of just name + Args: + name_param: Set the prameter key that corredsponds to the VM name + uuid_param: Set the prameter key that corredsponds to the VM UUID + moid_param: Set the prameter key that corredsponds to the VM MOID + name_match_param: Set the prameter key that corredsponds to the name_match option + use_instance_uuid_param: Set the prameter key that corredsponds use_instance_uuid option + fail_on_missing: If true, an error will be thrown if no VMs are found + Returns: + list(vm), or None if no matches were found + """ + if self.params.get(moid_param): + _search_type, _search_id, _search_value = 'moid', moid_param, self.params.get(moid_param) + elif self.params.get(uuid_param): + _search_type, _search_id, _search_value = 'uuid', uuid_param, self.params.get(uuid_param) + elif self.params.get(name_param): + _search_type, _search_id, _search_value = 'name', name_param, self.params.get(name_param) + else: + self.module.fail_json("Could not find any supported VM identifier params (name, uuid, or moid)") + + if _search_type == 'uuid': + _vm = self.si.content.searchIndex.FindByUuid( + instanceUuid=self.params.get(use_instance_uuid_param, True), + uuid=_search_value, + vmSearch=True + ) + vms = [_vm] if _vm else None + else: + vms = self.get_objs_by_name_or_moid([vim.VirtualMachine], _search_value, return_all=True) + + if vms and _search_type == 'name' and self.params.get(name_match_param): + if self.params.get(name_match_param) == 'first': + return [vms[0]] + elif self.params.get(name_match_param) == 'last': + return [vms[-1]] + else: + self.module.fail_json("Unrecognized name_match option '%s' " % self.params.get(name_match_param)) + + if not vms and fail_on_missing: + self.module.fail_json("Unable to find VM with %s %s" % _search_id, _search_value) + + return vms + + def get_folder_by_name(self, folder_name, fail_on_missing=False): + """ + Get all folders with the given name. Names are not unique + in a given cluster, so multiple folder objects can be returned + Args: + folder_name: Name of the folder to search for + fail_on_missing: If true, an error will be thrown if no folders are found + Returns: + list(folder object) or None + """ + folder = self.get_objs_by_name_or_moid([vim.Folder], folder_name, return_all=True) + if not folder and fail_on_missing: + self.module.fail_json("Unable to find folder with name %s" % folder_name) + return folder + + def get_folder_by_absolute_path(self, folder_path, fail_on_missing=False): + """ + Get a folder with the given path. Paths are unique when they are absolute so only + one folder can be returned at most. An absolute path might look like + 'Datacenter Name/vm/my/folder/structure' + Args: + folder_path: The absolute path to a folder to search for + fail_on_missing: If true, an error will be thrown if no folders are found + Returns: + folder object or None + """ + folder = self.si.content.searchIndex.FindByInventoryPath(folder_path) + + if not folder and fail_on_missing: + self.module.fail_json("Unable to find folder with absolute path %s" % folder_path) + return folder + + def get_datastore_by_name(self, ds_name, fail_on_missing=False): + """ + Get the datastore matching the given name. Datastore names must be unique + in a given cluster, so only one object is returned at most. + Args: + ds_name: Name of the datastore to search for + fail_on_missing: If true, an error will be thrown if no datastores are found + Returns: + datastore object or None + """ + ds = self.get_objs_by_name_or_moid([vim.Datastore], ds_name) + if not ds and fail_on_missing: + self.module.fail_json("Unable to find datastore with name %s" % ds_name) + return ds + + def get_resource_pool_by_name(self, pool_name, fail_on_missing=False): + """ + Get the resource pool matching the given name. Pool names must be unique + in a given cluster, so only one object is returned at most. + Args: + pool_name: Name of the pool to search for + fail_on_missing: If true, an error will be thrown if no pools are found + Returns: + resource pool object or None + """ + pool = self.get_objs_by_name_or_moid([vim.ResourcePool], pool_name) + if not pool and fail_on_missing: + self.module.fail_json("Unable to find resource pool with name %s" % pool_name) + return pool diff --git a/plugins/module_utils/vmware_folder_paths.py b/plugins/module_utils/vmware_folder_paths.py new file mode 100644 index 00000000..68fc6376 --- /dev/null +++ b/plugins/module_utils/vmware_folder_paths.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Ansible Cloud Team (@ansible-collections) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +FOLDER_TYPES = ('vm', 'host', 'network', 'datastore') + + +def __prepend_datacenter_and_folder_type(folder_path, datacenter_name, folder_type=None): + """ + Formats a folder path so it is absolute, meaning it includes the datacenter name and + type (vm, host, etc) at the start of the path. If path already starts with + the datacenter name, nothing is added. + Eg: rest/of/path -> datacenter name/type/rest/of/path + """ + folder_path = folder_path.lstrip('/') + if folder_path.startswith(datacenter_name): + return folder_path + + if folder_type not in FOLDER_TYPES: + raise ValueError("folder_type %s not in acceptable " % folder_type + + "folder type values %s" % ', '.join(FOLDER_TYPES)) + + return '/'.join([datacenter_name, folder_type, folder_path]) + + +def format_folder_path_as_vm_fq_path(folder_path, datacenter_name): + """ + Formats a VM folder path so it is absolute, meaning it prepends + 'datacenter name/vm/' to the path if needed. If path already starts with + the datacenter name, nothing is added. + Eg: rest/of/path -> datacenter name/vm/rest/of/path + """ + return __prepend_datacenter_and_folder_type(folder_path, datacenter_name, folder_type='vm') + + +def format_folder_path_as_host_fq_path(folder_path, datacenter_name): + """ + Formats a host folder path so it is absolute, meaning it prepends + 'datacenter name/vm/' to the path if needed. If path already starts with + the datacenter name, nothing is added. + Eg: rest/of/path -> datacenter name/host/rest/of/path + """ + return __prepend_datacenter_and_folder_type(folder_path, datacenter_name, folder_type='host') + + +def format_folder_path_as_network_fq_path(folder_path, datacenter_name): + """ + Formats a network folder path so it is absolute, meaning it prepends + 'datacenter name/network/' to the path if needed. If path already starts with + the datacenter name, nothing is added. + Eg: rest/of/path -> datacenter name/network/rest/of/path + """ + return __prepend_datacenter_and_folder_type(folder_path, datacenter_name, folder_type='network') + + +def format_folder_path_as_datastore_fq_path(folder_path, datacenter_name): + """ + Formats a datastore folder path so it is absolute, meaning it prepends + 'datacenter name/datastore/' to the path if needed. If path already starts with + the datacenter name, nothing is added. + Eg: rest/of/path -> datacenter name/datastore/rest/of/path + """ + return __prepend_datacenter_and_folder_type(folder_path, datacenter_name, folder_type='datastore') diff --git a/plugins/modules/folder_template_from_vm.py b/plugins/modules/folder_template_from_vm.py new file mode 100644 index 00000000..8ff3b94f --- /dev/null +++ b/plugins/modules/folder_template_from_vm.py @@ -0,0 +1,312 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Ansible Cloud Team (@ansible-collections) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: folder_template_from_vm +short_description: Create a template in a local VCenter folder from an existing VM +description: + - >- + This module creates a template in a local VCenter folder from an existing VM. The folder must already exist. + The VM must be powered off, and is otherwise unchanged. If the template already exists and the desired state + is 'present', nothing is done. +author: + - Ansible Cloud Team (@ansible-collections) +requirements: + - pyvmomi +options: + vm_name: + description: + - The name of the vm to be used to create the template + - One of vm_name, vm_moid, vm_uuid is required + - This parameter is ignored when state is 'absent' + type: str + required: False + vm_uuid: + description: + - The UUID of the vm to be used to create the template + - One of vm_name, vm_moid, vm_uuid is required + - This parameter is ignored when state is 'absent' + type: str + required: False + vm_moid: + description: + - The MOID of the vm to be used to create the template + - One of vm_name, vm_moid, vm_uuid is required + - This parameter is ignored when state is 'absent' + type: str + required: False + vm_use_instance_uuid: + description: + - If true, search by instance UUID instead of BIOS UUID. + - BIOS UUID may not be unique and may cause errors. + type: bool + required: False + default: True + vm_name_match: + description: + - If using vm_name and multiple VMs have the same name, specify which VM should be selected + type: str + required: False + choices: ['first', 'last'] + template_folder: + description: + - The name of the folder that the new template should be placed in + - Should be the full folder path, with or without the 'datacenter/vm/' prefix + - For example 'datacenter name/vm/path/to/folder' or 'path/to/folder' + type: str + required: True + template_name: + description: + - The name to give to the new template. + type: str + required: True + state: + description: + - If the template should be present or absent + type: str + required: False + default: present + choices: ['present', 'absent'] + datacenter: + description: + - The name of datacenter in which to operate + type: str + aliases: ['datacenter_name'] + required: True + datastore: + description: + - The name of datastore to use as storage for the template. + type: str + resource_pool: + description: + - The resource pool to place the template in. + type: str + wait_for_template: + description: + - If true, the module will wait until the template is created to exit. + type: bool + default: True +attributes: + check_mode: + description: The check_mode support. + support: full +extends_documentation_fragment: + - vmware.vmware.vmware.vcenter_documentation + +''' + +EXAMPLES = r''' +- name: Create A New Template Using VM UUID + vmware.vmware.folder_template: + hostname: "https://vcenter" + username: "username" + password: "password" + datacenter: "my-datacenter" + vm_uuid: "11111111-11111111-11111111" + template_folder: "my-datacenter/vm/netsted/folder/path/tempaltes" + template_name: "my_template" + +- name: Create A New Template Using VM Name + vmware.vmware.folder_template_from_vm: + hostname: "https://vcenter" + username: "username" + password: "password" + datacenter: "my-datacenter" + vm_name: "my_vm" + vm_name_match: "first" + template_name: "my_template" + template_folder: "nested/folder/path/templates" + +- name: Destroy A Template In A Folder + vmware.vmware.folder_template_from_vm: + hostname: "https://vcenter" + username: "username" + password: "password" + datacenter: "my-datacenter" + vm_name: "foo" + state: "absent" + template_name: "my_template" + template_folder: "nested/folder/path/templates" +''' + +RETURN = r''' +''' + +import time +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vmware.vmware.plugins.module_utils.vmware import PyVmomi, vmware_argument_spec +from ansible_collections.vmware.vmware.plugins.module_utils.vmware_folder_paths import format_folder_path_as_vm_fq_path + +PYVMOMI_IMP_ERR = None +try: + from pyVmomi import vim + HAS_PYVMOMI = True +except ImportError: + PYVMOMI_IMP_ERR = traceback.format_exc() + HAS_PYVMOMI = False + + +class VmwareFolderTemplate(PyVmomi): + def __init__(self, module): + super(VmwareFolderTemplate, self).__init__(module) + if not self.is_vcenter(): + self.module.fail_json("Only VCenter clusters are supported for this module.") + + self.template_name = self.params.get("template_name") + + fq_folder_path = format_folder_path_as_vm_fq_path( + self.params.get("template_folder"), + self.params.get("datacenter") + ) + self.template_folder = self.get_folder_by_absolute_path(fq_folder_path, fail_on_missing=True) + + def check_if_template_exists(self): + """ + Checks if a template with the given name and folder already exists + """ + templates = self.get_vm_using_params(name_param='template_name', fail_on_missing=False) + if not templates: + return False + + for template in templates: + if not template.parent == self.template_folder: + continue + + if template.config.template: + return template + else: + self.module.fail_json("A virtual machine already exists with desired template name, %s." % self.template_name) + + return False + + def __get_source_vm(self): + """ + Uses the UUID, MOID, or name provided to find the source VM for the template. Returns an error if using the name, + multiple matches are found, and the user did not provide a name_match strategy. + """ + vms = self.get_vm_using_params( + name_param='vm_name', + moid_param='vm_moid', + uuid_param='vm_uuid', + name_match_param='vm_name_match', + use_instance_uuid_param='vm_use_instance_uuid', + fail_on_missing=True) + + if len(vms) != 1: + self.module.fail_json("Multiple VMs found during search. Try using the vm_name_match or vm_uuid/vm_moid attributes.") + + vm = vms[0] + if vm.runtime.powerState != 'poweredOff': + self.module.fail_json(msg="VM must be in powered off state before creating a template from it.") + + return vm + + def create_template_in_folder(self): + """ + Clones an existing VM into a new template instance. + """ + vm = self.__get_source_vm() + template_location_spec = self.__create_template_location_spec() + template_spec = vim.vm.CloneSpec(powerOn=False, template=True, location=template_location_spec) + if self.module.check_mode: + return + + task = vm.Clone( + name=self.template_name, + folder=self.template_folder, + spec=template_spec + ) + + if self.params.get("wait_for_template"): + self.__wait_for_template(task) + + def __create_template_location_spec(self): + template_location_spec = vim.vm.RelocateSpec() + if self.params.get("datastore"): + template_location_spec.datastore = self.get_datastore_by_name( + self.params.get("datastore"), + fail_on_missing=True) + + if self.params.get("resource_pool"): + template_location_spec.pool = self.get_resource_pool_by_name( + self.params.get("resource_pool"), + fail_on_missing=True) + + return template_location_spec + + def __wait_for_template(self, task): + """ + Waits and provides updates on a vSphere task + """ + while task.info.state == vim.TaskInfo.State.running: + time.sleep(2) + + if task.info.state != vim.TaskInfo.State.success: + self.module.fail_json(msg=task.info.error) + + +def custom_validation(module): + """ + This validation is too complex to be done with the provided ansible validation + """ + if module.params.get('state') == 'present': + if (not module.params.get('vm_name') and + not module.params.get('vm_uuid') and + not module.params.get('vm_moid')): + module.fail_json("One of vm_name, vm_uuid, or vm_moid is required when state is 'present'") + + +def main(): + module = AnsibleModule( + argument_spec={ + **vmware_argument_spec(), **dict( + vm_name=dict(type='str', required=False, default=None), + vm_name_match=dict(type='str', required=False, choices=['first', 'last']), + vm_uuid=dict(type='str', required=False, default=None), + vm_use_instance_uuid=dict(type='bool', required=False, default=True), + vm_moid=dict(type='str', required=False, default=None), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), + template_name=dict(type='str', required=True), + template_folder=dict(type='str', required=True), + datacenter=dict(type='str', aliases=['datacenter_name'], required=True), + datastore=dict(type='str', required=False), + resource_pool=dict(type='str', required=False), + wait_for_template=dict(type='bool', required=False, default=True), + ) + }, + mutually_exclusive=[['vm_name', 'vm_uuid', 'vm_moid']], + supports_check_mode=True, + ) + + result = dict( + changed=False, + ) + + custom_validation(module) + folder_template = VmwareFolderTemplate(module) + if module.params.get('state') == 'present': + if not folder_template.check_if_template_exists(): + folder_template.create_template_in_folder() + result['changed'] = True + else: + template = folder_template.check_if_template_exists() + if template: + if not module.check_mode: + template.Destroy_Task() + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/vmware_folder_template_from_vm/defaults/main.yml b/tests/integration/targets/vmware_folder_template_from_vm/defaults/main.yml new file mode 100644 index 00000000..01039c81 --- /dev/null +++ b/tests/integration/targets/vmware_folder_template_from_vm/defaults/main.yml @@ -0,0 +1,16 @@ +--- +run_on_simulator: false +template_folder: e2e-qe +vm_name: "{{ tiny_prefix }}_folder_template_from_vm_test" +vm_name_match: first +template_name: "{{ vm_name }}_template" + +vm_guest_id: "rhel8_64Guest" +vm_disk: + - size_gb: 10 + type: thin + autoselect_datastore: true +vm_hardware: + memory_mb: 2000 + num_cpus: 2 + boot_firmware: efi diff --git a/tests/integration/targets/vmware_folder_template_from_vm/run.yml b/tests/integration/targets/vmware_folder_template_from_vm/run.yml new file mode 100644 index 00000000..b321157c --- /dev/null +++ b/tests/integration/targets/vmware_folder_template_from_vm/run.yml @@ -0,0 +1,27 @@ +- hosts: localhost + gather_facts: no + collections: + - community.general + + tasks: + - name: Import eco-vcenter credentials + ansible.builtin.include_vars: + file: ../../integration_config.yml + tags: eco-vcenter-ci + + - name: Import simulator vars + ansible.builtin.include_vars: + file: vars.yml + tags: integration-ci + + - name: Vcsim + ansible.builtin.import_role: + name: prepare_vcsim + tags: integration-ci + + - name: Import vmware_folder_template role + ansible.builtin.import_role: + name: vmware_folder_template_from_vm + tags: + - integration-ci + - eco-vcenter-ci diff --git a/tests/integration/targets/vmware_folder_template_from_vm/tasks/main.yml b/tests/integration/targets/vmware_folder_template_from_vm/tasks/main.yml new file mode 100644 index 00000000..7c18e3c7 --- /dev/null +++ b/tests/integration/targets/vmware_folder_template_from_vm/tasks/main.yml @@ -0,0 +1,104 @@ +--- +- name: Test On Simulator + when: run_on_simulator + block: + - name: "Test setup: Create Folder" + community.vmware.vcenter_folder: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + port: "{{ vcenter_port }}" + datacenter: "{{ vcenter_datacenter }}" + folder_name: "{{ template_folder }}" + folder_type: vm + state: present + + - name: "Test setup: Power Off VM guest {{ vm_name }}" + community.vmware.vmware_guest: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + cluster: "{{ vcenter_cluster_name }}" + datacenter: "{{ vcenter_datacenter }}" + port: "{{ vcenter_port }}" + state: poweredoff + name: "{{ vm_name }}" + + - name: Create template from vm in vcenter folder + vmware.vmware.folder_template_from_vm: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ vcenter_datacenter }}" + port: "{{ vcenter_port }}" + template_folder: "{{ template_folder }}" + vm_name: "{{ vm_name }}" + template_name: "{{ template_name }}" + register: __res + +- name: Test On VCenter + when: not run_on_simulator + block: + - name: Import common vars + ansible.builtin.include_vars: + file: ../group_vars.yml + - name: "Test setup: Create VM guest {{ vm }}" + community.vmware.vmware_guest: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + cluster: "{{ vcenter_cluster_name }}" + port: "{{ vcenter_port }}" + datacenter: "{{ vcenter_datacenter }}" + folder: "{{ template_folder }}" + state: present + name: "{{ vm_name }}" + disk: "{{ vm_disk }}" + guest_id: "{{ vm_guest_id }}" + hardware: "{{ vm_hardware }}" + + - name: Create template from vm in vcenter folder + vmware.vmware.folder_template_from_vm: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ vcenter_datacenter }}" + port: "{{ vcenter_port }}" + template_folder: "{{ template_folder }}" + vm_name: "{{ vm_name }}" + template_name: "{{ template_name }}" + register: __res + + always: + - name: "Test teardown: Destroy VM guest {{ vm_name }}" + community.vmware.vmware_guest: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + cluster: "{{ vcenter_cluster_name }}" + port: "{{ vcenter_port }}" + datacenter: "{{ vcenter_datacenter }}" + folder: "{{ template_folder }}" + state: absent + force: true + name: "{{ vm_name }}" + + - name: "Test teardown: Destroy VM template {{ template_name }}" + community.vmware.vmware_guest: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + cluster: "{{ vcenter_cluster_name }}" + port: "{{ vcenter_port }}" + datacenter: "{{ vcenter_datacenter }}" + folder: "{{ template_folder }}" + state: absent + force: true + name: "{{ template_name }}" diff --git a/tests/integration/targets/vmware_folder_template_from_vm/vars.yml b/tests/integration/targets/vmware_folder_template_from_vm/vars.yml new file mode 100644 index 00000000..a342faae --- /dev/null +++ b/tests/integration/targets/vmware_folder_template_from_vm/vars.yml @@ -0,0 +1,10 @@ +--- +vcenter_hostname: "127.0.0.1" +vcenter_username: "user" +vcenter_password: "pass" +vcenter_port: 8989 +vcenter_datacenter: DC0 +vcenter_cluster_name: DC0_C0 + +run_on_simulator: true +vm_name: DC0_H0_VM0