From f84f97d035cda8feea7a9469d7b5e8a5f6b3a116 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Wed, 14 Dec 2016 18:43:25 +0100 Subject: [PATCH] cloud: ovirt: Add support to upload/copy/move disks (#19337) --- .../modules/cloud/ovirt/ovirt_disks.py | 201 +++++++++++++++++- 1 file changed, 195 insertions(+), 6 deletions(-) diff --git a/lib/ansible/modules/cloud/ovirt/ovirt_disks.py b/lib/ansible/modules/cloud/ovirt/ovirt_disks.py index e10aa524f90..6a030b78ef4 100644 --- a/lib/ansible/modules/cloud/ovirt/ovirt_disks.py +++ b/lib/ansible/modules/cloud/ovirt/ovirt_disks.py @@ -19,7 +19,18 @@ # along with Ansible. If not, see . # +import os +import time import traceback +import ssl + +from httplib import HTTPSConnection + +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + try: import ovirtsdk4.types as otypes @@ -34,11 +45,12 @@ from ansible.module_utils.ovirt import ( create_connection, convert_to_bytes, equal, + follow_link, ovirt_full_argument_spec, search_by_name, + wait, ) - ANSIBLE_METADATA = {'status': ['preview'], 'supported_by': 'community', 'version': '1.0'} @@ -70,9 +82,22 @@ options: - "Should the Virtual Machine disk be present/absent/attached/detached." choices: ['present', 'absent', 'attached', 'detached'] default: 'present' + image_path: + description: + - "Path to disk image, which should be uploaded." + - "Note that currently we support only compability version 0.10 of the qcow disk." + - "Note that you must have an valid oVirt engine CA in your system trust store + or you must provide it in C(ca_file) parameter." + - "Note that there is no reliable way to achieve idempotency, so + if you want to upload the disk even if the disk with C(id) or C(name) exists, + then please use C(force) I(true). If you will use C(force) I(false), which + is default, then the disk image won't be uploaded." + version_added: "2.3" size: description: - - "Size of the disk. Size should be specified using IEC standard units. For example 10GiB, 1024MiB, etc." + - "Size of the disk. Size should be specified using IEC standard units. + For example 10GiB, 1024MiB, etc." + - "Size can be only increased, not decreased." interface: description: - "Driver of the storage interface." @@ -88,6 +113,19 @@ options: storage_domain: description: - "Storage domain name where disk should be created. By default storage is chosen by oVirt engine." + storage_domains: + description: + - "Storage domain names where disk should be copied." + - "C(**IMPORTANT**)" + - "There is no reliable way to achieve idempotency, so every time + you specify this parameter the disks are copied, so please handle + your playbook accordingly to not copy the disks all the time." + version_added: "2.3" + force: + description: + - "Please take a look at C(image_path) documentation to see the correct + usage of this parameter." + version_added: "2.3" profile: description: - "Disk profile name to be attached to disk. By default profile is chosen by oVirt engine." @@ -140,6 +178,17 @@ EXAMPLES = ''' size: 10GiB format: cow interface: virtio + +# Upload local image to disk and attach it to vm: +# Since Ansible 2.3 +- ovirt_disks: + name: mydisk + vm_name: myvm + interface: virtio + size: 10GiB + format: cow + image_path: /path/to/mydisk.qcow2 + storage_domain: data ''' @@ -174,6 +223,90 @@ def _search_by_lun(disks_service, lun_id): return res[0] if res else None +def upload_disk_image(connection, module): + size = os.path.getsize(module.params['image_path']) + transfers_service = connection.system_service().image_transfers_service() + transfer = transfers_service.add( + otypes.ImageTransfer( + image=otypes.Image( + id=module.params['id'], + ) + ) + ) + transfer_service = transfers_service.image_transfer_service(transfer.id) + + try: + # After adding a new transfer for the disk, the transfer's status will be INITIALIZING. + # Wait until the init phase is over. The actual transfer can start when its status is "Transferring". + while transfer.phase == otypes.ImageTransferPhase.INITIALIZING: + time.sleep(module.params['poll_interval']) + transfer = transfer_service.get() + + # Set needed headers for uploading: + upload_headers = { + 'Authorization': transfer.signed_ticket, + } + + proxy_url = urlparse(transfer.proxy_url) + context = ssl.create_default_context() + auth = module.params['auth'] + if auth.get('insecure'): + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + elif auth.get('ca_file'): + context.load_verify_locations(cafile='ca.pem') + + proxy_connection = HTTPSConnection( + proxy_url.hostname, + proxy_url.port, + context=context, + ) + + with open(module.params['image_path'], "rb") as disk: + chunk_size = 1024 * 1024 * 8 + pos = 0 + while pos < size: + transfer_service.extend() + upload_headers['Content-Range'] = "bytes %d-%d/%d" % (pos, min(pos + chunk_size, size) - 1, size) + proxy_connection.request( + 'PUT', + proxy_url.path, + disk.read(chunk_size), + headers=upload_headers, + ) + r = proxy_connection.getresponse() + if r.status >= 400: + raise Exception("Failed to upload disk image.") + pos += chunk_size + finally: + transfer_service.finalize() + while transfer.phase in [ + otypes.ImageTransferPhase.TRANSFERRING, + otypes.ImageTransferPhase.FINALIZING_SUCCESS, + ]: + time.sleep(module.params['poll_interval']) + transfer = transfer_service.get() + if transfer.phase in [ + otypes.ImageTransferPhase.UNKNOWN, + otypes.ImageTransferPhase.FINISHED_FAILURE, + otypes.ImageTransferPhase.FINALIZING_FAILURE, + otypes.ImageTransferPhase.CANCELLED, + ]: + raise Exception( + "Error occured while uploading image. The transfer is in %s" % transfer.phase + ) + if module.params.get('logical_unit'): + disks_service = connection.system_service().disks_service() + wait( + service=disks_service.service(module.params['id']), + condition=lambda d: d.status == otypes.DiskStatus.OK, + wait=module.params['wait'], + timeout=module.params['timeout'], + ) + + return True + + class DisksModule(BaseModule): def build_entity(self): @@ -185,7 +318,7 @@ class DisksModule(BaseModule): format=otypes.DiskFormat( self._module.params.get('format') ) if self._module.params.get('format') else None, - sparse=False if self._module.params.get('format') == 'raw' else True, + sparse=self._module.params.get('format') != 'raw', provisioned_size=convert_to_bytes( self._module.params.get('size') ), @@ -212,7 +345,45 @@ class DisksModule(BaseModule): ) if logical_unit else None, ) - def update_check(self, entity): + def update_storage_domains(self, disk_id): + changed = False + disk_service = self._service.service(disk_id) + disk = disk_service.get() + sds_service = self._connection.system_service().storage_domains_service() + + # We don't support move© for non file based storages: + if disk.storage_type != otypes.DiskStorageType.IMAGE: + return changed + + # Initiate move: + if self._module.params['storage_domain']: + new_disk_storage = search_by_name(sds_service, self._module.params['storage_domain']) + changed = self.action( + action='move', + entity=disk, + action_condition=lambda d: new_disk_storage.id != d.storage_domains[0].id, + wait_condition=lambda d: d.status == otypes.DiskStatus.OK, + storage_domain=otypes.StorageDomain( + id=new_disk_storage.id, + ), + post_action=lambda _: time.sleep(self._module.params['poll_interval']), + )['changed'] + + if self._module.params['storage_domains']: + for sd in self._module.params['storage_domains']: + new_disk_storage = search_by_name(sds_service, sd) + changed = changed or self.action( + action='copy', + entity=disk, + wait_condition=lambda disk: disk.status == otypes.DiskStatus.OK, + storage_domain=otypes.StorageDomain( + id=new_disk_storage.id, + ), + )['changed'] + + return changed + + 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 @@ -234,6 +405,7 @@ class DiskAttachmentsModule(DisksModule): def update_check(self, entity): return ( + super(DiskAttachmentsModule, self)._update_check(follow_link(self._connection, entity.disk)) and equal(self._module.params.get('interface'), str(entity.interface)) and equal(self._module.params.get('bootable'), entity.bootable) ) @@ -252,11 +424,14 @@ def main(): size=dict(default=None), interface=dict(default=None,), storage_domain=dict(default=None), + storage_domains=dict(default=None, type='list'), profile=dict(default=None), - format=dict(default=None, choices=['raw', 'cow']), + format=dict(default='cow', choices=['raw', 'cow']), bootable=dict(default=None, type='bool'), shareable=dict(default=None, type='bool'), logical_unit=dict(default=None, type='dict'), + image_path=dict(default=None), + force=dict(default=False, type='bool'), ) module = AnsibleModule( argument_spec=argument_spec, @@ -268,7 +443,7 @@ def main(): try: disk = None state = module.params['state'] - connection = create_connection(module.params.pop('auth')) + connection = create_connection(module.params.get('auth')) disks_service = connection.system_service().disks_service() disks_module = DisksModule( connection=connection, @@ -287,9 +462,16 @@ def main(): entity=disk, result_state=otypes.DiskStatus.OK if lun is None else None, ) + is_new_disk = ret['changed'] + ret['changed'] = ret['changed'] or disks_module.update_storage_domains(ret['id']) # 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 + + # Upload disk image in case it's new disk or force parameter is passed: + if module.params['image_path'] and (is_new_disk or module.params['force']): + uploaded = upload_disk_image(connection, module) + ret['changed'] = ret['changed'] or uploaded elif state == 'absent': ret = disks_module.remove() @@ -317,6 +499,13 @@ def main(): if state == 'present' or state == 'attached': ret = disk_attachments_module.create() + if lun is None: + wait( + service=disk_attachments_service.service(ret['id']), + condition=lambda d:follow_link(connection, d.disk).status == otypes.DiskStatus.OK, + wait=module.params['wait'], + timeout=module.params['timeout'], + ) elif state == 'detached': ret = disk_attachments_module.remove()