cloud: ovirt: Add support to upload/copy/move disks (#19337)

pull/19349/head
Ondra Machacek 8 years ago committed by Ryan Brown
parent ed933421fe
commit f84f97d035

@ -19,7 +19,18 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
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&copy 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()

Loading…
Cancel
Save