# Copyright (c) 2022-2023 VMware, Inc.  All rights reserved.
# All rights reserved. -- VMware Confidential

"""Entropy utils
"""

import ctypes
import logging
import os
from vmware import vsi
from vmware.runcommand import runcommand, RunCommandError

ENTROPY_TARDISK_NAME = 'entcache.tgz'
STORAGE_ENTROPY_CACHE = 'etc/entropy-cache'
USERSPACE_ENTROPY = 8

# These align with the values supported by entropyd.
ENTROPY_MIN_LENGTH = 512 * 1024 # 512KiB
ENTROPY_MAX_LENGTH = 10 * 1024 * 1024 # 10MiB

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

class EntropyError(Exception):
   pass


class EntropyCacheMetadata(ctypes.Structure):
   """Metadata of entropy-cache
   """
   _fields_ = [
      ('queueSize', ctypes.c_ssize_t),
      ('front', ctypes.c_ssize_t),
      ('rear', ctypes.c_ssize_t),
      ('length', ctypes.c_ssize_t),
   ]


def userspaceEntropyEnabled(strict=False):
   """Check if userspace entropy is configured.
      For strict=True, it will check if sources is set to 8, otherwise it just
      checks if the bit for 8 is set.
   """
   try:
      entropySources = vsi.get('/system/nrandom/entropySources')
   except ValueError:
      # The vsi node is not present in older release. Just return here.
      return False
   return int(entropySources) == USERSPACE_ENTROPY if strict \
      else int(entropySources) & USERSPACE_ENTROPY != 0


def checkEntropyCache(cacheFile=STORAGE_ENTROPY_CACHE):
   """Check if entropy cache flle 'cacheFile' is full. If not, then raise
      exception.

      Check is done if only the userspace entropy source is enabled. For the
      check, queue-size is compared to queue-length from the metadata.

      If non-standard cacheFile argument is passed, check for VSI node value is
      skipped. This is needed in weasel environment for ISO upgrade scenario
      where the VSI node only has default values and entropy storage cache is
      not loaded in ramdisk.
   """
   if cacheFile == STORAGE_ENTROPY_CACHE and \
      not userspaceEntropyEnabled(strict=True):
      return

   with open(os.path.join('/', cacheFile), 'rb') as cacheFile:
      metadata = cacheFile.read(32).hex()
      if len(metadata) < 32:
         raise EntropyError('Storage entropy cache is not valid.')
      elif metadata[:16] != metadata[-16:]:
         raise EntropyError('Storage entropy cache is not full. A full entropy '
                            'cache is required for any image operation. Refer '
                            'to KB 89854 for steps on how to refill the cache.')

def isEntropydRunning():
   """Returns True if entropyd is running, False otherwise.

      Raises an EntropyError exception if the command cannot be executed.
   """
   try:
      cmd = "/etc/init.d/entropyd status"
      rc, out = runcommand(cmd)
   except RunCommandError as e:
      raise EntropyError(str(e))

   out = out.decode().strip()
   if rc == 0 and "is running" in out:
      return True

   log.warning("Entropyd is likely not running. Reason: %s, rc: %d", out, rc)
   return False

