########################################################################
# Copyright (C) 2010-2020 VMware, Inc.
# All Rights Reserved
########################################################################

"""This module contains functions to query host related infomation
"""
import os
import logging
import re

import pyvsilib

log = logging.getLogger('HostInfo')

from vmware.runcommand import runcommand
from .. import Errors
from .Misc import byteToStr

PXE_BOOTING = None
SECURE_BOOTED = None

#
#  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.
   """
   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 = pyvsilib.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 pyvsilib.
       returns '', '' if attributes are not available.
   """
   try:
      dmiInfo = pyvsilib.get('/hardware/bios/dmiInfo')
      return dmiInfo.get('vendorName', ''), dmiInfo.get('productName', '')
   except Exception as e:
      log.warn('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.
   """
   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.warn('%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.warn("%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
   '''
   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 = pyvsilib.get(BOOTMAC_VSI_NODE)['macAddrStr']
         bootCmdLine = pyvsilib.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
   '''
   global SECURE_BOOTED

   if SECURE_BOOTED is None:
      SECURE_BOOT_STATUS_VSI_NODE = '/secureBoot/status'
      try:
         vsiOut = pyvsilib.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 GetEsxVersion():
   '''Get 3-digit ESXi version.
   '''
   VERSION_VSI_NODE = '/system/version'
   verInfo = pyvsilib.get(VERSION_VSI_NODE)
   return verInfo['productVersion']

def _getFileSystems():
   """Call esxcli storage file systel list and return the result.
   """
   from esxutils import EsxcliError, runCli
   try:
      return runCli(['storage', 'filesystem', 'list'], 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.
   '''
   fsList = _getFileSystems()

   realPath = os.path.realpath(fsPath)
   for fs in fsList:
      if fs['Mount Point'] and realPath.startswith(fs['Mount Point']):
         # Sub-path is accepted, thus 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 GetVmfslFileSystems():
   """Return a list of full paths to VMFS-L file systems.
   """
   return [fs['Mount Point'] for fs in _getFileSystems()
           if fs['Type'] == 'VMFS-L']

def IsFirmwareUefi():
   '''Check if the system has booted with UEFI.
   '''
   return pyvsilib.get('/hardware/firmwareType') == 1
