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")