#!/usr/bin/env python
#
# Copyright (c) 2016 Nutanix Inc. All rights reserved.
#
# Author: raghu.rapole@nutanix.com
#
# This script calls detect/upgrade functions of update modules.
# please make sure to not import anything which is not available
# with default python on the cluster
#
import base64
import datetime
import json
import os
import sys
import traceback

from exception import LcmInventoryException, LcmUpdateException
from thirdparty_libs import bytes_to_str, PY3

sys.path.append(os.getcwd())

from scratchpad import Scratchpad

MODULE_SPEC_V1_DETECT_PREFIX = "detect"
MODULE_SPEC_V2_DETECT_PREFIX = "detect_v2"
MODULE_SPEC_V1_UPGRADE_PREFIX = "upgrade"
MODULE_SPEC_V2_UPGRADE_PREFIX = "upgrade_v2"
MODULE_COLLECT_DATA = "collect_data"
MODULE_UPGRADE_ASYNC = "upgrade_async"
MODULE_UPGRADE_STATUS = "upgrade_status"
MODULE_SPEC_V2 = '2'
MOD_SUCCESS = 0
MOD_PRECHECK_ERROR = 1
MOD_EXCEPTION_ERROR = 4
MOD_IMPORT_ERROR = 5
# This will be returned when script is not called from expected py3 venv
# when called with python3, asking user to try again with correct python path
# 11 is also an error code for EAGAIN (Try again)
MOD_PY_VENV_ERROR = 11

def parse_options():
  """
  Parses options based on whether argparse or optparse is present.
  Returns:
    options - the curated options object.
  """
  use_argparse = False
  try:
    from argparse import ArgumentParser
    parser = ArgumentParser(description="LCM helper script")
    use_argparse = True
    arg_func = getattr(parser, "add_argument")
  except ImportError:
    # Use the older library to have it be compatible with older hypervisors
    from optparse import OptionParser
    # Set up CLI for lcm_helper script
    parser = OptionParser()
    arg_func = getattr(parser, "add_option")

  arg_func("-i", "--inventory", action="store_true",
      dest="inv_op", help="Run inventory")
  arg_func("-u", "--update", action="store_true",
      dest="update_op", help="Run update")
  arg_func("-p", "--precheck", action="store_true",
      dest="precheck_op", help="Run update")
  arg_func("--pu", "--preupgrade", action="store_true",
           dest="preupgrade_op", help="Run preupgrade")
  arg_func("--ref_name", action="store",
      dest="ref_name", help="Reference name of the module")
  arg_func("--entity_model", default=None, action="store",
      dest="entity_model", help="Entity model (required only for upgrade)")
  arg_func("--image", default=None, action="store",
      dest="image", help="Image details")
  arg_func("--upgrade_sno", default=0, action="store",
      dest= "upgrade_sno", help="Upgrade stage number, it is required to tell the framework which " \
           "upgrade stage to run. If not provided, it is assumed that there is " \
           "a single upgrade stage named 'upgrade'", type=int)
  arg_func("-v", action="store", dest="request_version", help="Version to"
      " update the entity to")

  # Optional argument
  arg_func("--module_spec_version", choices=['1','2'], default='1',
    action="store", dest="spec_version", help="RIM spec version")
  arg_func("--ipmi_cred", default=None, action="store", dest="ipmi_cred",
           help="For Redfish based updates")
  arg_func("--lcm_family", default=None, action="store", dest="lcm_family",
           help="For Redfish based updates")

  arg_func("--scratchpad", default=None, action="store", dest="scratchpad",
           help="The Scratchpad for this task (required only for upgrade)")

  arg_func("--collect_data", action="store_true",
      dest="collect_data_op", help="Run collect_data")

  arg_func("--async_upgrade", action="store_true",
      dest="upgrade_async_op", help="Run upgrade_async")

  arg_func("--async_status", action="store_true",
      dest="upgrade_status_op", help="Run upgrade_status")

  arg_func("--task_uuid", default=None, action="store", dest="task_uuid",
           help="The update task uuid in hex string format")

  arg_func("--env", default=None, action="store", dest="env",
           help="env can be leader or any other env identifier")

  arg_func("--entity_class", default=None, action="store",
           dest="entity_class", help="Entity class")

  arg_func("--from_version", default=None, action="store",
           dest="from_version", help="Current version of entity")

  arg_func("--disable_early_finish", action="store_true",
           dest="disable_early_finish",
           help="Is the early finish capability disabled in lcm config")

  # Optional args specific to lcm_ops_by_leader.
  arg_func("--cvm_ip", default=None, action="store", dest="cvm_ip",
           help="The IP of the target CVM")
  arg_func("--host_ip", default=None, action="store", dest="host_ip",
           help="The IP of the target host")
  arg_func("--host_user", default=None, action="store", dest="host_user",
           help="Username of the target host")
  arg_func("--iscsi_target", default=None, action="append", type=str,
           dest="iscsi_target", help="IQN of Volume Group for iSCSI based staging")
  arg_func("--aos_version", default=None, action="store", dest="aos_version",
           help="aos version of target host's cvm")
  arg_func("--check_for_python_venv", action="store",
           dest="check_for_python_venv",
           help="check if venv is required to run script")
  arg_func("--disable_intersight_inventory_prechecks", action="store_true",
           dest="disable_intersight_inventory_prechecks",
           help="Disable module level inventory prechecks for intersight")
  arg_func("--upgrade_tags", default=None, action="append", type=str,
           dest="upgrade_tags", help="Upgrade tags mapped to target version")

  if use_argparse:
    arg_func("--entity_id", nargs='?', default=None, action="store",
              dest="entity_id",
              help="Unique identifier for an entity (required only for upgrade)")
    options = parser.parse_args()
  else:
    arg_func("--entity_id", default=None, action="store",
              dest="entity_id",
              help="Unique identifier for an entity (required only for upgrade)")
    options, _ = parser.parse_args()

  options.image = (bytes_to_str(base64.b64decode(options.image))
                   if options.image else None)
  options.ipmi_cred = (bytes_to_str(base64.b64decode(options.ipmi_cred))
                       if options.ipmi_cred else None)
  return options

def print_timestamp(msg):
  """
  Helper function to print messages with timestamp
  Args:
    msg(str)      : Message string
  """
  log_msg = "%s %s" % (datetime.datetime.now(), msg)
  print(log_msg)

def get_module_attribute(module_name, attribute_name):
  """Used to get the callable function from the update module.

  Returns:
    dict of {
      "module": imported module,
      "attribute": callable function
      "module_error": Any error found while importing the module,
      "attribute_error": Any error found while looking for the callable function
    }
  """
  ret_dict = {
    "module": None,
    "attribute": None,
    "module_error": "",
    "attribute_error": ""
  }
  try:
    ret_dict["module"] = __import__(module_name,
                                    fromlist=module_name.split("."))
  except ImportError as ie:
    print_timestamp(traceback.format_exc())
    ret_dict["module_error"] = "Failed to import module %s. Error: %s" % (
      module_name, ie)
  ret_dict["attribute"] = getattr(ret_dict["module"], attribute_name, None)
  if ret_dict["attribute"] is None:
    ret_dict["attribute_error"] = ("Update module %s is missing a required "
                                   "method %s" % (module_name, attribute_name))
  return ret_dict

def get_leader_specific_params(options):
  """
  lcm_ops_by_leader sends extra params. This method returns a dictionary of
  these params.

  Returns:
    A dictionary of the params passed by lcm_ops_by_leader.
  """
  leader_params = {}
  if options.cvm_ip:
    leader_params["cvm_ip"] = options.cvm_ip
  if options.host_ip:
    leader_params["host_ip"] = options.host_ip
  if options.host_user:
    leader_params["host_user"] = options.host_user
  if options.iscsi_target:
    leader_params["iscsi_target"] = options.iscsi_target
  if options.disable_intersight_inventory_prechecks:
    leader_params["disable_intersight_inventory_prechecks"] = \
      options.disable_intersight_inventory_prechecks
  if options.upgrade_tags:
    leader_params["upgrade_tags"] = options.upgrade_tags
  return leader_params

def resolve_image_filepath(file_path):
  resolved_path = None
  # If the file doesn't exist, assume it was passed as a relative path and go
  # find it in the local path.
  if os.path.exists(file_path):
    resolved_path = file_path
  else:
    for root, _, files in os.walk('.'):
      for filename in files:
        if filename == file_path:
          resolved_path = "%s/%s" % (root, filename)
  return resolved_path

def inventory(options):
  """
  Calls detect function of input module.
  """
  try:
    if options.spec_version == MODULE_SPEC_V2:
      ret_dict = get_module_attribute(options.ref_name,
          MODULE_SPEC_V2_DETECT_PREFIX)
      if ret_dict["module_error"] or ret_dict["attribute_error"]:
        raise LcmInventoryException(options.ref_name,
          ret_dict.get("module_error", ret_dict.get("attribute_error")))
      params = {}
      if options.ipmi_cred:
        params["ipmi_cred"] = options.ipmi_cred
      if options.lcm_family:
        params["lcm_family"] = options.lcm_family
      leader_params = get_leader_specific_params(options)
      if leader_params:
        params.update(leader_params)
      ret = ret_dict["attribute"](params)
    else:
      ret_dict = get_module_attribute(options.ref_name,
          MODULE_SPEC_V1_DETECT_PREFIX)
      if ret_dict["module_error"] or ret_dict["attribute_error"]:
        raise LcmInventoryException(options.ref_name,
          ret_dict.get("module_error", ret_dict.get("attribute_error")))
      ret = ret_dict["attribute"]()

    if ret is None:
      raise LcmInventoryException(options.ref_name,
          "Unexpected inventory response: None")
    for out in ret:
      if (out.count and out.device_id) or (not out.count and not out.device_id):
        raise LcmInventoryException(options.ref_name,
            "Unexpected inventory response: Invalid count/device_id.")
    print_timestamp("##START##")
    for out in ret:
      # ENG-260799 - version and device id come from the modules logic.
      # assume they can be unicode and encode them to utf-8 before storing
      # in IDF.
      # Python 3 has renamed unicode to str. Some distros of hypervisors
      # don't have unicode_or_str which is a backwards compatible way of
      # identifying if a string is unicode. Hence there are 2 ways to have
      # lcm_helper work on different versions of python and also properly
      # work with unicode strings:
      # 1. Try unicode conversion and handle NameError exception. in
      #    that case, assume its a string and go ahead.
      # 2. Check for python version using sys.version[0] and verify it
      #    is 3 or above to assume unicode is str. This is risky if
      #    python changes its notion again.
      # Going with method 1.
      try:
        if isinstance(out.device_id, unicode):
          out.device_id = unicode(out.device_id).encode('utf-8')
        if isinstance(out.version, unicode):
          out.version = unicode(out.version).encode('utf-8')
      except NameError:
        # Do nothing.
        pass
      msg = json.dumps(vars(out))
      # Not changing this line to print_timestamp() because the calling
      # code in the framework is anticipating a valid json at this point
      print(msg)
    print_timestamp("##END##")
    sys.exit(MOD_SUCCESS)
  except Exception as ex_other:
    print_timestamp(traceback.format_exc())
    err_dict = {
        "name": options.ref_name,
        "err_msg": "Inventory failed with error: [%s]" % str(ex_other)
        }
    print_timestamp("EXCEPT:%s" % json.dumps(err_dict))
    sys.exit(MOD_EXCEPTION_ERROR)

def preupgrade(options):
  """
  Calls upgrade function of input module.
  """
  preupgrade_method_name = "preupgrade"

  print_timestamp(
    "Attempting to load update method %s from from update module %s"
    % (preupgrade_method_name, options.ref_name))
  ret_dict = get_module_attribute(options.ref_name, preupgrade_method_name)
  if ret_dict["module_error"] or ret_dict["attribute_error"]:
    sys.exit(MOD_SUCCESS)
  print_timestamp("Method %r loaded from module %r" %
                  (ret_dict["attribute"], ret_dict["module"]))

  params, opened = get_params(options)
  leader_params = get_leader_specific_params(options)
  if leader_params:
    params.update(leader_params)

  try:
    ret_dict["attribute"](params)
  except Exception as ex:
    print_timestamp(traceback.format_exc())
    err_dict = {
        "name": options.ref_name,
        "err_msg": "Preupgrade failed with error: [%s]" % str(ex),
      }
    print_timestamp("EXCEPT:%s" % json.dumps(err_dict))
    sys.exit(MOD_SUCCESS)

  sys.exit(MOD_SUCCESS)

def handle_image_name(image):
  """
  The image name passed to the update function needs to be formatted to make
  sure it is of type string which is sent to the modules. Modules should
  expect the image name to be a string and any further manipulations needed
  by the modules should be handle on their module-specific code.
  Args:
    image(str or bytes): It is the image as decoded by the base64 of python
  Returns:
    str: Returns the image as a string.
  """
  print_timestamp("The decoded image sent to this script is %s" % image)
  # Extra handling added as this script is run on different python versions
  if isinstance(image, bytes):
    try:
      # This is done to make sure that the options.image is not in bytes.
      # In python3, this fails with a TyperError. More can be read about
      # this error at https://stackoverflow.com/questions/19827615/. This
      # change will try change the bytes to str and then we should be able to
      # use the string functions like startswith.
      image = image.decode()
      # Make sure that the options.image is a string. In python3 after the
      # operation on line 277, we get a string but we get a unicode in case
      # of python 2.7 and 2.6. This can lead to further regressions in the
      # modules code. Added the example in the commit message of this
      # change with detailed description of the fix done.
      try:
        if isinstance(image, unicode):
          image = image.encode('utf-8', 'ignore')
      except NameError:
        # This happens when this is run on python3 as python has renamed
        # unicode to str in python3.
        # Please see https://stackoverflow.com/questions/19877306/
        pass
    except (UnicodeDecodeError, AttributeError):
      pass
  print_timestamp("Image passed for upgrade is %s and the "
        "type is %s" % (image, type(image)))
  return image

def update(options, upgrade_stage=0):
  """
  Calls upgrade function of input module.
  """
  if options.image is not None:
    options.image = handle_image_name(options.image)
  if options.spec_version == MODULE_SPEC_V2:
    upgrade_method_name = MODULE_SPEC_V2_UPGRADE_PREFIX
  else:
    upgrade_method_name = MODULE_SPEC_V1_UPGRADE_PREFIX

  if options.upgrade_async_op:
    upgrade_method_name = MODULE_UPGRADE_ASYNC

  if upgrade_stage > 0:
    upgrade_method_name += ("_%d" % upgrade_stage)

  print_timestamp("Attempting to load update method %s from from update module %s" % (
    upgrade_method_name, options.ref_name))
  ret_dict = get_module_attribute(options.ref_name, upgrade_method_name)
  if ret_dict["module_error"] or ret_dict["attribute_error"]:
    raise LcmUpdateException(options.ref_name,
      ret_dict["module_error"] if ret_dict["module_error"]
                               else ret_dict["attribute_error"]
    )
  print_timestamp("Update method %r loaded from module %r" % (ret_dict["attribute"],
                                                    ret_dict["module"]))

  if options.spec_version == MODULE_SPEC_V2:
    # pass in the params dict. TODO: Make this a stricter enforcement.
    print_timestamp("V2_spec module.")
    params, opened = get_params(options)
    leader_params = get_leader_specific_params(options)
    if leader_params:
      params.update(leader_params)

    try:
      ret_dict["attribute"](params)
    except Exception as ex:
      print_timestamp(traceback.format_exc())
      err_dict = {
          "name": options.ref_name,
          "err_msg": "Update failed with error: [%s]" % str(ex),
          "stage": upgrade_stage
          }
      print_timestamp("EXCEPT:%s" % json.dumps(err_dict))
      sys.exit(MOD_EXCEPTION_ERROR)
    finally:
      if opened:
        params["image_details"].close()
  else:
    # Non V2 specs
    # ADFS path staging will only be supported with a V2 spec module.
    if options.image.startswith(':'):
      raise LcmUpdateException(options.ref_name,
          "Update module needs to conform to spec V2 to support ADFS staging")
    if not options.image:
      raise LcmUpdateException(options.ref_name,
          "File %s does not exist" % options.image)
    image_path = resolve_image_filepath(options.image)
    if not image_path:
      raise LcmUpdateException("Failed to locate file %s within %s" %
                                (options.image, os.getcwd()))
    opened = True
    image = open(image_path)
    if not image:
      raise LcmUpdateException(options.ref_name, "Failed to open file %s" % image_path)
    try:
      ret_dict["attribute"](options.entity_id, options.entity_model,
          image, options.request_version)
    except Exception as ex:
      print_timestamp(traceback.format_exc())
      err_dict = {
            "name": options.ref_name,
            "err_msg": "Update failed with error: [%s]" % str(ex),
            "stage": upgrade_stage
          }
      print_timestamp("EXCEPT:%s" % json.dumps(err_dict))
      sys.exit(MOD_EXCEPTION_ERROR)
    finally:
      if opened:
        image.close()

  # Send the Scratchpad back to the framework through stdout,
  # where the framework will parse and extract the Scratchpad.
  if options.scratchpad:
    print_timestamp("#SCRATCHPAD_START#")
    print(Scratchpad.get_json_dict())
    print_timestamp("#SCRATCHPAD_END#")
  sys.exit(MOD_SUCCESS)


def get_image_details(image, module_type):

  if module_type == "RIM":
    # Image is a list of dictionaries of format
    # {
    #   "image_version": "v",
    #   "entity_class": "class",
    #   "entity_model": "model",
    #   "images": [list of image info]
    #  }
    #
    # Extract image info from all dictionaries and create a new list for
    # backward compatibility.
    new_image_list = []
    for image_detail in image:
      image_list = image_detail.get("images", [])
      for img in image_list:
        if img.get("adfs_path"):
          img["file_path"] = img["adfs_path"]
        # For Leader skip the image path verification is not needed as its
        # staged by the module not by the LCM framework.
        # however, if it is set we pass in the file_url instead, which
        # can be used by the module to stage the image
        elif options.env and options.env == "leader":
          img["file_path"] = img.get("file_url")
        else:
          image_path = resolve_image_filepath(img.get("image_name"))
          if not image_path:
            raise LcmUpdateException(options.ref_name, "File %s does not exist"
                                     % img.get("image_name"))
          img["file_path"] = image_path
      new_image_list.extend(image_list)
    opened = False
    return new_image_list, opened
  else:
    if image.startswith(':'):
        raise LcmUpdateException(options.ref_name,
            "Update module needs to conform to spec V2 to support ADFS staging")
    if not image:
      raise LcmUpdateException(options.ref_name,
          "File %s does not exist" % options.image)
    image_path = resolve_image_filepath(image)
    opened = True
    image = open(image_path)
    if not image:
      raise LcmUpdateException(options.ref_name, "Failed to open file %s"
                               % image_path)
    return image, opened


def precheck():
  """
  Calls verify_precheck_dependencies and test function of input module.
  """
  precheck_module = __import__(options.ref_name, fromlist=options.ref_name.split('.'))
  ret = precheck_module.verify_precheck_dependencies()
  if not ret:
    print_timestamp("Pre-check %s is not relevant. Skipping.." % options.ref_name)
    sys.exit(MOD_SUCCESS)
  ret, err_msg = precheck_module.test()
  if not ret:
    print_timestamp("Pre-check %s failed with %s" % (options.ref_name, err_msg))
    sys.exit(MOD_PRECHECK_ERROR)
  sys.exit(MOD_SUCCESS)

def collect_data(options):
  """
  Calls collect_data function of input module.
  """
  collect_data_method_name = MODULE_COLLECT_DATA

  ret_dict = get_module_attribute(options.ref_name, collect_data_method_name)
  if ret_dict["module_error"] or ret_dict["attribute_error"]:
    raise LcmUpdateException(options.ref_name,
      ret_dict["module_error"] if ret_dict["module_error"]
                               else ret_dict["attribute_error"]
    )

  data_dict = {}
  if options.spec_version == MODULE_SPEC_V2:
    # pass in the params dict.
    params = {}
    params["entity_model"] = options.entity_model
    params["request_version"] = options.request_version
    params["entity_id"] = options.entity_id
    leader_params = get_leader_specific_params(options)
    if leader_params:
      params.update(leader_params)

    try:
      data_dict = ret_dict["attribute"](params)
    except Exception as ex:
      print_timestamp(traceback.format_exc())
      err_dict = {
          "name": options.ref_name,
          "err_msg": "Collect_data failed with error: [%s]" % str(ex),
          "stage": "collect_data"
          }
      print_timestamp("EXCEPT:%s" % json.dumps(err_dict))
      sys.exit(MOD_EXCEPTION_ERROR)
  else:
    # Non V2 specs
    try:
      data_dict = ret_dict["attribute"](options.entity_id, options.entity_model,
                            options.request_version)
    except Exception as ex:
      print_timestamp(traceback.format_exc())
      err_dict = {
            "name": options.ref_name,
            "err_msg": "Collect_data failed with error: [%s]" % str(ex),
            "stage": "collect_data"
          }
      print_timestamp("EXCEPT:%s" % json.dumps(err_dict))
      sys.exit(MOD_EXCEPTION_ERROR)

  #The standard output should only include data which is
  #used as the collected data by lcm_ops_by_phoenix script
  print_timestamp("#COLLECT_DATA_START#")
  print("%s" % json.dumps(data_dict))
  print_timestamp("#COLLECT_DATA_END#")
  sys.exit(MOD_SUCCESS)

def upgrade_status(options, upgrade_stage=0):
  """
  Calls upgrade_status function of input module.
  """
  if options.spec_version != MODULE_SPEC_V2:
    raise LcmUpdateException(options.ref_name,
    "Update module needs to conform to spec V2 to support async upgrade")

  options.image = handle_image_name(options.image)

  if options.upgrade_status_op:
    upgrade_method_name = MODULE_UPGRADE_STATUS

  if upgrade_stage > 0:
    upgrade_method_name += ("_%d" % upgrade_stage)

  print_timestamp("Attempting to load update_status method %s from update"
        "module %s" % (upgrade_method_name, options.ref_name))
  ret_dict = get_module_attribute(options.ref_name, upgrade_method_name)
  if ret_dict["module_error"]:
    # For upgrade status checking phase, in some case, the update package
    # can be wiped out from the upgrade environment, so instead of raising
    # an exeption, lcm_helper exits with a different value which tells LCM
    # framework an module import error happened where a re-staging of the
    # modules is needed.
    print_timestamp(ret_dict["module_error"])
    sys.exit(MOD_IMPORT_ERROR)

  if ret_dict["attribute_error"]:
    raise LcmUpdateException(options.ref_name,
      ret_dict["module_error"] if ret_dict["module_error"]
                               else ret_dict["attribute_error"]
    )
  print_timestamp("Upgrade_status method %r loaded from module %r" % (ret_dict["attribute"],
                                                    ret_dict["module"]))

  # pass in the params dict. TODO: Make this a stricter enforcement.
  print_timestamp("V2_spec module.")
  params, opened = get_params(options)
  leader_params = get_leader_specific_params(options)
  if leader_params:
    params.update(leader_params)

  data_dict = {}
  try:
    data_dict = ret_dict["attribute"](params)
  except Exception as ex:
    print_timestamp(traceback.format_exc())
    err_dict = {
        "name": options.ref_name,
        "err_msg": "Update_status failed with error: [%s]" % str(ex),
        "stage": upgrade_stage
        }
    print_timestamp("EXCEPT:%s" % json.dumps(err_dict))
    sys.exit(MOD_EXCEPTION_ERROR)
  finally:
    if opened:
      params["image_details"].close()

  # Send the Scratchpad back to the framework through stdout,
  # where the framework will parse and extract the Scratchpad.
  if options.scratchpad:
    print_timestamp("#SCRATCHPAD_START#")
    print(Scratchpad.get_json_dict())
    print_timestamp("#SCRATCHPAD_END#")

  #The standard output should include the returned dict
  #from the upgrade_status function
  print_timestamp("#ASYNC_STATUS_START#")
  print("%s" % json.dumps(data_dict))
  print_timestamp("#ASYNC_STATUS_END#")
  sys.exit(MOD_SUCCESS)

def get_params(options):
  """
  Parse the command options to generate the params
  """
  params = {}
  try:
    opened = False
    if options.image is not None:
      image_list = json.loads(options.image)
      print_timestamp("RIM  based update")
      # This will be a list of dictionaries with details about all images
      # required for both entity and sub-entities upgrade
      params["image_details"], opened = get_image_details(image_list, "RIM")
      # For sub-entity support, use "versioned_image_details" as this param
      # have info about version mapping of images.
      # Format: [{
      #             "image_version": "v1",
      #             "entity_class": "class",
      #             "entity_model": "model",
      #             "images": [{
      #                         "file_path": <>,
      #                         "location": <>,
      #                         "image_name": <>,
      #                         "size": <>,
      #                         "digest": <>,
      #                         "digest_type": <>
      #                         }]
      #          }]
      params["versioned_image_details"] = image_list
  except (ValueError, TypeError) as vex:
    # We have to catch TypeError also, as in ESX, we have python 3.5 and
    # json.loads() gives TypeError for type(Bytes) if its not in json format
    # as in case of legacy module.
    print_timestamp("Legacy based update")
    params["image_details"], opened = get_image_details(
      options.image, "Legacy")

  params["image_version"] = options.request_version
  params["entity_model"] = options.entity_model
  params["request_version"] = options.request_version
  params["entity_id"] = options.entity_id
  if options.entity_class:
    params["entity_class"] = options.entity_class
  if options.from_version:
    params["from_version"] = options.from_version
  if options.task_uuid:
    params["task_uuid"] = options.task_uuid
  if options.ipmi_cred:
    params["ipmi_cred"] = options.ipmi_cred
  if options.lcm_family:
    params["lcm_family"] = options.lcm_family
  if options.aos_version:
    params["aos_version"] = options.aos_version
  if options.disable_early_finish:
    params["disable_early_finish"] = True
  return params, opened

if __name__ == "__main__":
  msg = """
  Usage:
    Inventory:
    ./lcm_helper -i <ref_name> - For inventory
    ./lcm_helper -u --entity_id <entity_id> --entity_model <model> --image <firmware_bits>
                  --upgrade_sno <us> --collect_data --async_upgrade --async_status
        ref_name      - reference name of the module
        entity_id     - a unique identifier for an entity (required only for
                        upgrade)
        entity_model  - entity model (required only for upgrade)
        image         - filename of firmware bits (required only for upgrade)
        upgrade_sno   - upgrade stage number, an integer. This will tell the
                        framework to run the given upgrade stage. If not
                        provided, it is assumed that there is a single
                        upgrade stage named 'upgrade'.
        collect_data  - collect the data from module
        async_upgrade - perform asynchronous upgrade
        async_status  - perform status collection for asynchronous upgrade
  """
  try:
    if sys.version_info[0] == 3:
      import gflags
      # Using an empty list to initialize gflags to avoid
      # UnparsedFlagAccessError when accessing gflags-dependent modules.
      gflags.FLAGS([])
      from nutanix.tools import ptest_imports
      if ptest_imports.is_ptest_mode():
        ptest_imports.load_flags_from_env()

  except Exception as e:
    print_timestamp("INFO: This module is meant to be imported only on CVM. Import "
          "error raised is %s." % e)
    pass

  try:
    options = parse_options()
  except Exception as ex:
    print_timestamp(traceback.format_exc())
    err_msg = "Module invocation was incorrect, error: %s" % str(ex)
    err_dict = {
        "name": "",
        "err_msg": err_msg
    }
    err_msg = "EXCEPT: %s" % json.dumps(err_dict)
    sys.exit(MOD_EXCEPTION_ERROR)

  if PY3 and options.check_for_python_venv:
    python_binary = os.path.realpath('/proc/%s/exe' % os.getpid())
    if (python_binary.strip() != os.path.realpath(
        options.check_for_python_venv)):
      print_timestamp("script not invoked with python3 virtual env, returning")
      sys.exit(MOD_PY_VENV_ERROR)

  if not options.collect_data_op:
    print_timestamp("lcm_helper invoked as %s" % (options))

  # Initialize Scratchpad if passed in (only during Upgrades)

  if options.scratchpad is not None:
   Scratchpad.initialize(options.scratchpad)

  # Run Inventory operation
  if options.inv_op and options.ref_name:
    inventory(options)

  # Run Upgrade operation
  elif options.update_op:
    kwargs=dict()
    # Earlier, image was mandatory so we only updated the stage if image
    # was provided, skipping this for intersight
    if options.image or (options.env and options.env == "leader"):
      kwargs.update(upgrade_stage=options.upgrade_sno)
    print_timestamp("kwargs before invoking update: %r" % (kwargs))
    update(options, **kwargs)

  # Run Precheck operation
  elif options.precheck_op:
    precheck(options)

  # Run Collect_data operation
  elif options.collect_data_op:
    collect_data(options)

  # Run upgrade_status operation
  elif options.upgrade_status_op:
    upgrade_status(options, upgrade_stage=options.upgrade_sno)

  # Run the preupgrade operation
  elif options.preupgrade_op:
    preupgrade(options)
