From b322841edfec770cfc8766ca1324ceae2b771fc1 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Tue, 18 Feb 2020 14:42:45 -0500 Subject: [PATCH] Complete integration of OVA provisioner Finishes a basic form of OVA provisioning within the existing create_vm function. Future plans should include separating out the functions and cleaning them up a bit more, but this is sufficient for basic operation. Closes #71 --- api-daemon/pvcapid/libvirt_schema.py | 2 +- api-daemon/pvcapid/ova-tmp.py | 46 ------ api-daemon/pvcapid/provisioner.py | 227 ++++++++++++++++----------- 3 files changed, 139 insertions(+), 136 deletions(-) delete mode 100644 api-daemon/pvcapid/ova-tmp.py diff --git a/api-daemon/pvcapid/libvirt_schema.py b/api-daemon/pvcapid/libvirt_schema.py index e56252ca..7b79b225 100755 --- a/api-daemon/pvcapid/libvirt_schema.py +++ b/api-daemon/pvcapid/libvirt_schema.py @@ -53,6 +53,7 @@ libvirt_header = """ restart restart + """ # File footer, closing devices and domain elements @@ -75,7 +76,6 @@ devices_default = """ /usr/bin/kvm devices_serial = """ - """ # VNC device diff --git a/api-daemon/pvcapid/ova-tmp.py b/api-daemon/pvcapid/ova-tmp.py deleted file mode 100644 index 8f50a68f..00000000 --- a/api-daemon/pvcapid/ova-tmp.py +++ /dev/null @@ -1,46 +0,0 @@ - -# -# TEMP -# -def tempstuff(): - # Verify that the cluster has enough space to store all OVA disk volumes - total_size_bytes = 0 - for disk in disk_map: - # Normalize the dev size to MB - # The function always return XXXXB, so strip off the B and convert to an integer - dev_size_bytes = int(pvc_ceph.format_bytes_fromhuman(disk.get('capacity', 0))[:-1]) - ova_size_bytes = int(pvc_ceph.format_bytes_fromhuman(ova_size)[:-1]) - # Get the actual image size - total_size_bytes += dev_size_bytes - # Add on the OVA size to account for the VMDK - total_size_bytes += ova_size_bytes - - zk_conn = pvc_common.startZKConnection(config['coordinators']) - pool_information = pvc_ceph.getPoolInformation(zk_conn, pool) - pvc_common.stopZKConnection(zk_conn) - pool_free_space_bytes = int(pool_information['stats']['free_bytes']) - if total_size_bytes >= pool_free_space_bytes: - output = { - 'message': "ERROR: The cluster does not have enough free space ({}) to store the VM ({}).".format( - pvc_ceph.format_bytes_tohuman(pool_free_space_bytes), - pvc_ceph.format_bytes_tohuman(total_size_bytes) - ) - } - retcode = 400 - cleanup_ova_maps_and_volumes() - return output, retcode - - # Convert from the temporary to destination format on the blockdevs - retcode, stdout, stderr = pvc_common.run_os_command( - 'qemu-img convert -C -f {} -O raw {} {}'.format(img_type, temp_blockdev, dest_blockdev) - ) - if retcode: - output = { - 'message': "ERROR: Failed to convert image '{}' format from '{}' to 'raw': {}".format(disk.get('src'), img_type, stderr) - } - retcode = 400 - cleanup_img_maps_and_volumes() - cleanup_ova_maps_and_volumes() - return output, retcode - - diff --git a/api-daemon/pvcapid/provisioner.py b/api-daemon/pvcapid/provisioner.py index 0dc517ff..b5ab8ca2 100755 --- a/api-daemon/pvcapid/provisioner.py +++ b/api-daemon/pvcapid/provisioner.py @@ -918,12 +918,6 @@ def delete_profile(name): close_database(conn, cur) return retmsg, retcode -# -# Cloned VM provisioning function - executed by the Celery worker -# -def clone_vm(self, vm_name, vm_profile, source_volumes): - pass - # # Main VM provisioning function - executed by the Celery worker # @@ -972,11 +966,17 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): args = (vm_profile,) db_cur.execute(query, args) profile_data = db_cur.fetchone() - if profile_data['arguments']: - vm_data['script_arguments'] = profile_data['arguments'].split('|') + if profile_data.get('arguments'): + vm_data['script_arguments'] = profile_data.get('arguments').split('|') else: vm_data['script_arguments'] = [] - + + if profile_data.get('profile_type') == 'ova': + is_ova_install = True + is_script_install = False # By definition + else: + is_ova_install = False + # Get the system details query = 'SELECT * FROM system_template WHERE id = %s' args = (profile_data['system_template'],) @@ -984,10 +984,14 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): vm_data['system_details'] = db_cur.fetchone() # Get the MAC template - query = 'SELECT * FROM network_template WHERE id = %s' + query = 'SELECT mac_template FROM network_template WHERE id = %s' args = (profile_data['network_template'],) db_cur.execute(query, args) - vm_data['mac_template'] = db_cur.fetchone()['mac_template'] + db_row = db_cur.fetchone() + if db_row: + vm_data['mac_template'] = db_row.get('mac_template') + else: + vm_data['mac_template'] = None # Get the networks query = 'SELECT * FROM network WHERE network_template = %s' @@ -1003,18 +1007,28 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): vm_data['volumes'] = db_cur.fetchall() # Get the script - query = 'SELECT * FROM script WHERE id = %s' + query = 'SELECT script FROM script WHERE id = %s' args = (profile_data['script'],) db_cur.execute(query, args) - vm_data['script'] = db_cur.fetchone()['script'] + vm_data['script'] = db_cur.fetchone() - if not vm_data['script']: - # We do not have a script; skip it - is_script_install = False - else: + if vm_data['script'] and not is_ova_install: is_script_install = True + else: + is_script_install = False + + # Get the OVA details + if is_ova_install: + query = 'SELECT * FROM ova WHERE id = %s' + args = (profile_data['ova'],) + db_cur.execute(query, args) + vm_data['ova_details'] = db_cur.fetchone() + + query = 'SELECT * FROM ova_volume WHERE ova = %s' + args = (profile_data['ova'],) + db_cur.execute(query, args) + vm_data['volumes'] = db_cur.fetchall() - close_database(db_conn, db_cur) print("VM configuration data:\n{}".format(json.dumps(vm_data, sort_keys=True, indent=2))) @@ -1064,7 +1078,7 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): # Verify that there is enough disk space free to provision all VM disks pools = dict() for volume in vm_data['volumes']: - if volume['source_volume'] is not None: + if volume.get('source_volume') is not None: if not volume['pool'] in pools: volume_data, status = pvc_ceph.getVolumeInformation(zk_conn, volume['pool'], volume['source_volume']) pools[volume['pool']] = volume_data['disk_size_gb'] @@ -1093,25 +1107,26 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): print("There is enough space on cluster to store VM volumes") - # Verify that every specified filesystem is valid - used_filesystems = list() - for volume in vm_data['volumes']: - if volume['source_volume'] is not None: - continue - if volume['filesystem'] and volume['filesystem'] not in used_filesystems: - used_filesystems.append(volume['filesystem']) + if not is_ova_install: + # Verify that every specified filesystem is valid + used_filesystems = list() + for volume in vm_data['volumes']: + if volume['source_volume'] is not None: + continue + if volume['filesystem'] and volume['filesystem'] not in used_filesystems: + used_filesystems.append(volume['filesystem']) + + for filesystem in used_filesystems: + if filesystem == 'swap': + retcode, stdout, stderr = pvc_common.run_os_command("which mkswap") + if retcode: + raise ProvisioningError("Failed to find binary for mkswap: {}".format(filesystem, stderr)) + else: + retcode, stdout, stderr = pvc_common.run_os_command("which mkfs.{}".format(filesystem)) + if retcode: + raise ProvisioningError("Failed to find binary for mkfs.{}: {}".format(filesystem, stderr)) - for filesystem in used_filesystems: - if filesystem == 'swap': - retcode, stdout, stderr = pvc_common.run_os_command("which mkswap") - if retcode: - raise ProvisioningError("Failed to find binary for mkswap: {}".format(filesystem, stderr)) - else: - retcode, stdout, stderr = pvc_common.run_os_command("which mkfs.{}".format(filesystem)) - if retcode: - raise ProvisioningError("Failed to find binary for mkfs.{}: {}".format(filesystem, stderr)) - - print("All selected filesystems are valid") + print("All selected filesystems are valid") # Phase 3 - provisioning script preparation # * Import the provisioning script as a library with importlib @@ -1176,9 +1191,8 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): vm_id_hex = '{:x}'.format(int(vm_id % 16)) net_id_hex = '{:x}'.format(int(network_id % 16)) - if vm_data['mac_template']: + if vm_data.get('mac_template') is not None: mac_prefix = '52:54:01' - mactemplate = "{prefix}:ff:f6:{vmid}{netid}" macgen_template = vm_data['mac_template'] eth_macaddr = macgen_template.format( prefix=mac_prefix, @@ -1286,7 +1300,7 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): time.sleep(1) for volume in vm_data['volumes']: - if volume['source_volume'] is not None: + if volume.get('source_volume') is not None: success, message = pvc_ceph.clone_volume(zk_conn, volume['pool'], "{}_{}".format(vm_name, volume['disk_id']), volume['source_volume']) print(message) if not success: @@ -1306,37 +1320,71 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): time.sleep(1) for volume in vm_data['volumes']: - if volume['source_volume'] is not None: - continue + dst_volume_name = "{}_{}".format(vm_name, volume['disk_id']) + dst_volume = "{}/{}".format(volume['pool'], dst_volume_name) - if not volume['filesystem']: - continue + if is_ova_install: + src_volume_name = volume['volume_name'] + src_volume = "{}/{}".format(volume['pool'], src_volume_name) - rbd_volume = "{}/{}_{}".format(volume['pool'], vm_name, volume['disk_id']) - - filesystem_args_list = list() - for arg in volume['filesystem_args'].split(): - arg_entry, arg_data = arg.split('=') - filesystem_args_list.append(arg_entry) - filesystem_args_list.append(arg_data) - filesystem_args = ' '.join(filesystem_args_list) - - # Map the RBD device - retcode, stdout, stderr = pvc_common.run_os_command("rbd map {}".format(rbd_volume)) - if retcode: - raise ProvisioningError('Failed to map volume "{}": {}'.format(rbd_volume, stderr)) - - # Create the filesystem - if volume['filesystem'] == 'swap': - retcode, stdout, stderr = pvc_common.run_os_command("mkswap -f /dev/rbd/{}".format(rbd_volume)) + # Map the target RBD device + retcode, retmsg = pvc_ceph.map_volume(zk_conn, volume['pool'], dst_volume_name) + if not retcode: + raise ProvisioningError('Failed to map destination volume "{}": {}'.format(dst_volume_name, retmsg)) + # Map the source RBD device + retcode, retmsg = pvc_ceph.map_volume(zk_conn, volume['pool'], src_volume_name) + if not retcode: + raise ProvisioningError('Failed to map source volume "{}": {}'.format(src_volume_name, retmsg)) + # Convert from source to target + retcode, stdout, stderr = pvc_common.run_os_command( + 'qemu-img convert -C -f {} -O raw {} {}'.format( + volume['volume_format'], + "/dev/rbd/{}".format(src_volume), + "/dev/rbd/{}".format(dst_volume) + ) + ) if retcode: - raise ProvisioningError('Failed to create swap on "{}": {}'.format(rbd_volume, stderr)) + raise ProvisioningError('Failed to convert {} volume "{}" to raw volume "{}": {}'.format(volume['volume_format'], src_volume, dst_volume, stderr)) + + # Unmap the source RBD device (don't bother later) + retcode, retmsg = pvc_ceph.unmap_volume(zk_conn, volume['pool'], src_volume_name) + if not retcode: + raise ProvisioningError('Failed to unmap source volume "{}": {}'.format(src_volume_name, retmsg)) + # Unmap the target RBD device (don't bother later) + retcode, retmsg = pvc_ceph.unmap_volume(zk_conn, volume['pool'], dst_volume_name) + if not retcode: + raise ProvisioningError('Failed to unmap destination volume "{}": {}'.format(dst_volume_name, retmsg)) + print('Converted {} source volume {} to raw format on {}'.format(volume['volume_format'], src_volume, dst_volume)) else: - retcode, stdout, stderr = pvc_common.run_os_command("mkfs.{} {} /dev/rbd/{}".format(volume['filesystem'], filesystem_args, rbd_volume)) - if retcode: - raise ProvisioningError('Failed to create {} filesystem on "{}": {}'.format(volume['filesystem'], rbd_volume, stderr)) + if volume.get('source_volume') is not None: + continue - print("Created {} filesystem on {}:\n{}".format(volume['filesystem'], rbd_volume, stdout)) + if volume.get('filesystem') is None: + continue + + filesystem_args_list = list() + for arg in volume['filesystem_args'].split(): + arg_entry, arg_data = arg.split('=') + filesystem_args_list.append(arg_entry) + filesystem_args_list.append(arg_data) + filesystem_args = ' '.join(filesystem_args_list) + + # Map the RBD device + retcode, retmsg = pvc_ceph.map_volume(zk_conn, volume['pool'], dst_volume_name) + if not retcode: + raise ProvisioningError('Failed to map volume "{}": {}'.format(dst_volume, retmsg)) + + # Create the filesystem + if volume['filesystem'] == 'swap': + retcode, stdout, stderr = pvc_common.run_os_command("mkswap -f /dev/rbd/{}".format(dst_volume)) + if retcode: + raise ProvisioningError('Failed to create swap on "{}": {}'.format(dst_volume, stderr)) + else: + retcode, stdout, stderr = pvc_common.run_os_command("mkfs.{} {} /dev/rbd/{}".format(volume['filesystem'], filesystem_args, dst_volume)) + if retcode: + raise ProvisioningError('Failed to create {} filesystem on "{}": {}'.format(volume['filesystem'], dst_volume, stderr)) + + print("Created {} filesystem on {}:\n{}".format(volume['filesystem'], dst_volume, stdout)) if is_script_install: # Create temporary directory @@ -1352,7 +1400,7 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): if not volume['mountpoint'] or volume['mountpoint'] == 'swap': continue - mapped_rbd_volume = "/dev/rbd/{}/{}_{}".format(volume['pool'], vm_name, volume['disk_id']) + mapped_dst_volume = "/dev/rbd/{}/{}_{}".format(volume['pool'], vm_name, volume['disk_id']) mount_path = "{}{}".format(temp_dir, volume['mountpoint']) # Ensure the mount path exists (within the filesystems) @@ -1361,11 +1409,11 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): raise ProvisioningError('Failed to create mountpoint "{}": {}'.format(mount_path, stderr)) # Mount filesystems to temporary directory - retcode, stdout, stderr = pvc_common.run_os_command("mount {} {}".format(mapped_rbd_volume, mount_path)) + retcode, stdout, stderr = pvc_common.run_os_command("mount {} {}".format(mapped_dst_volume, mount_path)) if retcode: - raise ProvisioningError('Failed to mount "{}" on "{}": {}'.format(mapped_rbd_volume, mount_path, stderr)) + raise ProvisioningError('Failed to mount "{}" on "{}": {}'.format(mapped_dst_volume, mount_path, stderr)) - print("Successfully mounted {} on {}".format(mapped_rbd_volume, mount_path)) + print("Successfully mounted {} on {}".format(mapped_dst_volume, mount_path)) # Phase 8 - provisioning script execution # * Execute the provisioning script main function ("install") passing any custom arguments @@ -1400,28 +1448,29 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True): self.update_state(state='RUNNING', meta={'current': 9, 'total': 10, 'status': 'Cleaning up local mounts and directories'}) time.sleep(1) - for volume in list(reversed(vm_data['volumes'])): - if volume['source_volume'] is not None: - continue + if not is_ova_install: + for volume in list(reversed(vm_data['volumes'])): + if volume.get('source_volume') is not None: + continue - if is_script_install: - # Unmount the volume - if volume['mountpoint'] and volume['mountpoint'] != 'swap': - print("Cleaning up mount {}{}".format(temp_dir, volume['mountpoint'])) + if is_script_install: + # Unmount the volume + if volume.get('mountpoint') is not None and volume.get('mountpoint') != 'swap': + print("Cleaning up mount {}{}".format(temp_dir, volume['mountpoint'])) - mount_path = "{}{}".format(temp_dir, volume['mountpoint']) - retcode, stdout, stderr = pvc_common.run_os_command("umount {}".format(mount_path)) + mount_path = "{}{}".format(temp_dir, volume['mountpoint']) + retcode, stdout, stderr = pvc_common.run_os_command("umount {}".format(mount_path)) + if retcode: + raise ProvisioningError('Failed to unmount "{}": {}'.format(mount_path, stderr)) + + # Unmap the RBD device + if volume['filesystem']: + print("Cleaning up RBD mapping /dev/rbd/{}/{}_{}".format(volume['pool'], vm_name, volume['disk_id'])) + + rbd_volume = "/dev/rbd/{}/{}_{}".format(volume['pool'], vm_name, volume['disk_id']) + retcode, stdout, stderr = pvc_common.run_os_command("rbd unmap {}".format(rbd_volume)) if retcode: - raise ProvisioningError('Failed to unmount "{}": {}'.format(mount_path, stderr)) - - # Unmap the RBD device - if volume['filesystem']: - print("Cleaning up RBD mapping /dev/rbd/{}/{}_{}".format(volume['pool'], vm_name, volume['disk_id'])) - - rbd_volume = "/dev/rbd/{}/{}_{}".format(volume['pool'], vm_name, volume['disk_id']) - retcode, stdout, stderr = pvc_common.run_os_command("rbd unmap {}".format(rbd_volume)) - if retcode: - raise ProvisioningError('Failed to unmap volume "{}": {}'.format(rbd_volume, stderr)) + raise ProvisioningError('Failed to unmap volume "{}": {}'.format(rbd_volume, stderr)) print("Cleaning up temporary directories and files")