Repository: ansible/ansible
Pull request: template add cloud init
##### SUMMARY
<!--- Describe the change below, including rationale and design decisions -->
ovirt template add 

usb_support
timezone
sso
soundcard_enabled
smartcard_enabled
nics
cloud_init_persist
cloud_init_nics
cloud_init
ballooning_enabled

Fixes #55342

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
- Feature Pull Request

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below -->
ovirt
##### ADDITIONAL INFORMATION
<!--- Include additional information to help people understand the change here -->
<!--- A step-by-step reproduction of the problem is helpful if there is no related issue -->

<!--- Paste verbatim command output below, e.g. before and after your change -->
```paste below

```


Problem statement:
[RFE] Extend  ovirt_template Module - VM Definitions
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->

The **ovirt_template** module needs to be able to provide and set multiple features similar to the **ovirt_vm**.

Need to be able to set multiple pieces of information (USB Redirect, Networking/vNICS, Cloud-Init, etc.). Essentially, this is missing the rest of the features that can be specified from the RHVM UI.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->

ovirt_template

Pieces to add are from the **ovirt_vm** module.

vnic_profile_mappings
usb_support
timezone
sso
soundcard_enabled
smartcard_enabled
nics
memory_max
memory
memory_guaranteed
cloud_init_persist
cloud_init_nics
cloud_init
ballooning_enabled

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->

This feature would provide the full functionality for the **ovirt_template** module allowing items to be changed or set differently than the VM that the template was based on. Usage would be similar to that of the **ovirt_vm** module.


<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- HINT: You can also paste gist.github.com links for larger files -->

Hints from issue discussion:
Files identified in the description:
* [lib/ansible/modules/cloud/misc/cloud_init_data_facts.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/misc/cloud_init_data_facts.py)
* [lib/ansible/modules/cloud/ovirt/ovirt_template.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/ovirt/ovirt_template.py)
* [lib/ansible/modules/cloud/ovirt/ovirt_vm.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/ovirt/ovirt_vm.py)
* [lib/ansible/modules/system/timezone.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/timezone.py)
* [lib/ansible/plugins/cache/memory.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/cache/memory.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 @indrajitr @jasperla @machacekondra @mnecas @mwperina @resmo @tmshn
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Commit message: remove duplicate version

Patch to review:
diff --git a/lib/ansible/modules/cloud/ovirt/ovirt_template.py b/lib/ansible/modules/cloud/ovirt/ovirt_template.py
index a39a628c477367..d123a7b96af865 100644
--- a/lib/ansible/modules/cloud/ovirt/ovirt_template.py
+++ b/lib/ansible/modules/cloud/ovirt/ovirt_template.py
@@ -189,6 +189,147 @@
             - Name for importing Template from storage domain.
             - If not defined, C(name) will be used.
         version_added: "2.8"
+    usb_support:
+        description:
+            - "I(True) enable USB support, I(False) to disable it. By default is chosen by oVirt/RHV engine."
+        type: bool
+        version_added: "2.9"
+    timezone:
+        description:
+            - Sets time zone offset of the guest hardware clock.
+            - For example C(Etc/GMT)
+        version_added: "2.9"
+    sso:
+        description:
+            - "I(True) enable Single Sign On by Guest Agent, I(False) to disable it. By default is chosen by oVirt/RHV engine."
+        type: bool
+        version_added: "2.9"
+    soundcard_enabled:
+        description:
+            - "If I(true), the sound card is added to the virtual machine."
+        type: bool
+        version_added: "2.9"
+    smartcard_enabled:
+        description:
+            - "If I(true), use smart card authentication."
+        type: bool
+        version_added: "2.9"
+    cloud_init:
+        description:
+            - Dictionary with values for Unix-like Virtual Machine initialization using cloud init.
+        suboptions:
+            host_name:
+                description:
+                    - Hostname to be set to Virtual Machine when deployed.
+            timezone:
+                description:
+                    - Timezone to be set to Virtual Machine when deployed.
+            user_name:
+                description:
+                    - Username to be used to set password to Virtual Machine when deployed.
+            root_password:
+                description:
+                    - Password to be set for user specified by C(user_name) parameter.
+            authorized_ssh_keys:
+                description:
+                    - Use this SSH keys to login to Virtual Machine.
+            regenerate_ssh_keys:
+                description:
+                    - If I(True) SSH keys will be regenerated on Virtual Machine.
+                type: bool
+            custom_script:
+                description:
+                    - Cloud-init script which will be executed on Virtual Machine when deployed.
+                    - This is appended to the end of the cloud-init script generated by any other options.
+            dns_servers:
+                description:
+                    - DNS servers to be configured on Virtual Machine.
+            dns_search:
+                description:
+                    - DNS search domains to be configured on Virtual Machine.
+            nic_boot_protocol:
+                description:
+                    - Set boot protocol of the network interface of Virtual Machine.
+                choices: ['none', 'dhcp', 'static']
+            nic_ip_address:
+                description:
+                    - If boot protocol is static, set this IP address to network interface of Virtual Machine.
+            nic_netmask:
+                description:
+                    - If boot protocol is static, set this netmask to network interface of Virtual Machine.
+            nic_gateway:
+                description:
+                    - If boot protocol is static, set this gateway to network interface of Virtual Machine.
+            nic_name:
+                description:
+                    - Set name to network interface of Virtual Machine.
+            nic_on_boot:
+                description:
+                    - If I(True) network interface will be set to start on boot.
+                type: bool
+        version_added: "2.9"
+    cloud_init_nics:
+        description:
+            - List of dictionaries representing network interfaces to be setup by cloud init.
+            - This option is used, when user needs to setup more network interfaces via cloud init.
+            - If one network interface is enough, user should use C(cloud_init) I(nic_*) parameters. C(cloud_init) I(nic_*) parameters
+              are merged with C(cloud_init_nics) parameters.
+        suboptions:
+            nic_boot_protocol:
+                description:
+                    - Set boot protocol of the network interface of Virtual Machine. Can be one of C(none), C(dhcp) or C(static).
+            nic_ip_address:
+                description:
+                    - If boot protocol is static, set this IP address to network interface of Virtual Machine.
+            nic_netmask:
+                description:
+                    - If boot protocol is static, set this netmask to network interface of Virtual Machine.
+            nic_gateway:
+                description:
+                    - If boot protocol is static, set this gateway to network interface of Virtual Machine.
+            nic_name:
+                description:
+                    - Set name to network interface of Virtual Machine.
+            nic_on_boot:
+                description:
+                    - If I(True) network interface will be set to start on boot.
+                type: bool
+        version_added: "2.9"
+    ballooning_enabled:
+        description:
+            - "If I(true), use memory ballooning."
+            - "Memory balloon is a guest device, which may be used to re-distribute / reclaim the host memory
+               based on VM needs in a dynamic way. In this way it's possible to create memory over commitment states."
+        type: bool
+        version_added: "2.9"
+    nics:
+        description:
+            - List of NICs, which should be attached to Virtual Machine. NIC is described by following dictionary.
+        suboptions:
+            name:
+                description:
+                    - Name of the NIC.
+            profile_name:
+                description:
+                    - Profile name where NIC should be attached.
+            interface:
+                description:
+                    - Type of the network interface.
+                choices: ['virtio', 'e1000', 'rtl8139']
+                default: 'virtio'
+            mac_address:
+                description:
+                    - Custom MAC address of the network interface, by default it's obtained from MAC pool.
+                    - "NOTE - This parameter is used only when C(state) is I(running) or I(present) and is able to only create NICs.
+                    To manage NICs of the VM in more depth please use M(ovirt_nics) module instead."
+        version_added: "2.9"
+    cloud_init_persist:
+        description:
+            - "If I(yes) the C(cloud_init) parameters will be saved for the template and the virtual machine from template won't be started as run-once."
+        type: bool
+        aliases: [ 'sysprep_persist' ]
+        default: 'no'
+        version_added: "2.9"
 extends_documentation_fragment: ovirt
 '''
 
@@ -300,6 +441,54 @@
     vm: rhel7
     version:
         name: subversion
+
+- name: Template with cloud init
+  ovirt_template:
+    name: mytemplate
+    cluster: Default
+    memory: 1GiB
+    high_availability: true
+    high_availability_priority: 50  # Available from Ansible 2.5
+    cloud_init:
+      nic_boot_protocol: static
+      nic_ip_address: 10.34.60.86
+      nic_netmask: 255.255.252.0
+      nic_gateway: 10.34.63.254
+      nic_name: eth1
+      nic_on_boot: true
+      host_name: example.com
+      custom_script: |
+        write_files:
+         - content: |
+             Hello, world!
+           path: /tmp/greeting.txt
+           permissions: '0644'
+      user_name: root
+      root_password: super_password
+
+- name: Template with cloud init, with multiple network interfaces
+  ovirt_template:
+    name: mytemplate
+    cluster: mycluster
+    cloud_init_nics:
+    - nic_name: eth0
+      nic_boot_protocol: dhcp
+      nic_on_boot: true
+    - nic_name: eth1
+      nic_boot_protocol: static
+      nic_ip_address: 10.34.60.86
+      nic_netmask: 255.255.252.0
+      nic_gateway: 10.34.63.254
+      nic_on_boot: true
+
+- name: Template with timezone and nic
+  ovirt_template:
+    cluster: MyCluster
+    name: mytemplate
+    timezone: America/Godthab
+    memory_max: 2Gib
+    nics:
+      - name: nic1
 '''
 
 RETURN = '''
@@ -341,6 +530,10 @@
 
 class TemplatesModule(BaseModule):
 
+    def __init__(self, *args, **kwargs):
+        super(TemplatesModule, self).__init__(*args, **kwargs)
+        self._initialization = None
+
     def build_entity(self):
         return otypes.Template(
             id=self._module.params['id'],
@@ -358,26 +551,44 @@ def build_entity(self):
                     self._module.params['cpu_profile'],
                 ).id
             ) if self._module.params['cpu_profile'] else None,
+            display=otypes.Display(
+                smartcard_enabled=self.param('smartcard_enabled')
+            ) if self.param('smartcard_enabled') is not None else None,
             os=otypes.OperatingSystem(
                 type=self.param('operating_system'),
             ) if self.param('operating_system') else None,
             memory=convert_to_bytes(
                 self.param('memory')
             ) if self.param('memory') else None,
+            soundcard_enabled=self.param('soundcard_enabled'),
+            usb=(
+                otypes.Usb(enabled=self.param('usb_support'))
+            ) if self.param('usb_support') is not None else None,
+            sso=(
+                otypes.Sso(
+                    methods=[otypes.Method(id=otypes.SsoMethod.GUEST_AGENT)] if self.param('sso') else []
+                )
+            ) if self.param('sso') is not None else None,
+            time_zone=otypes.TimeZone(
+                name=self.param('timezone'),
+            ) if self.param('timezone') else None,
             version=otypes.TemplateVersion(
                 base_template=self._get_base_template(),
                 version_name=self.param('version').get('name'),
             ) if self.param('version') else None,
             memory_policy=otypes.MemoryPolicy(
                 guaranteed=convert_to_bytes(self.param('memory_guaranteed')),
+                ballooning=self.param('ballooning_enabled'),
                 max=convert_to_bytes(self.param('memory_max')),
             ) if any((
                 self.param('memory_guaranteed'),
+                self.param('ballooning_enabled'),
                 self.param('memory_max')
             )) else None,
             io=otypes.Io(
                 threads=self.param('io_threads'),
             ) if self.param('io_threads') is not None else None,
+            initialization=self.get_initialization() if self.param('cloud_init_persist') else None,
         )
 
     def _get_base_template(self):
@@ -388,12 +599,119 @@ def _get_base_template(self):
                     id=template.id
                 )
 
+    def post_update(self, entity):
+        self.post_present(entity.id)
+
+    def post_present(self, entity_id):
+        # After creation of the VM, attach disks and NICs:
+        entity = self._service.service(entity_id).get()
+        self.__attach_nics(entity)
+
+    def __get_vnic_profile_id(self, nic):
+        """
+        Return VNIC profile ID looked up by it's name, because there can be
+        more VNIC profiles with same name, other criteria of filter is cluster.
+        """
+        vnics_service = self._connection.system_service().vnic_profiles_service()
+        clusters_service = self._connection.system_service().clusters_service()
+        cluster = search_by_name(clusters_service, self.param('cluster'))
+        profiles = [
+            profile for profile in vnics_service.list()
+            if profile.name == nic.get('profile_name')
+        ]
+        cluster_networks = [
+            net.id for net in self._connection.follow_link(cluster.networks)
+        ]
+        try:
+            return next(
+                profile.id for profile in profiles
+                if profile.network.id in cluster_networks
+            )
+        except StopIteration:
+            raise Exception(
+                "Profile '%s' was not found in cluster '%s'" % (
+                    nic.get('profile_name'),
+                    self.param('cluster')
+                )
+            )
+
+    def __attach_nics(self, entity):
+        # Attach NICs to VM, if specified:
+        nics_service = self._service.service(entity.id).nics_service()
+        for nic in self.param('nics'):
+            if search_by_name(nics_service, nic.get('name')) is None:
+                if not self._module.check_mode:
+                    nics_service.add(
+                        otypes.Nic(
+                            name=nic.get('name'),
+                            interface=otypes.NicInterface(
+                                nic.get('interface', 'virtio')
+                            ),
+                            vnic_profile=otypes.VnicProfile(
+                                id=self.__get_vnic_profile_id(nic),
+                            ) if nic.get('profile_name') else None,
+                            mac=otypes.Mac(
+                                address=nic.get('mac_address')
+                            ) if nic.get('mac_address') else None,
+                        )
+                    )
+                self.changed = True
+
+    def get_initialization(self):
+        if self._initialization is not None:
+            return self._initialization
+
+        cloud_init = self.param('cloud_init')
+        cloud_init_nics = self.param('cloud_init_nics') or []
+        if cloud_init is not None:
+            cloud_init_nics.append(cloud_init)
+
+        if cloud_init or cloud_init_nics:
+            self._initialization = otypes.Initialization(
+                nic_configurations=[
+                    otypes.NicConfiguration(
+                        boot_protocol=otypes.BootProtocol(
+                            nic.pop('nic_boot_protocol').lower()
+                        ) if nic.get('nic_boot_protocol') else None,
+                        name=nic.pop('nic_name', None),
+                        on_boot=nic.pop('nic_on_boot', None),
+                        ip=otypes.Ip(
+                            address=nic.pop('nic_ip_address', None),
+                            netmask=nic.pop('nic_netmask', None),
+                            gateway=nic.pop('nic_gateway', None),
+                        ) if (
+                            nic.get('nic_gateway') is not None or
+                            nic.get('nic_netmask') is not None or
+                            nic.get('nic_ip_address') is not None
+                        ) else None,
+                    )
+                    for nic in cloud_init_nics
+                    if (
+                        nic.get('nic_gateway') is not None or
+                        nic.get('nic_netmask') is not None or
+                        nic.get('nic_ip_address') is not None or
+                        nic.get('nic_boot_protocol') is not None or
+                        nic.get('nic_on_boot') is not None
+                    )
+                ] if cloud_init_nics else None,
+                **cloud_init
+            )
+        return self._initialization
+
     def update_check(self, entity):
+        template_display = entity.display
         return (
+            not self.param('cloud_init_persist') and
             equal(self._module.params.get('cluster'), get_link_name(self._connection, entity.cluster)) and
             equal(self._module.params.get('description'), entity.description) and
             equal(self.param('operating_system'), str(entity.os.type)) and
             equal(self.param('name'), str(entity.name)) and
+            equal(self.param('smartcard_enabled'), getattr(template_display, 'smartcard_enabled', False)) and
+            equal(self.param('soundcard_enabled'), entity.soundcard_enabled) and
+            equal(self.param('ballooning_enabled'), entity.memory_policy.ballooning) and
+            equal(self.param('sso'), True if entity.sso.methods else False) and
+            equal(self.param('timezone'), getattr(entity.time_zone, 'name', None)) and
+            equal(self.param('usb_support'), entity.usb.enabled) and
             equal(convert_to_bytes(self.param('memory_guaranteed')), entity.memory_policy.guaranteed) and
             equal(convert_to_bytes(self.param('memory_max')), entity.memory_policy.max) and
             equal(convert_to_bytes(self.param('memory')), entity.memory) and
@@ -524,8 +842,12 @@ def main():
         id=dict(default=None),
         name=dict(default=None),
         vm=dict(default=None),
+        timezone=dict(type='str'),
         description=dict(default=None),
+        sso=dict(type='bool'),
+        ballooning_enabled=dict(type='bool', default=None),
         cluster=dict(default=None),
+        usb_support=dict(type='bool'),
         allow_partial_import=dict(default=None, type='bool'),
         cpu_profile=dict(default=None),
         clone_permissions=dict(type='bool'),
@@ -534,6 +856,8 @@ def main():
         exclusive=dict(type='bool'),
         clone_name=dict(default=None),
         image_provider=dict(default=None),
+        soundcard_enabled=dict(type='bool', default=None),
+        smartcard_enabled=dict(type='bool', default=None),
         image_disk=dict(default=None, aliases=['glance_image_disk_name']),
         io_threads=dict(type='int', default=None),
         template_image_disk_name=dict(default=None),
@@ -547,6 +871,10 @@ def main():
         memory=dict(type='str'),
         memory_guaranteed=dict(type='str'),
         memory_max=dict(type='str'),
+        nics=dict(type='list', default=[]),
+        cloud_init=dict(type='dict'),
+        cloud_init_nics=dict(type='list', default=[]),
+        cloud_init_persist=dict(type='bool', default=False, aliases=['sysprep_persist']),
     )
     module = AnsibleModule(
         argument_spec=argument_spec,


Code review comments:
diff_hunk: @@ -189,6 +189,147 @@
             - Name for importing Template from storage domain.
             - If not defined, C(name) will be used.
         version_added: "2.8"
+    usb_support:
+        description:
+            - "I(True) enable USB support, I(False) to disable it. By default is chosen by oVirt/RHV engine."
+        type: bool
+        version_added: "2.9"
+    timezone:
+        description:
+            - Sets time zone offset of the guest hardware clock.
+            - For example C(Etc/GMT)
+        version_added: "2.9"
+    sso:
+        description:
+            - "I(True) enable Single Sign On by Guest Agent, I(False) to disable it. By default is chosen by oVirt/RHV engine."
+        type: bool
+        version_added: "2.9"
+    soundcard_enabled:
+        description:
+            - "If I(true), the sound card is added to the virtual machine."
+        type: bool
+        version_added: "2.9"
+    smartcard_enabled:
+        description:
+            - "If I(true), use smart card authentication."
+        type: bool
+        version_added: "2.9"
+    cloud_init:
+        description:
+            - Dictionary with values for Unix-like Virtual Machine initialization using cloud init.
+        suboptions:
+            host_name:
+                description:
+                    - Hostname to be set to Virtual Machine when deployed.
+            timezone:
+                description:
+                    - Timezone to be set to Virtual Machine when deployed.
+            user_name:
+                description:
+                    - Username to be used to set password to Virtual Machine when deployed.
+            root_password:
+                description:
+                    - Password to be set for user specified by C(user_name) parameter.
+            authorized_ssh_keys:
+                description:
+                    - Use this SSH keys to login to Virtual Machine.
+            regenerate_ssh_keys:
+                description:
+                    - If I(True) SSH keys will be regenerated on Virtual Machine.
+                type: bool
+            custom_script:
+                description:
+                    - Cloud-init script which will be executed on Virtual Machine when deployed.
+                    - This is appended to the end of the cloud-init script generated by any other options.
+            dns_servers:
+                description:
+                    - DNS servers to be configured on Virtual Machine.
+            dns_search:
+                description:
+                    - DNS search domains to be configured on Virtual Machine.
+            nic_boot_protocol:
+                description:
+                    - Set boot protocol of the network interface of Virtual Machine.
+                choices: ['none', 'dhcp', 'static']
+            nic_ip_address:
+                description:
+                    - If boot protocol is static, set this IP address to network interface of Virtual Machine.
+            nic_netmask:
+                description:
+                    - If boot protocol is static, set this netmask to network interface of Virtual Machine.
+            nic_gateway:
+                description:
+                    - If boot protocol is static, set this gateway to network interface of Virtual Machine.
+            nic_name:
+                description:
+                    - Set name to network interface of Virtual Machine.
+            nic_on_boot:
+                description:
+                    - If I(True) network interface will be set to start on boot.
+                type: bool
+        version_added: "2.9"
+    cloud_init_nics:
+        description:
+            - List of dictionaries representing network interfaces to be setup by cloud init.
+            - This option is used, when user needs to setup more network interfaces via cloud init.
+            - If one network interface is enough, user should use C(cloud_init) I(nic_*) parameters. C(cloud_init) I(nic_*) parameters
+              are merged with C(cloud_init_nics) parameters.
+        suboptions:
+            nic_boot_protocol:
+                description:
+                    - Set boot protocol of the network interface of Virtual Machine. Can be one of C(none), C(dhcp) or C(static).
+            nic_ip_address:
+                description:
+                    - If boot protocol is static, set this IP address to network interface of Virtual Machine.
+            nic_netmask:
+                description:
+                    - If boot protocol is static, set this netmask to network interface of Virtual Machine.
+            nic_gateway:
+                description:
+                    - If boot protocol is static, set this gateway to network interface of Virtual Machine.
+            nic_name:
+                description:
+                    - Set name to network interface of Virtual Machine.
+            nic_on_boot:
+                description:
+                    - If I(True) network interface will be set to start on boot.
+                type: bool
+        version_added: "2.9"
+    ballooning_enabled:
+        description:
+            - "If I(true), use memory ballooning."
+            - "Memory balloon is a guest device, which may be used to re-distribute / reclaim the host memory
+               based on VM needs in a dynamic way. In this way it's possible to create memory over commitment states."
+        type: bool
+        version_added: "2.9"
+    nics:
+        description:
+            - List of NICs, which should be attached to Virtual Machine. NIC is described by following dictionary.
+        suboptions:
+            name:
+                description:
+                    - Name of the NIC.
+            profile_name:
+                description:
+                    - Profile name where NIC should be attached.
+            interface:
+                description:
+                    - Type of the network interface.
+                choices: ['virtio', 'e1000', 'rtl8139']
+                default: 'virtio'
+            mac_address:
+                description:
+                    - Custom MAC address of the network interface, by default it's obtained from MAC pool.
+                    - "NOTE - This parameter is used only when C(state) is I(running) or I(present) and is able to only create NICs.
+                    To manage NICs of the VM in more depth please use M(ovirt_nics) module instead."
+        version_added: "2.9"
+    cloud_init_persist:
path: lib/ansible/modules/cloud/ovirt/ovirt_template.py
text: @user1:
It doesn't make sense in case of template as there is no run-once, so plaese remove it.