From 999a6488cffa61fc7ff06197ed6c6470b5e13de4 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Wed, 21 Sep 2016 18:48:05 +0200 Subject: [PATCH] Ovirt auth/vms/disks modules (#2836) * Add oVirt module to manage VMs This patch add oVirt module to manage Virtual Machines * Add oVirt module to manage authentication This patch add oVirt module to manage authentication * Add oVirt module to manage disks * Added VM state management and fixups --- .../modules/extras/cloud/ovirt/__init__.py | 0 .../modules/extras/cloud/ovirt/ovirt_auth.py | 233 +++++ .../modules/extras/cloud/ovirt/ovirt_disks.py | 316 +++++++ .../modules/extras/cloud/ovirt/ovirt_vms.py | 804 ++++++++++++++++++ 4 files changed, 1353 insertions(+) create mode 100644 lib/ansible/modules/extras/cloud/ovirt/__init__.py create mode 100644 lib/ansible/modules/extras/cloud/ovirt/ovirt_auth.py create mode 100644 lib/ansible/modules/extras/cloud/ovirt/ovirt_disks.py create mode 100644 lib/ansible/modules/extras/cloud/ovirt/ovirt_vms.py diff --git a/lib/ansible/modules/extras/cloud/ovirt/__init__.py b/lib/ansible/modules/extras/cloud/ovirt/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/extras/cloud/ovirt/ovirt_auth.py b/lib/ansible/modules/extras/cloud/ovirt/ovirt_auth.py new file mode 100644 index 00000000000..22abf1d8394 --- /dev/null +++ b/lib/ansible/modules/extras/cloud/ovirt/ovirt_auth.py @@ -0,0 +1,233 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk +except ImportError: + pass + + +DOCUMENTATION = ''' +--- +module: ovirt_auth +short_description: "Module to manage authentication to oVirt." +author: "Ondra Machacek (@machacekondra)" +version_added: "2.2" +description: + - "This module authenticates to oVirt engine and creates SSO token, which should be later used in + all other oVirt modules, so all modules don't need to perform login and logout. + This module returns an Ansible fact called I(ovirt_auth). Every module can use this + fact as C(auth) parameter, to perform authentication." +options: + state: + default: present + choices: ['present', 'absent'] + description: + - "Specifies if a token should be created or revoked." + username: + required: True + description: + - "The name of the user. For example: I(admin@internal)." + password: + required: True + description: + - "The password of the user." + url: + required: True + description: + - "A string containing the base URL of the server. + For example: I(https://server.example.com/ovirt-engine/api)." + insecure: + required: False + description: + - "A boolean flag that indicates if the server TLS certificate and host name should be checked." + ca_file: + required: False + description: + - "A PEM file containing the trusted CA certificates. The + certificate presented by the server will be verified using these CA + certificates. If C(ca_file) parameter is not set, system wide + CA certificate store is used." + timeout: + required: False + description: + - "The maximum total time to wait for the response, in + seconds. A value of zero (the default) means wait forever. If + the timeout expires before the response is received an exception + will be raised." + compress: + required: False + description: + - "A boolean flag indicating if the SDK should ask + the server to send compressed responses. The default is I(True). + Note that this is a hint for the server, and that it may return + uncompressed data even when this parameter is set to I(True)." + kerberos: + required: False + description: + - "A boolean flag indicating if Kerberos authentication + should be used instead of the default basic authentication." +notes: + - "Everytime you use ovirt_auth module to obtain ticket, you need to also revoke the ticket, + when you no longer need it, otherwise the ticket would be revoked by engine when it expires. + For an example of how to achieve that, please take a look at I(examples) section." +''' + +EXAMPLES = ''' +tasks: + - block: + # Create a vault with `ovirt_password` variable which store your + # oVirt user's password, and include that yaml file with variable: + - include_vars: ovirt_password.yml + + # Always be sure to pass 'no_log: true' to ovirt_auth task, + # so the oVirt user's password is not logged: + - name: Obtain SSO token with using username/password credentials: + no_log: true + ovirt_auth: + url: https://ovirt.example.com/ovirt-engine/api + username: admin@internal + ca_file: ca.pem + password: "{{ ovirt_password }}" + + # Previous task generated I(ovirt_auth) fact, which you can later use + # in different modules as follows: + - ovirt_vms: + auth: "{{ ovirt_auth }}" + state: absent + name: myvm + + always: + - name: Always revoke the SSO token + ovirt_auth: + state: absent + ovirt_auth: {{ ovirt_auth }} +''' + +RETURN = ''' +ovirt_auth: + description: Authentication facts, needed to perform authentication to oVirt. + returned: success + type: dictionary + contains: + token: + description: SSO token which is used for connection to oVirt engine. + returned: success + type: string + sample: "kdfVWp9ZgeewBXV-iq3Js1-xQJZPSEQ334FLb3eksoEPRaab07DhZ8ED8ghz9lJd-MQ2GqtRIeqhvhCkrUWQPw" + url: + description: URL of the oVirt engine API endpoint. + returned: success + type: string + sample: "https://ovirt.example.com/ovirt-engine/api" + ca_file: + description: CA file, which is used to verify SSL/TLS connection. + returned: success + type: string + sample: "ca.pem" + insecure: + description: Flag indicating if insecure connection is used. + returned: success + type: bool + sample: False + timeout: + description: Number of seconds to wait for response. + returned: success + type: int + sample: 0 + compress: + description: Flag indicating if compression is used for connection. + returned: success + type: bool + sample: True + kerberos: + description: Flag indicating if kerberos is used for authentication. + returned: success + type: bool + sample: False +''' + + +def main(): + module = AnsibleModule( + argument_spec=dict( + url=dict(default=None), + username=dict(default=None), + password=dict(default=None), + ca_file=dict(default=None), + insecure=dict(required=False, type='bool', default=False), + timeout=dict(required=False, type='int', default=0), + compress=dict(required=False, type='bool', default=True), + kerberos=dict(required=False, type='bool', default=False), + state=dict(default='present', choices=['present', 'absent']), + ovirt_auth=dict(required=None, type='dict'), + ), + required_if=[ + ('state', 'absent', ['ovirt_auth']), + ('state', 'present', ['username', 'password', 'url']), + ], + ) + check_sdk(module) + + state = module.params.get('state') + if state == 'present': + params = module.params + elif state == 'absent': + params = module.params['ovirt_auth'] + + connection = sdk.Connection( + url=params.get('url'), + username=params.get('username'), + password=params.get('password'), + ca_file=params.get('ca_file'), + insecure=params.get('insecure'), + timeout=params.get('timeout'), + compress=params.get('compress'), + kerberos=params.get('kerberos'), + token=params.get('token'), + ) + try: + token = connection.authenticate() + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_auth=dict( + token=token, + url=params.get('url'), + ca_file=params.get('ca_file'), + insecure=params.get('insecure'), + timeout=params.get('timeout'), + compress=params.get('compress'), + kerberos=params.get('kerberos'), + ) if state == 'present' else dict() + ) + ) + except Exception as e: + module.fail_json(msg="Error: %s" % e) + finally: + # Close the connection, but don't revoke token + connection.close(logout=state == 'absent') + + +from ansible.module_utils.basic import * +from ansible.module_utils.ovirt import * +if __name__ == "__main__": + main() diff --git a/lib/ansible/modules/extras/cloud/ovirt/ovirt_disks.py b/lib/ansible/modules/extras/cloud/ovirt/ovirt_disks.py new file mode 100644 index 00000000000..a8f84c26e83 --- /dev/null +++ b/lib/ansible/modules/extras/cloud/ovirt/ovirt_disks.py @@ -0,0 +1,316 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.ovirt import * + + +DOCUMENTATION = ''' +--- +module: ovirt_disks +short_description: "Module to manage Virtual Machine and floating disks in oVirt." +version_added: "2.2" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage Virtual Machine and floating disks in oVirt." +options: + id: + description: + - "ID of the disk to manage. Either C(id) or C(name) is required." + name: + description: + - "Name of the disk to manage. Either C(id) or C(name)/C(alias) is required." + aliases: ['alias'] + vm_name: + description: + - "Name of the Virtual Machine to manage. Either C(vm_id) or C(vm_name) is required if C(state) is I(attached) or I(detached)." + vm_id: + description: + - "ID of the Virtual Machine to manage. Either C(vm_id) or C(vm_name) is required if C(state) is I(attached) or I(detached)." + state: + description: + - "Should the Virtual Machine disk be present/absent/attached/detached." + choices: ['present', 'absent', 'attached', 'detached'] + default: 'present' + size: + description: + - "Size of the disk. Size should be specified using IEC standard units. For example 10GiB, 1024MiB, etc." + interface: + description: + - "Driver of the storage interface." + choices: ['virtio', 'ide', 'virtio_scsi'] + default: 'virtio' + format: + description: + - "Format of the disk. Either copy-on-write or raw." + choices: ['raw', 'cow'] + storage_domain: + description: + - "Storage domain name where disk should be created. By default storage is chosen by oVirt engine." + profile: + description: + - "Disk profile name to be attached to disk. By default profile is chosen by oVirt engine." + bootable: + description: + - "I(True) if the disk should be bootable. By default when disk is created it isn't bootable." + shareable: + description: + - "I(True) if the disk should be shareable. By default when disk is created it isn't shareable." + logical_unit: + description: + - "Dictionary which describes LUN to be directly attached to VM:" + - "C(address) - Address of the storage server. Used by iSCSI." + - "C(port) - Port of the storage server. Used by iSCSI." + - "C(target) - iSCSI target." + - "C(lun_id) - LUN id." + - "C(username) - CHAP Username to be used to access storage server. Used by iSCSI." + - "C(password) - CHAP Password of the user to be used to access storage server. Used by iSCSI." + - "C(storage_type) - Storage type either I(fcp) or I(iscsi)." +extends_documentation_fragment: ovirt +''' + + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create and attach new disk to VM +- ovirt_disks: + name: myvm_disk + vm_name: rhel7 + size: 10GiB + format: cow + interface: virtio + +# Attach logical unit to VM rhel7 +- ovirt_disks: + vm_name: rhel7 + logical_unit: + target: iqn.2016-08-09.brq.str-01:omachace + id: 1IET_000d0001 + address: 10.34.63.204 + interface: virtio + +# Detach disk from VM +- ovirt_disks: + state: detached + name: myvm_disk + vm_name: rhel7 + size: 10GiB + format: cow + interface: virtio +''' + + +RETURN = ''' +id: + description: "ID of the managed disk" + returned: "On success if disk is found." + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +disk: + description: "Dictionary of all the disk attributes. Disk attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/disk." + returned: "On success if disk is found and C(vm_id) or C(vm_name) wasn't passed." + +disk_attachment: + description: "Dictionary of all the disk attachment attributes. Disk attachment attributes can be found + on your oVirt instance at following url: + https://ovirt.example.com/ovirt-engine/api/model#types/disk_attachment." + returned: "On success if disk is found and C(vm_id) or C(vm_name) was passed and VM was found." +''' + + + +def _search_by_lun(disks_service, lun_id): + """ + Find disk by LUN ID. + """ + res = [ + disk for disk in disks_service.list(search='disk_type=lun') if ( + disk.lun_storage.id == lun_id + ) + ] + return res[0] if res else None + + +class DisksModule(BaseModule): + + def build_entity(self): + logical_unit = self._module.params.get('logical_unit') + return otypes.Disk( + id=self._module.params.get('id'), + name=self._module.params.get('name'), + description=self._module.params.get('description'), + format=otypes.DiskFormat( + self._module.params.get('format') + ) if self._module.params.get('format') else None, + provisioned_size=convert_to_bytes( + self._module.params.get('size') + ), + storage_domains=[ + otypes.StorageDomain( + name=self._module.params.get('storage_domain'), + ), + ], + shareable=self._module.params.get('shareable'), + lun_storage=otypes.HostStorage( + type=otypes.StorageType( + logical_unit.get('storage_type', 'iscsi') + ), + logical_units=[ + otypes.LogicalUnit( + address=logical_unit.get('address'), + port=logical_unit.get('port', 3260), + target=logical_unit.get('target'), + id=logical_unit.get('id'), + username=logical_unit.get('username'), + password=logical_unit.get('password'), + ) + ], + ) if logical_unit else None, + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('description'), entity.description) and + equal(convert_to_bytes(self._module.params.get('size')), entity.provisioned_size) and + equal(self._module.params.get('format'), str(entity.format)) and + equal(self._module.params.get('shareable'), entity.shareable) + ) + + +class DiskAttachmentsModule(DisksModule): + + def build_entity(self): + return otypes.DiskAttachment( + disk=super(DiskAttachmentsModule, self).build_entity(), + interface=otypes.DiskInterface( + self._module.params.get('interface') + ) if self._module.params.get('interface') else None, + bootable=self._module.params.get('bootable'), + active=True, + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('interface'), str(entity.interface)) and + equal(self._module.params.get('bootable'), entity.bootable) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent', 'attached', 'detached'], + default='present' + ), + id=dict(default=None), + name=dict(default=None, aliases=['alias']), + vm_name=dict(default=None), + vm_id=dict(default=None), + size=dict(default=None), + interface=dict(default=None,), + allocation_policy=dict(default=None), + storage_domain=dict(default=None), + profile=dict(default=None), + format=dict(default=None, choices=['raw', 'cow']), + bootable=dict(default=None, type='bool'), + shareable=dict(default=None, type='bool'), + logical_unit=dict(default=None, type='dict'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + disk = None + state = module.params['state'] + connection = create_connection(module.params.pop('auth')) + disks_service = connection.system_service().disks_service() + disks_module = DisksModule( + connection=connection, + module=module, + service=disks_service, + ) + + lun = module.params.get('logical_unit') + if lun: + disk = _search_by_lun(disks_service, lun.get('id')) + + ret = None + # First take care of creating the VM, if needed: + if state == 'present' or state == 'detached' or state == 'attached': + ret = disks_module.create( + entity=disk, + result_state=otypes.DiskStatus.OK if lun is None else None, + ) + # We need to pass ID to the module, so in case we want detach/attach disk + # we have this ID specified to attach/detach method: + module.params['id'] = ret['id'] if disk is None else disk.id + elif state == 'absent': + ret = disks_module.remove() + + # If VM was passed attach/detach disks to/from the VM: + if 'vm_id' in module.params or 'vm_name' in module.params and state != 'absent': + vms_service = connection.system_service().vms_service() + + # If `vm_id` isn't specified, find VM by name: + vm_id = module.params['vm_id'] + if vm_id is None: + vm_id = getattr(search_by_name(vms_service, module.params['vm_name']), 'id', None) + + if vm_id is None: + module.fail_json( + msg="VM don't exists, please create it first." + ) + + disk_attachments_service = vms_service.vm_service(vm_id).disk_attachments_service() + disk_attachments_module = DiskAttachmentsModule( + connection=connection, + module=module, + service=disk_attachments_service, + changed=ret['changed'] if ret else False, + ) + + if state == 'present' or state == 'attached': + ret = disk_attachments_module.create() + elif state == 'detached': + ret = disk_attachments_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e)) + finally: + connection.close(logout=False) + + +from ansible.module_utils.basic import * +if __name__ == "__main__": + main() diff --git a/lib/ansible/modules/extras/cloud/ovirt/ovirt_vms.py b/lib/ansible/modules/extras/cloud/ovirt/ovirt_vms.py new file mode 100644 index 00000000000..8aa4a98364b --- /dev/null +++ b/lib/ansible/modules/extras/cloud/ovirt/ovirt_vms.py @@ -0,0 +1,804 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.ovirt import * + + +DOCUMENTATION = ''' +--- +module: ovirt_vms +short_description: "Module to manage Virtual Machines in oVirt." +version_added: "2.2" +author: "Ondra Machacek (@machacekondra)" +description: + - "This module manages whole lifecycle of the Virtual Machine(VM) in oVirt. Since VM can hold many states in oVirt, + this see notes to see how the states of the VM are handled." +options: + name: + description: + - "Name of the the Virtual Machine to manage. If VM don't exists C(name) is required. + Otherwise C(id) or C(name) can be used." + id: + description: + - "ID of the the Virtual Machine to manage." + state: + description: + - "Should the Virtual Machine be running/stopped/present/absent/suspended/next_run." + - "I(present) and I(running) are equal states." + - "I(next_run) state updates the VM and if the VM has next run configuration it will be rebooted." + - "Please check I(notes) to more detailed description of states." + choices: ['running', 'stopped', 'present', 'absent', 'suspended', 'next_run'] + default: present + cluster: + description: + - "Name of the cluster, where Virtual Machine should be created. Required if creating VM." + template: + description: + - "Name of the template, which should be used to create Virtual Machine. Required if creating VM." + - "If template is not specified and VM doesn't exist, VM will be created from I(Blank) template." + memory: + description: + - "Amount of memory of the Virtual Machine. Prefix uses IEC 60027-2 standard (for example 1GiB, 1024MiB)." + - "Default value is set by engine." + memory_guaranteed: + description: + - "Amount of minimal guaranteed memory of the Virtual Machine. + Prefix uses IEC 60027-2 standard (for example 1GiB, 1024MiB)." + - "C(memory_guaranteed) parameter can't be lower than C(memory) parameter. Default value is set by engine." + cpu_shares: + description: + - "Set a CPU shares for this Virtual Machine. Default value is set by oVirt engine." + cpu_cores: + description: + - "Number of virtual CPUs cores of the Virtual Machine. Default value is set by oVirt engine." + cpu_sockets: + description: + - "Number of virtual CPUs sockets of the Virtual Machine. Default value is set by oVirt engine." + type: + description: + - "Type of the Virtual Machine. Default value is set by oVirt engine." + choices: [server, desktop] + operating_system: + description: + - "Operating system of the Virtual Machine. Default value is set by oVirt engine." + choices: [ + rhel_6_ppc64, other, freebsd, windows_2003x64, windows_10, rhel_6x64, rhel_4x64, windows_2008x64, + windows_2008R2x64, debian_7, windows_2012x64, ubuntu_14_04, ubuntu_12_04, ubuntu_13_10, windows_8x64, + other_linux_ppc64, windows_2003, other_linux, windows_10x64, windows_2008, rhel_3, rhel_5, rhel_4, + other_ppc64, sles_11, rhel_6, windows_xp, rhel_7x64, freebsdx64, rhel_7_ppc64, windows_7, rhel_5x64, + ubuntu_14_04_ppc64, sles_11_ppc64, windows_8, windows_2012R2x64, windows_2008r2x64, ubuntu_13_04, + ubuntu_12_10, windows_7x64 + ] + boot_devices: + description: + - "List of boot devices which should be used to boot. Choices I(network), I(hd) and I(cdrom)." + - "For example: ['cdrom', 'hd']. Default value is set by oVirt engine." + host: + description: + - "Specify host where Virtual Machine should be running. By default the host is chosen by engine scheduler." + - "This parameter is used only when C(state) is I(running) or I(present)." + high_availability: + description: + - "If I(True) Virtual Machine will be set as highly available." + - "If I(False) Virtual Machine won't be set as highly available." + - "If no value is passed, default value is set by oVirt engine." + delete_protected: + description: + - "If I(True) Virtual Machine will be set as delete protected." + - "If I(False) Virtual Machine won't be set as delete protected." + - "If no value is passed, default value is set by oVirt engine." + stateless: + description: + - "If I(True) Virtual Machine will be set as stateless." + - "If I(False) Virtual Machine will be unset as stateless." + - "If no value is passed, default value is set by oVirt engine." + clone: + description: + - "If I(True) then the disks of the created virtual machine will be cloned and independent of the template." + - "This parameter is used only when C(state) is I(running) or I(present) and VM didn't exist before." + default: False + clone_permissions: + description: + - "If I(True) then the permissions of the template (only the direct ones, not the inherited ones) + will be copied to the created virtual machine." + - "This parameter is used only when C(state) is I(running) or I(present) and VM didn't exist before." + default: False + cd_iso: + description: + - "ISO file from ISO storage domain which should be attached to Virtual Machine." + - "If you pass empty string the CD will be ejected from VM." + - "If used with C(state) I(running) or I(present) and VM is running the CD will be attached to VM." + - "If used with C(state) I(running) or I(present) and VM is down the CD will be attached to VM persistently." + force: + description: + - "Please check to I(Synopsis) to more detailed description of force parameter, it can behave differently + in different situations." + default: False + nics: + description: + - "List of NICs, which should be attached to Virtual Machine. NIC is described by following dictionary:" + - "C(name) - Name of the NIC." + - "C(profile_name) - Profile name where NIC should be attached." + - "C(interface) - Type of the network interface. One of following: I(virtio), I(e1000), I(rtl8139), default is I(virtio)." + - "C(mac_address) - Custom MAC address of the network interface, by default it's obtained from MAC pool." + - "C(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." + disks: + description: + - "List of disks, which should be attached to Virtual Machine. Disk is described by following dictionary:" + - "C(name) - Name of the disk. Either C(name) or C(id) is reuqired." + - "C(id) - ID of the disk. Either C(name) or C(id) is reuqired." + - "C(interface) - Interface of the disk, either I(virtio) or I(IDE), default is I(virtio)." + - "C(bootable) - I(True) if the disk should be bootable, default is non bootable." + - "C(activate) - I(True) if the disk should be activated, default is activated." + - "C(Note:)" + - "This parameter is used only when C(state) is I(running) or I(present) and is able to only attach disks. + To manage disks of the VM in more depth please use M(ovirt_disks) module instead." + sysprep: + description: + - "Dictionary with values for Windows Virtual Machine initialization using sysprep:" + - "C(host_name) - Hostname to be set to Virtual Machine when deployed." + - "C(active_directory_ou) - Active Directory Organizational Unit, to be used for login of user." + - "C(org_name) - Organization name to be set to Windows Virtual Machine." + - "C(domain) - Domain to be set to Windows Virtual Machine." + - "C(timezone) - Timezone to be set to Windows Virtual Machine." + - "C(ui_language) - UI language of the Windows Virtual Machine." + - "C(system_locale) - System localization of the Windows Virtual Machine." + - "C(input_locale) - Input localization of the Windows Virtual Machine." + - "C(windows_license_key) - License key to be set to Windows Virtual Machine." + - "C(user_name) - Username to be used for set password to Windows Virtual Machine." + - "C(root_password) - Password to be set for username to Windows Virtual Machine." + cloud_init: + description: + - "Dictionary with values for Unix-like Virtual Machine initialization using cloud init:" + - "C(host_name) - Hostname to be set to Virtual Machine when deployed." + - "C(timezone) - Timezone to be set to Virtual Machine when deployed." + - "C(user_name) - Username to be used to set password to Virtual Machine when deployed." + - "C(root_password) - Password to be set for user specified by C(user_name) parameter." + - "C(authorized_ssh_keys) - Use this SSH keys to login to Virtual Machine." + - "C(regenerate_ssh_keys) - If I(True) SSH keys will be regenerated on Virtual Machine." + - "C(custom_script) - Cloud-init script which will be executed on Virtual Machine when deployed." + - "C(dns_servers) - DNS servers to be configured on Virtual Machine." + - "C(dns_search) - DNS search domains to be configured on Virtual Machine." + - "C(nic_boot_protocol) - Set boot protocol of the network interface of Virtual Machine. Can be one of None, DHCP or Static." + - "C(nic_ip_address) - If boot protocol is static, set this IP address to network interface of Virtual Machine." + - "C(nic_netmask) - If boot protocol is static, set this netmask to network interface of Virtual Machine." + - "C(nic_gateway) - If boot protocol is static, set this gateway to network interface of Virtual Machine." + - "C(nic_name) - Set name to network interface of Virtual Machine." + - "C(nic_on_boot) - If I(True) network interface will be set to start on boot." +notes: + - "If VM is in I(UNASSIGNED) or I(UNKNOWN) state before any operation, the module will fail. + If VM is in I(IMAGE_LOCKED) state before any operation, we try to wait for VM to be I(DOWN). + If VM is in I(SAVING_STATE) state before any operation, we try to wait for VM to be I(SUSPENDED). + If VM is in I(POWERING_DOWN) state before any operation, we try to wait for VM to be I(UP) or I(DOWN). VM can + get into I(UP) state from I(POWERING_DOWN) state, when there is no ACPI or guest agent running inside VM, or + if the shutdown operation fails. + When user specify I(UP) C(state), we always wait to VM to be in I(UP) state in case VM is I(MIGRATING), + I(REBOOTING), I(POWERING_UP), I(RESTORING_STATE), I(WAIT_FOR_LAUNCH). In other states we run start operation on VM. + When user specify I(stopped) C(state), and If user pass C(force) parameter set to I(true) we forcibly stop the VM in + any state. If user don't pass C(force) parameter, we always wait to VM to be in UP state in case VM is + I(MIGRATING), I(REBOOTING), I(POWERING_UP), I(RESTORING_STATE), I(WAIT_FOR_LAUNCH). If VM is in I(PAUSED) or + I(SUSPENDED) state, we start the VM. Then we gracefully shutdown the VM. + When user specify I(suspended) C(state), we always wait to VM to be in UP state in case VM is I(MIGRATING), + I(REBOOTING), I(POWERING_UP), I(RESTORING_STATE), I(WAIT_FOR_LAUNCH). If VM is in I(PAUSED) or I(DOWN) state, + we start the VM. Then we suspend the VM. + When user specify I(absent) C(state), we forcibly stop the VM in any state and remove it." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Creates a new Virtual Machine from template named 'rhel7_template' +ovirt_vms: + state: present + name: myvm + template: rhel7_template + +# Creates a new server rhel7 Virtual Machine from Blank template +# on brq01 cluster with 2GiB memory and 2 vcpu cores/sockets +# and attach bootable disk with name rhel7_disk and attach virtio NIC +ovirt_vms: + state: present + cluster: brq01 + name: myvm + memory: 2GiB + cpu_cores: 2 + cpu_sockets: 2 + cpu_shares: 1024 + type: server + operating_system: rhel_7x64 + disks: + - name: rhel7_disk + bootable: True + nics: + - name: nic1 + +# Run VM with cloud init: +ovirt_vms: + name: rhel7 + template: rhel7 + cluster: Default + memory: 1GiB + high_availability: true + 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 + +# Run VM with sysprep: +ovirt_vms: + name: windows2012R2_AD + template: windows2012R2 + cluster: Default + memory: 3GiB + high_availability: true + sysprep: + host_name: windowsad.example.com + user_name: Administrator + root_password: SuperPassword123 + +# Migrate/Run VM to/on host named 'host1' +ovirt_vms: + state: running + name: myvm + host: host1 + +# Change Vm's CD: +ovirt_vms: + name: myvm + cd_iso: drivers.iso + +# Eject Vm's CD: +ovirt_vms: + name: myvm + cd_iso: '' + +# Boot VM from CD: +ovirt_vms: + name: myvm + cd_iso: centos7_x64.iso + boot_devices: + - cdrom + +# Stop vm: +ovirt_vms: + state: stopped + name: myvm + +# Upgrade memory to already created VM: +ovirt_vms: + name: myvm + memory: 4GiB + +# Hot plug memory to already created and running VM: +# (VM won't be restarted) +ovirt_vms: + name: myvm + memory: 4GiB + +# When change on the VM needs restart of the VM, use next_run state, +# The VM will be updated and rebooted if there are any changes. +# If present state would be used, VM won't be restarted. +ovirt_vms: + state: next_run + name: myvm + boot_devices: + - network + +# Remove VM, if VM is running it will be stopped: +ovirt_vms: + state: absent + name: myvm +''' + + +RETURN = ''' +id: + description: ID of the VM which is managed + returned: On success if VM is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +vm: + description: "Dictionary of all the VM attributes. VM attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/vm." + returned: On success if VM is found. +''' + + +class VmsModule(BaseModule): + + def build_entity(self): + return otypes.Vm( + name=self._module.params['name'], + cluster=otypes.Cluster( + name=self._module.params['cluster'] + ) if self._module.params['cluster'] else None, + template=otypes.Template( + name=self._module.params['template'] + ) if self._module.params['template'] else None, + stateless=self._module.params['stateless'], + delete_protected=self._module.params['delete_protected'], + high_availability=otypes.HighAvailability( + enabled=self._module.params['high_availability'] + ) if self._module.params['high_availability'] is not None else None, + cpu=otypes.Cpu( + topology=otypes.CpuTopology( + cores=self._module.params['cpu_cores'], + sockets=self._module.params['cpu_sockets'], + ) + ) if ( + self._module.params['cpu_cores'] or self._module.params['cpu_sockets'] + ) else None, + cpu_shares=self._module.params['cpu_shares'], + os=otypes.OperatingSystem( + type=self._module.params['operating_system'], + boot=otypes.Boot( + devices=[ + otypes.BootDevice(dev) for dev in self._module.params['boot_devices'] + ], + ) if self._module.params['boot_devices'] else None, + ) if ( + self._module.params['operating_system'] or self._module.params['boot_devices'] + ) else None, + type=otypes.VmType( + self._module.params['type'] + ) if self._module.params['type'] else None, + memory=convert_to_bytes( + self._module.params['memory'] + ) if self._module.params['memory'] else None, + memory_policy=otypes.MemoryPolicy( + guaranteed=convert_to_bytes(self._module.params['memory_guaranteed']), + ) if self._module.params['memory_guaranteed'] else None, + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('cluster'), get_link_name(self._connection, entity.cluster)) and + equal(convert_to_bytes(self._module.params['memory']), entity.memory) and + equal(convert_to_bytes(self._module.params['memory_guaranteed']), entity.memory_policy.guaranteed) and + equal(self._module.params.get('cpu_cores'), entity.cpu.topology.cores) and + equal(self._module.params.get('cpu_sockets'), entity.cpu.topology.sockets) and + equal(self._module.params.get('type'), str(entity.type)) and + equal(self._module.params.get('operating_system'), str(entity.os.type)) and + equal(self._module.params.get('high_availability'), entity.high_availability.enabled) and + equal(self._module.params.get('stateless'), entity.stateless) and + equal(self._module.params.get('cpu_shares'), entity.cpu_shares) and + equal(self._module.params.get('delete_protected'), entity.delete_protected) and + equal(self._module.params.get('boot_devices'), [str(dev) for dev in getattr(entity.os, 'devices', [])]) + ) + + def pre_create(self, entity): + # If VM don't exists, and template is not specified, set it to Blank: + if entity is None: + if self._module.params.get('template') is None: + self._module.params['template'] = 'Blank' + + def post_update(self, entity): + self.post_create(entity) + + def post_create(self, entity): + # After creation of the VM, attach disks and NICs: + self.changed = self.__attach_disks(entity) + self.changed = self.__attach_nics(entity) + + def pre_remove(self, entity): + # Forcibly stop the VM, if it's not in DOWN state: + if entity.status != otypes.VmStatus.DOWN: + if not self._module.check_mode: + self.changed = self.action( + action='stop', + action_condition=lambda vm: vm.status != otypes.VmStatus.DOWN, + wait_condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + )['changed'] + + def __suspend_shutdown_common(self, vm_service): + if vm_service.get().status in [ + otypes.VmStatus.MIGRATING, + otypes.VmStatus.POWERING_UP, + otypes.VmStatus.REBOOT_IN_PROGRESS, + otypes.VmStatus.WAIT_FOR_LAUNCH, + otypes.VmStatus.UP, + otypes.VmStatus.RESTORING_STATE, + ]: + self._wait_for_UP(vm_service) + + def _pre_shutdown_action(self, entity): + vm_service = self._service.vm_service(entity.id) + self.__suspend_shutdown_common(vm_service) + if entity.status in [otypes.VmStatus.SUSPENDED, otypes.VmStatus.PAUSED]: + vm_service.start() + self._wait_for_UP(vm_service) + return vm_service.get() + + def _pre_suspend_action(self, entity): + vm_service = self._service.vm_service(entity.id) + self.__suspend_shutdown_common(vm_service) + if entity.status in [otypes.VmStatus.PAUSED, otypes.VmStatus.DOWN]: + vm_service.start() + self._wait_for_UP(vm_service) + return vm_service.get() + + def _post_start_action(self, entity): + vm_service = self._service.service(entity.id) + self._wait_for_UP(vm_service) + self._attach_cd(vm_service.get()) + self._migrate_vm(vm_service.get()) + + def _attach_cd(self, entity): + cd_iso = self._module.params['cd_iso'] + if cd_iso is not None: + vm_service = self._service.service(entity.id) + current = vm_service.get().status == otypes.VmStatus.UP + cdroms_service = vm_service.cdroms_service() + cdrom_device = cdroms_service.list()[0] + cdrom_service = cdroms_service.cdrom_service(cdrom_device.id) + cdrom = cdrom_service.get(current=current) + if getattr(cdrom.file, 'id', '') != cd_iso: + if not self._module.check_mode: + cdrom_service.update( + cdrom=otypes.Cdrom( + file=otypes.File(id=cd_iso) + ), + current=current, + ) + self.changed = True + + return entity + + def _migrate_vm(self, entity): + vm_host = self._module.params['host'] + vm_service = self._service.vm_service(entity.id) + if vm_host is not None: + # In case VM is preparing to be UP, wait to be up, to migrate it: + if entity.status == otypes.VmStatus.UP: + hosts_service = self._connection.system_service().hosts_service() + current_vm_host = hosts_service.host_service(entity.host.id).get().name + if vm_host != current_vm_host: + if not self._module.check_mode: + vm_service.migrate(host=otypes.Host(name=vm_host)) + self._wait_for_UP(vm_service) + self.changed = True + + return entity + + def _wait_for_UP(self, vm_service): + wait( + service=vm_service, + condition=lambda vm: vm.status == otypes.VmStatus.UP, + wait=self._module.params['wait'], + timeout=self._module.params['timeout'], + ) + + def __attach_disks(self, entity): + disks_service = self._connection.system_service().disks_service() + + for disk in self._module.params['disks']: + # If disk ID is not specified, find disk by name: + disk_id = disk.get('id') + if disk_id is None: + disk_id = getattr( + search_by_name( + service=disks_service, + name=disk.get('name') + ), + 'id', + None + ) + + # Attach disk to VM: + disk_attachments_service = self._service.service(entity.id).disk_attachments_service() + if disk_attachments_service.attachment_service(disk_id).get() is None: + if not self._module.check_mode: + disk_attachments_service.add( + otypes.DiskAttachment( + disk=otypes.Disk( + id=disk_id, + active=disk.get('activate', True), + ), + interface=otypes.DiskInterface( + disk.get('interface', 'virtio') + ), + bootable=disk.get('bootable', False), + ) + ) + self.changed = True + + def __attach_nics(self, entity): + # Attach NICs to VM, if specified: + vnic_profiles_service = self._connection.system_service().vnic_profiles_service() + nics_service = self._service.service(entity.id).nics_service() + for nic in self._module.params['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=search_by_name( + vnic_profiles_service, + nic.get('profile_name'), + ).id + ) 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(sysprep, cloud_init): + initialization = None + if cloud_init: + initialization = otypes.Initialization( + nic_configurations=[ + otypes.NicConfiguration( + boot_protocol=otypes.BootProtocol( + cloud_init.pop('nic_boot_protocol').lower() + ) if cloud_init.get('nic_boot_protocol') else None, + name=cloud_init.pop('nic_name'), + on_boot=cloud_init.pop('nic_on_boot'), + ip=otypes.Ip( + address=cloud_init.pop('nic_ip_address'), + netmask=cloud_init.pop('nic_netmask'), + gateway=cloud_init.pop('nic_gateway'), + ) if ( + cloud_init.get('nic_gateway') is not None or + cloud_init.get('nic_netmask') is not None or + cloud_init.get('nic_ip_address') is not None + ) else None, + ) + ] if ( + cloud_init.get('nic_gateway') is not None or + cloud_init.get('nic_netmask') is not None or + cloud_init.get('nic_ip_address') is not None or + cloud_init.get('nic_boot_protocol') is not None or + cloud_init.get('nic_on_boot') is not None + ) else None, + **cloud_init + ) + elif sysprep: + initialization = otypes.Initialization( + **sysprep + ) + return initialization + + +def control_state(vm, vms_service, module): + if vm is None: + return + + force = module.params['force'] + state = module.params['state'] + + vm_service = vms_service.vm_service(vm.id) + if vm.status == otypes.VmStatus.IMAGE_LOCKED: + wait( + service=vm_service, + condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + ) + elif vm.status == otypes.VmStatus.SAVING_STATE: + # Result state is SUSPENDED, we should wait to be suspended: + wait( + service=vm_service, + condition=lambda vm: vm.status == otypes.VmStatus.SUSPENDED, + ) + elif ( + vm.status == otypes.VmStatus.UNASSIGNED or + vm.status == otypes.VmStatus.UNKNOWN + ): + # Invalid states: + module.fail_json("Not possible to control VM, if it's in '{}' status".format(vm.status)) + elif vm.status == otypes.VmStatus.POWERING_DOWN: + if (force and state == 'stopped') or state == 'absent': + vm_service.stop() + wait( + service=vm_service, + condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + ) + else: + # If VM is powering down, wait to be DOWN or UP. + # VM can end in UP state in case there is no GA + # or ACPI on the VM or shutdown operation crashed: + wait( + service=vm_service, + condition=lambda vm: vm.status in [otypes.VmStatus.DOWN, otypes.VmStatus.UP], + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['running', 'stopped', 'present', 'absent', 'suspended', 'next_run'], + default='present', + ), + name=dict(default=None), + id=dict(default=None), + cluster=dict(default=None), + template=dict(default=None), + disks=dict(default=[], type='list'), + memory=dict(default=None), + memory_guaranteed=dict(default=None), + cpu_sockets=dict(default=None, type='int'), + cpu_cores=dict(default=None, type='int'), + cpu_shares=dict(default=None, type='int'), + type=dict(choices=['server', 'desktop']), + operating_system=dict( + default=None, + choices=[ + 'rhel_6_ppc64', 'other', 'freebsd', 'windows_2003x64', 'windows_10', + 'rhel_6x64', 'rhel_4x64', 'windows_2008x64', 'windows_2008R2x64', + 'debian_7', 'windows_2012x64', 'ubuntu_14_04', 'ubuntu_12_04', + 'ubuntu_13_10', 'windows_8x64', 'other_linux_ppc64', 'windows_2003', + 'other_linux', 'windows_10x64', 'windows_2008', 'rhel_3', 'rhel_5', + 'rhel_4', 'other_ppc64', 'sles_11', 'rhel_6', 'windows_xp', 'rhel_7x64', + 'freebsdx64', 'rhel_7_ppc64', 'windows_7', 'rhel_5x64', + 'ubuntu_14_04_ppc64', 'sles_11_ppc64', 'windows_8', + 'windows_2012R2x64', 'windows_2008r2x64', 'ubuntu_13_04', + 'ubuntu_12_10', 'windows_7x64', + ], + ), + cd_iso=dict(default=None), + boot_devices=dict(default=None, type='list'), + high_availability=dict(type='bool'), + stateless=dict(type='bool'), + delete_protected=dict(type='bool'), + force=dict(type='bool', default=False), + nics=dict(default=[], type='list'), + cloud_init=dict(type='dict'), + sysprep=dict(type='dict'), + host=dict(default=None), + clone=dict(type='bool', default=False), + clone_permissions=dict(type='bool', default=False), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + state = module.params['state'] + connection = create_connection(module.params.pop('auth')) + vms_service = connection.system_service().vms_service() + vms_module = VmsModule( + connection=connection, + module=module, + service=vms_service, + ) + vm = vms_module.search_entity() + + control_state(vm, vms_service, module) + if state == 'present' or state == 'running' or state == 'next_run': + cloud_init = module.params['cloud_init'] + sysprep = module.params['sysprep'] + + # In case VM don't exist, wait for VM DOWN state, + # otherwise don't wait for any state, just update VM: + vms_module.create( + entity=vm, + result_state=otypes.VmStatus.DOWN if vm is None else None, + clone=module.params['clone'], + clone_permissions=module.params['clone_permissions'], + ) + ret = vms_module.action( + action='start', + post_action=vms_module._post_start_action, + action_condition=lambda vm: ( + vm.status not in [ + otypes.VmStatus.MIGRATING, + otypes.VmStatus.POWERING_UP, + otypes.VmStatus.REBOOT_IN_PROGRESS, + otypes.VmStatus.WAIT_FOR_LAUNCH, + otypes.VmStatus.UP, + otypes.VmStatus.RESTORING_STATE, + ] + ), + wait_condition=lambda vm: vm.status == otypes.VmStatus.UP, + # Start action kwargs: + use_cloud_init=cloud_init is not None, + use_sysprep=sysprep is not None, + vm=otypes.Vm( + placement_policy=otypes.VmPlacementPolicy( + hosts=[otypes.Host(name=module.params['host'])] + ) if module.params['host'] else None, + initialization=_get_initialization(sysprep, cloud_init), + ), + ) + + if state == 'next_run': + # Apply next run configuration, if needed: + vm = vms_service.vm_service(ret['id']).get() + if vm.next_run_configuration_exists: + ret = vms_module.action( + action='reboot', + entity=vm, + action_condition=lambda vm: vm.status == otypes.VmStatus.UP, + wait_condition=lambda vm: vm.status == otypes.VmStatus.UP, + ) + elif state == 'stopped': + vms_module.create( + clone=module.params['clone'], + clone_permissions=module.params['clone_permissions'], + ) + if module.params['force']: + ret = vms_module.action( + action='stop', + post_action=vms_module._attach_cd, + action_condition=lambda vm: vm.status != otypes.VmStatus.DOWN, + wait_condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + ) + else: + ret = vms_module.action( + action='shutdown', + pre_action=vms_module._pre_shutdown_action, + post_action=vms_module._attach_cd, + action_condition=lambda vm: vm.status != otypes.VmStatus.DOWN, + wait_condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + ) + elif state == 'suspended': + vms_module.create( + clone=module.params['clone'], + clone_permissions=module.params['clone_permissions'], + ) + ret = vms_module.action( + action='suspend', + pre_action=vms_module._pre_suspend_action, + action_condition=lambda vm: vm.status != otypes.VmStatus.SUSPENDED, + wait_condition=lambda vm: vm.status == otypes.VmStatus.SUSPENDED, + ) + elif state == 'absent': + ret = vms_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e)) + finally: + connection.close(logout=False) + +from ansible.module_utils.basic import * +if __name__ == "__main__": + main()