Fixes OVF converter and upload
Adds the ability to specify hardware version
Defaults hardware version to 14 for compatibility
Displays list of OS types in help
Adds a working vCD upload
Adds simple usage file
Cleans up installation path
Bug 850
Change-Id: I1f4658e48869149c523a80401487e3e6c25dc809
Signed-off-by: beierl <mbeierl@vmware.com>
diff --git a/tools/OVF_converter/uploader.py b/tools/OVF_converter/uploader.py
new file mode 100644
index 0000000..3702d38
--- /dev/null
+++ b/tools/OVF_converter/uploader.py
@@ -0,0 +1,220 @@
+# #
+# 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
+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)
+ client.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):
+ try:
+ try:
+ catalog = self.org.get_catalog(self.image_name)
+ self.catalog_id = catalog.attrib['id'].split(':')[-1]
+ except EntityNotFoundException:
+ 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):
+
+ try:
+ # Check if the content already exists:
+ items = self.org.list_catalog_items(self.image_name)
+ for item in items:
+ if item['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_tarfilename, _ = os.path.splitext(self.ovf_file)
+ ova_tarfilename += '.ova'
+ 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")