########################################################################
# Copyright (c) 2010-2024 Broadcom. All Rights Reserved.
# Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
########################################################################

"""This module contains functions to query host related infomation.
   Warning: Any esx-only import must be under the check IS_ESX_ESXIO.
"""
import os
import logging
import re

log = logging.getLogger('HostInfo')

from .. import IS_ESX_ESXIO
if IS_ESX_ESXIO:
   from vmware import vsi
   from vmware.runcommand import runcommand

from .. import Errors
from .. import MIB
from .Misc import byteToStr


VMFS_VOLUMES = os.path.join(os.sep, 'vmfs', 'volumes')

PXE_BOOTING = None
SECURE_BOOTED = None
UNSUPPORTED_MSG = "This function is not supported on a non-ESX system."

#
#  Host has to be in maintenance mode before the transaction, if a VIB
#   being installed/removed requires maintenance mode.
#
#  Even better would be to enter maintenance mode ourselves -- that has a
#    much stronger guarantee, that no ops are in flight to start/stop a VM,
#    for example.  However, we do not do that for these reasons:
#    i) we'd have to exit maint mode, even for exceptions & errors;
#          - what happens if we have errors exiting maint mode?
#    ii) it's much more testing
#
#    TODO: If we do ever get around to entering maint mode, we should use
#    pyVim/pyVmomi, and connect to hostd via a local ticket (or file a
#    PR to do this).
#
def GetMaintenanceMode():
   """Returns True if the host is in maintenance mode, and False otherwise.
      MaintenanceModeError is thrown if vsi node cannot be read or vsi
      node is showing invalid/unknown status.
   """
   if not IS_ESX_ESXIO:
      raise RuntimeError(UNSUPPORTED_MSG)

   HOSTD = '/bin/hostd'
   if not os.path.exists(HOSTD):
      # If hostd is not present at all, then we are on a system like ESXCore,
      # where hostd has not been installed. In this - legitimate - case, there
      # is no maintenance mode really, but we still want to install VIBs that
      # require entering in maintenance mode. So our best option is to consider
      # that systems with no hostd are operating in maintenance mode.
      return True

   MMODE_VSI_NODE = '/system/maintenanceMode'
   MMODE_ERROR = 'Unable to determine if the system is in maintenance mode: ' \
                 '%s. To be safe, installation will not continue.'
   try:
      mmodeStatus = vsi.get(MMODE_VSI_NODE)
   except Exception as e:
      raise Errors.MaintenanceModeError(MMODE_ERROR % str(e))

   # 1 - enabled, 2 - disabled, otherwise unknown or invalid
   # See hostctl/include/system/SystemInfo.h
   if mmodeStatus == 1:
      return True
   elif mmodeStatus == 2:
      return False
   reason = 'Unknown or invalid maintenance mode status %d' % mmodeStatus
   raise Errors.MaintenanceModeError(MMODE_ERROR % reason)

def GetBiosVendorModel():
   """Returns the BIOS vendor name and model strings from VSI.
      returns '', '' if attributes are not available.
   """
   if not IS_ESX_ESXIO:
      raise RuntimeError(UNSUPPORTED_MSG)

   try:
      dmiInfo = vsi.get('/hardware/bios/dmiInfo')
      return dmiInfo.get('vendorName', ''), dmiInfo.get('productName', '')
   except Exception as e:
      log.warning('Failed to get BIOS vendor model: %s', e)
      return '', ''

def GetBiosOEMStrings():
   """Return the BIOS OEM String (type 11) entries.
      An empty list is return if none are found.

      @returns: A list of strings

      XXX: As of now the best source for this is the output of smbiosDump.
   """
   if not IS_ESX_ESXIO:
      raise RuntimeError(UNSUPPORTED_MSG)

   SMBIOSDUMP_CMD = '/sbin/smbiosDump'
   label = 'OEM String'
   if os.path.exists(SMBIOSDUMP_CMD):
      rc, out = runcommand([SMBIOSDUMP_CMD])
      out = byteToStr(out)
      if rc != 0:
         log.warning('%s returned nonzero status %d\nOutput:\n%s',
            SMBIOSDUMP_CMD, rc, out)
         return []

      heading = None
      indent = 0
      values = list()
      for line in out.split('\n'):
         # we're interested in this specific heading
         if label in line:
            heading = line.lstrip(' ')
            indent = len(line) - len(heading)
         elif not heading:
            continue
         else:
            val = line.lstrip(' ')
            if (len(line) - len(val)) > indent:
               # this line is indented further than the heading
               # that makes it a value
               values.append(val.rstrip())
            else:
               return values
   else:
      log.warning("%s command cannot be found", SMBIOSDUMP_CMD)
   return []

def IsPxeBooting():
   """Return True if host is booting from PXE, which is indicated by non-empty
      bootMAC.
      Stateless cache boot is also considered PXE booted to avoid stateful
      behaviors.
      Raises:
         InstallationError - If there was an error determining PXE boot status.
   """
   if not IS_ESX_ESXIO:
      raise RuntimeError(UNSUPPORTED_MSG)

   global PXE_BOOTING
   #
   # HostSimulator is not considered pxe boot even if the underlying
   # host booted up via pxe.
   #
   if HostOSIsSimulator():
      return 0
   if PXE_BOOTING is None:
      BOOTMAC_VSI_NODE = '/system/bootMAC'
      BOOTCMD_VSI_NODE = '/system/bootCmdLine'
      try:
         bootMAC = vsi.get(BOOTMAC_VSI_NODE)['macAddrStr']
         bootCmdLine = vsi.get(BOOTCMD_VSI_NODE)['bootCmdLineStr']
         PXE_BOOTING = (bootMAC != '') or ('statelessCacheBoot' in bootCmdLine)
      except Exception as e:
         msg = 'Unable to get boot MAC or boot command line, cannot ' \
               'determine PXE boot status: %s' % str(e)
         raise Errors.InstallationError(e, None, msg)
   return PXE_BOOTING

def HostOSIsSimulator():
   """Check if the host is running in a simulator.
   """
   return os.path.exists("/etc/vmware/hostd/mockupEsxHost.txt")

def GetContainerId():
   """Check if we are running in simulator environment and fetch the
      container ID. Return empty string otherwise.
   """
   ctId = ''

   if HostOSIsSimulator():
      # Read the container ID that is written in
      # '/etc/profile'.
      profileFilePath = '/etc/profile'
      pattern = "echo In container"

      if not os.path.exists(profileFilePath):
         msg = 'Cannot find file %s' % profileFilePath
         raise Errors.FileIOError(profileFilePath, msg)

      with open(profileFilePath, 'r') as profileFile:
         for line in profileFile:
            m = re.search(pattern, line)
            if m:
               ctId = line.strip().split(' ')[-1]
               ctId = ctId + '-'
               break
         else:
            msg = 'Unable to get container ID from file %s' % profileFilePath
            raise Errors.FileIOError(profileFilePath, msg)

   return ctId

def IsHostSecureBooted():
   """Check if the host is secure booted.
      @return True if secure booted
              False if not secure booted
   """
   if not IS_ESX_ESXIO:
      raise RuntimeError(UNSUPPORTED_MSG)

   global SECURE_BOOTED

   if SECURE_BOOTED is None:
      SECURE_BOOT_STATUS_VSI_NODE = '/secureBoot/status'
      try:
         vsiOut = vsi.get(SECURE_BOOT_STATUS_VSI_NODE)
         SECURE_BOOTED = vsiOut['attempted'] != 0
      except Exception as e:
         log.error(e)
         log.error("Encountered an exception while trying to check secure boot "
                   "status. Assuming secure boot is enabled.")
         # We should try to keep our system tight on security, and thus assume
         # that secure boot is enabled here.
         # This assumption shall help us in stopping unauthorized
         # vibs from being installed on the system.
         return 1
   return SECURE_BOOTED

def _getEsxVerInfo():
   """Return the ESXi version info VSI node.
   """
   if not IS_ESX_ESXIO:
      raise RuntimeError(UNSUPPORTED_MSG)

   VERSION_VSI_NODE = '/system/version'
   return vsi.get(VERSION_VSI_NODE)

def GetEsxVersion():
   """Get 3-digit ESXi version.
   """
   return _getEsxVerInfo()['productVersion']

def GetEsxVersionPair():
   """Return a pair of ESXi 3-digit version and patch release number.
   """
   verNode = _getEsxVerInfo()
   return verNode['productVersion'], verNode['releasePatch']

def _getFileSystemList(fsUuid=None):
   """Call esxcli storage filesystem list and return the result in a list.
      fsUuid: if given, provide --uuid/-u option to the command. The return
              is either a single-member list or an error would be raised.
   """
   from esxutils import EsxcliError, runCli
   try:
      cmd = ['storage', 'filesystem', 'list']
      if fsUuid:
         cmd += ['-u', fsUuid]
      return runCli(cmd, True)
   except EsxcliError as e:
      msg = 'Failed to query file system stats: %s' % str(e)
      raise Errors.InstallationError(e, None, msg)

def _getFsStats(fsPath):
   """Get stats of a filesystem with localcli.
   """
   # Real path turns a volume name to UUID.
   realPath = os.path.realpath(fsPath)

   # Try to find the volume UUID.
   volRelPath = os.path.relpath(realPath, VMFS_VOLUMES)
   firstSep = volRelPath.find(os.sep)
   volUuid = volRelPath[:firstSep] if firstSep != -1 else volRelPath
   if len(volUuid) > 16:
      # Shortest UUID of a volume is of format <8>-<8> for an NFS volume.
      # Try using the --uuid/-u option.
      try:
         return _getFileSystemList(fsUuid=volUuid)[0]
      except Errors.InstallationError as e:
         # ESXi 6.7 does not have the -u option; volUuid can also be a long
         # name.
         log.error('Failed to call filesystem list with -u option: %s, fall '
                   'back to full filesystem list', e)

   fsList = _getFileSystemList()
   for fs in fsList:
      if fs['Mount Point'] and realPath.startswith(fs['Mount Point']):
         # Mount point must be a part of the path being looked up.
         return fs

   msg = 'Cannot find filesystem with path %s' % realPath
   raise Errors.InstallationError(None, None, msg)

def GetFsFreeSpace(fsPath):
   """Get current available space of a filesystem.
      @input:
         fsPath - path to the filesystem, can be a sub-path
      @return:
         Available space in bytes
   """
   fs = _getFsStats(fsPath)
   return fs['Free']

def GetFsSize(fsPath):
   """Get size of a filesystem.
      @input:
         fsPath - path to the filesystem, can be a sub-path
      @return:
         Size in bytes
   """
   fs = _getFsStats(fsPath)
   return fs['Size']

def GetVmfsOSFileSystems():
   """Return a list of full paths to VMFSOS file systems.
   """
   return [fs['Mount Point'] for fs in _getFileSystemList()
           if fs['Type'] == 'VMFSOS']

def IsFirmwareUefi():
   """Check if the system has booted with UEFI.
   """
   if not IS_ESX_ESXIO:
      raise RuntimeError(UNSUPPORTED_MSG)

   return vsi.get('/hardware/firmwareType') == 1

def IsDiskBacked(path):
   """Returns True if the path exists and is backed by disk
      False otherwise
   """
   return os.path.exists(path) and \
      os.path.realpath(path).startswith(VMFS_VOLUMES)

def IsFreeSpaceAvailable(path, requiredSpace, reserveRatio=0,
                         minimumReserveSpace=0):
   """Returns True if the requiredSpace(in MIB) amount of space is
      available after reserving either reserveRatio of total space or
      minimumReserveSpace, whichever is higher. For example if a disk
      is of size 10 GB and reserveRatio is 0.1, then this function will
      return true if requiredSpace at the path is <= 9GB
   """
   if not IsDiskBacked(path):
      msg = 'IsFreeSpaceAvailable works only with disk-backed FileSystem paths'
      raise Errors.FileIOError(msg)

   fs = _getFsStats(path)
   totalSpace = fs['Size'] // MIB
   freeSpace = fs['Free'] // MIB
   reservedSpace = max(round(totalSpace * reserveRatio), minimumReserveSpace)

   return requiredSpace <= (freeSpace - reservedSpace)

def IsTpmActive():
   """Checks if TPM is active in the system. Returns True if TPM is active,
      False otherwise. If we cannot get the TPM active status, regard it as
      TPM is not active.
   """
   if not IS_ESX_ESXIO:
      raise RuntimeError(UNSUPPORTED_MSG)

   try:
      return bool(vsi.get("/hardware/tpm/active"))
   except Exception as e:
      log.exception('Failed to determine whether TPM is active: %s', str(e))
      return False

def hasManagedDpus():
   """Checks if the host manages one or more DPU(s). Returns True if it does,
      False otherwise. If we cannot get DPU information, regard it as
      the host does not manage a DPU.
   """
   from ..ESXioImage.DpuLib import getManagedDpuInfo
   try:
      dpuList = getManagedDpuInfo()
      return bool(len(dpuList))
   except Exception as e:
      log.exception("Failed to determine whether the host manages a DPU: %s",
                    str(e))
      return False

def IsInstaller():
   """Checks if the system currently runs an installer. Returns True if it does,
      False otherwise.
   """
   if not IS_ESX_ESXIO:
      raise RuntimeError(UNSUPPORTED_MSG)

   try:
      BOOTCMD_VSI_NODE = '/system/bootCmdLine'
      bootCmdLine = vsi.get(BOOTCMD_VSI_NODE)['bootCmdLineStr']
      return re.search(r"(^|\s)(runweasel|ks=\S)", bootCmdLine) != None
   except Exception as e:
      log.exception("Failed to determine whether the system is an ESXi "
                    "installer: %s", str(e))
      return False

def getMaxMemAllocation(groupName):
   """Get the max allocated mem for a group
   """
   try:
      grpId = vsi.set("/sched/groupPathNameToID", groupName)
      tempRes = vsi.get("/sched/groups/" +
                        str(grpId) +
                        "/memAllocationInMB")["max"]
      return tempRes
   except Exception as e:
      log.exception("Failed to determine max memory allocation for "
                    "group: %s Error: %s", groupName, str(e))
      raise
