# # # Copyright 2019 VMware Inc. # This file is part of ETSI OSM # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # # For those usages not covered by the Apache License, Version 2.0 please # contact: osslegalrouting@vmware.com # # import logging from lxml import etree import os from pyvcloud.vcd.client import BasicLoginCredentials, Client, QueryResultFormat, ResourceType, TaskStatus, ApiVersion from pyvcloud.vcd.exceptions import EntityNotFoundException, InternalServerException from pyvcloud.vcd.org import Org import sys import tarfile import time MODULE_DIR = os.path.dirname(__file__) # Set logger LOG_FILE = os.path.join(MODULE_DIR, "logs/ovf_uploader.log") os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) logger = logging.getLogger(__name__) file_handler = logging.FileHandler(LOG_FILE) file_handler.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) logger.addHandler(file_handler) stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setLevel(logging.INFO) logger.addHandler(stdout_handler) logger.setLevel(10) logging.captureWarnings(True) __version__ = "1.0" __description__ = "Initial Release" def get_version(): """ get version of this application""" version = str(__version__) + " - " + str(__description__) return version def report_progress(bytes_written, total_size): percent_complete = int((bytes_written * 100) / total_size) print("{}% complete \r".format(percent_complete), end='') class OVFUploader(object): """ Class to convert input image into OVF format """ def __init__(self, ovf_file, vcd_url=None, username=None, password=None, orgname=None): self.ovf_file = os.path.abspath(ovf_file) self.vcd_url = vcd_url self.username = username self.password = password self.orgname = orgname try: client = Client(self.vcd_url, verify_ssl_certs=False, api_version=ApiVersion.VERSION_32.value, log_requests=True, log_headers=True, log_bodies=True, log_file=LOG_FILE) # sclient.set_highest_supported_version() client.set_credentials(BasicLoginCredentials(self.username, self.orgname, self.password)) logger.info("Logged into {} using version {}".format(self.vcd_url, client.get_api_version())) self.client = client self.org = Org(self.client, resource=self.client.get_org()) except Exception as exp: problem = Exception("Failed to connect to vCD at {}, org {}, username {}:\n{}".format( self.vcd_url, self.orgname, self.username, exp)) logger.error(problem) raise problem try: # Retrieve the VM name from the OVF. We will use this as both the image and catalog name OVF_tree = etree.parse(self.ovf_file) root = OVF_tree.getroot() nsmap = {k: v for k, v in root.nsmap.items() if k} nsmap["xmlns"] = "http://schemas.dmtf.org/ovf/envelope/1" virtuasystem = root.find('xmlns:VirtualSystem', nsmap) name_tag = virtuasystem.find('xmlns:Name', nsmap) self.image_name = name_tag.text info_tag = virtuasystem.find('xmlns:Info', nsmap) self.image_description = info_tag.text references = root.find('xmlns:References', nsmap) file = references.find('xmlns:File', nsmap) self.vmdk_file = "{}/{}".format( os.path.dirname(self.ovf_file), file.attrib['{http://schemas.dmtf.org/ovf/envelope/1}href']) logger.info("Loaded VM {}: {}".format(self.image_name, self.image_description)) except Exception as exp: problem = Exception("Failed to fetch VirtualSystem Name element from OVF {}:\n{}".format( self.ovf_file, exp)) logger.error(problem) raise problem def make_catalog(self): self.catalog_id = None try: for catalog in self.org.list_catalogs(): if catalog['name'] == self.image_name: self.catalog_id = catalog['id'] if self.catalog_id is None: logger.info("Creating a new catalog entry {} in vCD".format(self.image_name)) result = self.org.create_catalog(self.image_name, self.image_description) if result is None: raise Exception("Failed to create new catalog entry") self.catalog_id = result.attrib['id'].split(':')[-1] self.org.reload() logger.debug("Using catalog {}, id {}".format(self.image_name, self.catalog_id)) except Exception as exp: problem = Exception("Failed to fetch catalog for {}:\n{} ".format(self.image_name, exp)) logger.error(problem) raise problem def upload_ovf(self): ova_tarfilename, _ = os.path.splitext(self.ovf_file) ova_tarfilename += '.ova' try: # Check if the content already exists: resource_type = ResourceType.CATALOG_ITEM.value q = self.client.get_typed_query( resource_type, query_result_format=QueryResultFormat.ID_RECORDS, equality_filter=('catalogName', self.image_name)) for item in list(q.execute()): if item.get('name') == self.image_name: logger.info("Removing old version from catalog") try: self.org.delete_catalog_item(self.image_name, self.image_name) except InternalServerException as exp: problem = Exception( "Cannot delete vAppTemplate {}. Please check in vCD if " "the content is still being imported into the catalog".format( self.image_name)) raise problem # Create a single OVA bundle ova = tarfile.open(name=ova_tarfilename, mode='w') ova.add(self.ovf_file, arcname=os.path.basename(self.ovf_file)) ova.add(self.vmdk_file, arcname=os.path.basename(self.vmdk_file)) ova.close() logger.info("Uploading content to vCD") self.org.upload_ovf(self.image_name, ova_tarfilename, item_name=self.image_name, description=self.image_description, callback=report_progress) except Exception as exp: problem = Exception("Failed to upload OVF {}:\n{} ".format(self.ovf_file, exp)) logger.error(problem) raise problem finally: if os.path.exists(ova_tarfilename): os.remove(ova_tarfilename) def wait_for_task_completion(self): logger.info("Importing content to vCD") try: query = self.client.get_typed_query( query_type_name=ResourceType.TASK.value, qfilter='ownerName==' + self.username + ';(status==queued,status==preRunning,status==running)', query_result_format=QueryResultFormat.REFERENCES) upload_task = None tasks = list(query.execute()) for task in tasks: if task.get('name') == 'VDC_UPLOAD_OVF_CONTENTS': upload_task = self.client.get_resource(task.get('href')) break bad_statuses = [ TaskStatus.ABORTED, TaskStatus.CANCELED, TaskStatus.ERROR ] except Exception as exp: problem = Exception("Failed to import OVF {}:\n{} ".format(self.ovf_file, exp)) logger.error(problem) raise problem while(True): task_status = upload_task.get('status').lower() if(hasattr(upload_task, 'Progress')): print("{}% complete \r".format(upload_task.Progress), end='') for status in bad_statuses: if task_status == status.value.lower(): problem = Exception( "vCD failed to import OVF {}:\n{}: {} ".format(self.ovf_file, task_status, upload_task.Error.get('Message'))) logger.error(problem) raise problem if task_status == str(TaskStatus.SUCCESS.value).lower(): break time.sleep(2) upload_task = self.client.get_resource(upload_task.get('href')) logger.info("OVF upload and import complete, content is ready to use")