mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
cloud: ovirt: Add support to upload/copy/move disks (#19337)
This commit is contained in:
parent
ed933421fe
commit
f84f97d035
1 changed files with 195 additions and 6 deletions
|
@ -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© 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…
Reference in a new issue