# Copyright 2018-2021 VMware, Inc.
# All rights reserved. -- VMware Confidential

"""System VMFS-L volumes handling.

Classes and functions to retrieve information and create System VMFS-L file
systems.
"""
import os
import logging
import ctypes
from ctypes import cdll
from uuid import uuid4

try:
   from esxLogging import callFuncRedirStderr
except ImportError:
   # Legacy vSphere version, limited module availability
   pass

from esxutils import runCli
from systemStorage import *
from systemStorage.esxdisk import iterDisks, EsxDisk
from systemStorage.esxfs import (EsxFsUuid, FssVolume, getFssVolumes,
                                 getVolumeFullPath, SIZEOF_FS_UUID,
                                 waitFsMounted, waitFsUmounted)
from vmware import vsi

# ctypes.cdll reference hook to libsysstorage
_ssLib = None
SYS_STORAGE_LIB = "libsysstorage.so"

# VMFS defaults
VMKFSLIB_DEFAULT_BLOCKSIZE = MiB

# VMFS mount / umount timeout in seconds. Core-storage makes no specific
# recommendation on an absolute timeout value after which a disk should be
# considered as unresponsive. 60 seconds is quite generous on physical, but
# a large enough timeout is needed to account for nested ESX cases where
# guest I/O's can be very slow if the outer host is overloaded.
VMFS_MOUNT_TIMEOUT = 60

logger = logging.getLogger(os.path.basename(__file__))

# ctypes copied from bora/apps/voma/fstools/symbols/vmfsSymbols.py
class FDS_VolInfo(ctypes.Structure):
   _fields_ = [('id', ctypes.c_uint8 * 32)]
   _pack_ = 1

class FS3_Checksum(ctypes.Structure):
   _fields_ = [('value',       ctypes.c_uint64),
               ('checksumGen', ctypes.c_uint64)]
   _pack_ = 1

# FS3_Descriptor is stable across releases in the VMFS header
class FS3_Descriptor(ctypes.Structure):
    _fields_ = [('magic',                  ctypes.c_uint32),
                ('majorVersion',           ctypes.c_uint32),
                ('minorVersion',           ctypes.c_uint8),
                ('uuid',                   ctypes.c_ubyte * SIZEOF_FS_UUID),
                ('config',                 ctypes.c_uint32),
                ('fsLabel',                ctypes.c_char * 128),
                ('diskBlockSize',          ctypes.c_uint32),
                ('fileBlockSize',          ctypes.c_uint64),
                ('creationTime',           ctypes.c_uint32),
                ('snapID',                 ctypes.c_uint32),
                ('volInfo',                FDS_VolInfo),
                ('fdcClusterGroupOffset',  ctypes.c_uint32),
                ('fdcClustersPerGroup',    ctypes.c_uint32),
                ('subBlockSize',           ctypes.c_uint32),
                ('maxJournalSlotsPerTxn',  ctypes.c_uint32),
                ('pb2VolAddr',             ctypes.c_uint64),
                ('pb2FDAddr',              ctypes.c_uint32),
                ('hostUuid',               ctypes.c_ubyte * 16),
                ('gblGeneration',          ctypes.c_uint64),
                ('sddVolAddr',             ctypes.c_uint64),
                ('sddFDAddr',              ctypes.c_uint32),
                ('checksumType',           ctypes.c_uint8),
                ('unmapPriority',          ctypes.c_uint16),
                ('pad1',                   ctypes.c_uint8 * 4),
                ('checksumGen',            ctypes.c_uint64),
                ('checksum',               FS3_Checksum),
                ('physDiskBlockSize',      ctypes.c_uint32),
                ('mdAlignment',            ctypes.c_uint32),
                ('sfbToLfbShift',          ctypes.c_uint16),
                ('reserved16_1',           ctypes.c_uint16),
                ('reserved16_2',           ctypes.c_uint16),
                ('ptrBlockShift',          ctypes.c_uint16),
                ('sfbAddrBits',            ctypes.c_uint16),
                ('reserved16_3',           ctypes.c_uint16),
                ('tbzGranularity',         ctypes.c_uint32),
                ('journalBlockSize',       ctypes.c_uint32),
                ('leaseIntervalMs',        ctypes.c_uint32),
                ('reclaimWindowMs',        ctypes.c_uint32),
                ('localStampUS',           ctypes.c_uint64),
                ('localMountOwnerMacAddr', ctypes.c_uint8 * 6)]
    _pack_ = 1


def _initSystemStorageLib():
   global _ssLib
   if _ssLib is None:
      _ssLib = cdll.LoadLibrary(SYS_STORAGE_LIB)

def toCtypeCharPtr(s, encoding='utf-8'):
   """Get a ctype char* for calling into C-type API
   """
   return ctypes.c_char_p(s.encode(encoding))

def createVMFS(devicePath, fsType, volumeLabel,
               blockSize=VMKFSLIB_DEFAULT_BLOCKSIZE, numFiles=0, flags=0,
               appendUUID=False):
   """Format a partition to VMFS(-L).
   """
   _initSystemStorageLib()

   if fsType == FS_TYPE_VMFS:
      fsType = 'vmfs6'
   elif fsType == FS_TYPE_VMFS_L:
      fsType = 'vmfs6l'
      flags = FS_CREATE_OSDATA
   else:
      raise ValueError("%s: unsupported filesystem type" % fsType)

   # If appendUUID, create a temporary name with a random UUID4 string.
   # The UUID value is not used as input to CreateFS, so it's renamed
   # after creating the volume further below.
   fsLabel = volumeLabel
   if appendUUID:
      fsLabel += "-%s" % uuid4().hex

   uuidBuffer = bytearray(SIZEOF_FS_UUID)
   uuid_t = ctypes.c_ubyte * SIZEOF_FS_UUID

   # Call vmkfslib CreateFS with stderr redirected
   err, log = callFuncRedirStderr(_ssLib.VMFS_CreateFS,
                                  toCtypeCharPtr(devicePath),
                                  toCtypeCharPtr(fsType),
                                  toCtypeCharPtr(fsLabel),
                                  ctypes.c_uint32(blockSize),
                                  ctypes.c_uint32(numFiles),
                                  ctypes.byref(uuid_t.from_buffer(uuidBuffer)),
                                  ctypes.c_uint32(flags))
   if err < 0:
      raise OSError("%s: failed to format %s volume (rc=%d): [%s]" %
                    (devicePath, fsType, err, log))

   uuid = EsxFsUuid(uuidBuffer)

   # Wait until the new VMFS volume is mounted
   mountPoint = getVolumeFullPath(fsLabel)
   waitFsMounted(mountPoint, timeout=VMFS_MOUNT_TIMEOUT)

   if appendUUID:
      # Rename the volume with the new UUID suffix.
      volumeLabel += "-%s" % uuid
      try:
         setVolumeName(fsLabel, volumeLabel)
      except Exception as e:
         raise IOError("%s: failed to rename %s volume: %s" %
                       (mountPoint, fsType, e))

   return uuid

def setVolumeName(oldName, newName):
   """Set or rename a mounted filesystem.

   oldName can be either the UUID or filesystem label in /vmfs/volumes.
   """
   _initSystemStorageLib()

   path = getVolumeFullPath(oldName)
   err, log = callFuncRedirStderr(_ssLib.VMFS_SetVolumeName,
                                  toCtypeCharPtr(path),
                                  toCtypeCharPtr(newName))
   if err < 0:
      raise IOError("%s: VMFS_SetVolumeName() error (rc=%d): [%s]" %
                    (path, err, log))

def getVmfsDesc(partDev):
   """Get the fsDesc of a VMFS(-L) volume.
   """
   _initSystemStorageLib()

   fsDesc = FS3_Descriptor()
   sz = ctypes.sizeof(fsDesc)
   err = _ssLib.VMFS_GetVmfsDesc(toCtypeCharPtr(partDev),
                                 ctypes.byref(fsDesc), ctypes.c_uint32(sz))
   if err < 0:
      raise ValueError("%s: not a VMFS(-L) volume (err=%d)" % (partDev, err))

   return fsDesc

def getSystemVmfslDesc(partDev):
   """Get the fsDesc of a System VMFS-L volume.
   """
   fsDesc = getVmfsDesc(partDev)
   if not (fsDesc.config & FS_CONFIG_SYSTEM):
      raise ValueError("%s: not a system VMFS-L volume" % partDev)
   return fsDesc

def vmfsMount(uuid):
   """Mount a VMFS volume.
   """
   runCli(['storage', 'filesystem', 'mount', '--volume-uuid', str(uuid)])

def vmfsUnmount(uuid, nonPersist=True, waitUnmounted=True):
   """Unmount a VMFS volume.

   @param nonPersist if True then the filesystem is temporary unmounted until
                     the next reboot.
   @param waitUnmounted indicates if the function will wait until the filesystem
                        is unmounted.
   """
   cmd = ['storage', 'filesystem', 'unmount', '--volume-uuid', str(uuid)]
   if nonPersist:
      cmd.append('-n')

   # Call with stderr redirected because of IORM printing out not .pid files
   callFuncRedirStderr(runCli, cmd)

   if waitUnmounted:
      waitFsUmounted(uuid, timeout=VMFS_MOUNT_TIMEOUT)

class SystemVmfslVolume(FssVolume):
   """Hold information about an OS-Data volume.
   """
   def __init__(self, vsiInfo):
      super().__init__(vsiInfo)
      fsDesc = getSystemVmfslDesc(self.devPath)

      assert self.uuid == EsxFsUuid(fsDesc.uuid)
      assert self.label == fsDesc.fsLabel.decode('utf-8')


def findOsdata(diskName=None):
   """Find the system OSDATA partitions.
   """
   return findSystemVmfsl(diskName, skipLocker=True)

def findSystemVmfsl(diskName=None, skipLocker=False):
   """Find VMFS-L volumes on the specified disk, or all disks if None.

   If multiple VMFS-L partitions are found, only the first one is returned in
   the following order of precedence: 1) fast+boot, 2) fast, 3) boot.

   @param skipLocker - skip locker(USB) devices when looking for VMFS-L volumes.
   """
   # Create an index of disks to search for VMFS-L, all disks are scanned if no
   # diskName is specified.
   disks = list(iterDisks()) if diskName is None else [EsxDisk(diskName)]
   disks = {disk.name: disk for disk in disks if not skipLocker or
                                                 disk.supportsOsdata}
   if not disks:
      raise OSError("no OSDATA-compatible disk found")

   # Iterate through each volume to see if it is a system VMFS-L type
   volumes = []

   for vmfslVolume in getFssVolumes(diskName=diskName, fsType=FS_TYPE_VMFS_L):
      try:
         disk = disks[vmfslVolume.diskName]
      except KeyError:
         continue

      try:
          volume = SystemVmfslVolume.fromFssVolume(vmfslVolume)
      except ValueError:
         # Not a system VMFS-L partition.
         continue

      if disk.isActiveBootDisk:
         volumes.insert(0, volume)
      else:
         volumes.append(volume)

   try:
      return volumes[0]
   except IndexError:
      raise OSError("no System VMFS-L partition found")

def getVmfsLabel(volumePath):
   """Retrieve the VMFS(-L) partition label.
   """
   fsDesc = getVmfsDesc(volumePath)
   return fsDesc.fsLabel.decode('utf-8')
