#!/usr/bin/python
########################################################################
# Copyright (C) 2010,2019-2020 VMWare, Inc.                            #
# All Rights Reserved                                                  #
########################################################################

import logging
import os
import posixpath
import shutil
import sys
import tarfile
import time

if sys.version_info[0] >= 3:
   from io import BytesIO
else:
   try:
      from StringIO import StringIO as BytesIO
   except ImportError:
      # This module sometimes used in an ESXi 4.0.0 environment, which does
      # not have StringIO but it does have cStringIO
      from cStringIO import StringIO as BytesIO

from . import (Bulletin, Errors, ImageProfile, ReleaseCollection, Vib,
               VibCollection)
from .ReleaseCollection import genReleaseUnitFileName
from .Utils import XmlUtils
from .Utils.Misc import byteToStr, isString

etree = XmlUtils.FindElementTree()

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

def _PopulateDirs(dbroot, forcePosixPath=False):
   """Return a dict of db sub-folder paths.
      @forcePosixPath: if set, sub-folder paths will be in Posix
      format regardless of the host.
   """
   joinFunc = posixpath.join if forcePosixPath else os.path.join
   return \
      {'vibs'               : joinFunc(dbroot, 'vibs'),
       'bulletins'          : joinFunc(dbroot, 'bulletins'),
       'profiles'           : joinFunc(dbroot, 'profiles'),
       'baseimages'         : joinFunc(dbroot, 'baseimages'),
       'addons'             : joinFunc(dbroot, 'addons'),
       'solutions'          : joinFunc(dbroot, 'solutions'),
       'manifests'          : joinFunc(dbroot, 'manifests'),
       'reservedComponents' : joinFunc(dbroot, 'reservedComponents'),
       'reservedVibs'       : joinFunc(dbroot, 'reservedVibs')}

def _RaiseException(e, msg):
   """ Raise an appropriate database I/O or format error based on the
       category of the provided error.
   """
   cls = e.__class__
   if cls in (Errors.BulletinIOError, Errors.ComponentIOError,
              Errors.VibIOError, Errors.ProfileIOError,
              Errors.ReleaseUnitIOError):
      raise Errors.DatabaseIOError(msg)
   raise Errors.DatabaseFormatError(msg)

def _LoadBulletins(collection, dirName, dbpath):
   try:
      # On a legacy host, there is no components in the database
      if os.path.exists(dirName):
         collection.FromDirectory(dirName)
   except Exception as e:
      msg = 'Could not parse Bulletin xml from database %s: %s' % (dbpath, e)
      _RaiseException(e, msg)

def _LoadVibs(collection, dirName, dbpath, validate, isReserved=False):
   if isReserved and not os.path.exists(dirName):
      # Similar to release units, the reserved VIB dir may not exist.
      return
   try:
      collection.FromDirectory(dirName, validate=validate)
   except Exception as e:
      msg = 'Could not parse Vib xml from database %s: %s' % (dbpath, e)
      _RaiseException(e, msg)

def _LoadProfiles(collection, dirName, dbpath, validate):
   try:
      collection.FromDirectory(dirName, validate=validate)
   except Exception as e:
      msg = 'Could not parse ImageProfile from database %s: %s' %  (
            dbpath, e)
      _RaiseException(e, msg)

def _LoadReleaseUnits(collection, dirName, dbpath, metaType):
   try:
      # Unless the host is imaged with an image spec, there is no baseimage
      # or addon in the database.
      if os.path.exists(dirName):
         collection.FromDirectory(dirName)
   except Errors.ReleaseUnitIOError as e:
      msg = 'Could not parse %s from database %s: %s' % (metaType, dbpath, e)
      _RaiseException(e, msg)

def _ExtractMember(path, t):
   ret = None
   try:
      member = t.getmember(path)
      if member is not None and member.size != 0:
         memFile = t.extractfile(member)
         ret = memFile.read()
         memFile.close()
   except KeyError:
      pass
   return ret

def _AddVibXmlToCollection(collection, infoName, t, content, validate):
   # skip signature and original descriptor files
   if not infoName.endswith(Vib.EXTENSION_ORIG_DESC) and \
      not infoName.endswith(Vib.EXTENSION_ORIG_SIG):
      sigpath = infoName + Vib.EXTENSION_ORIG_SIG
      signature = _ExtractMember(sigpath, t)

      origpath = infoName + Vib.EXTENSION_ORIG_DESC
      origdesc = _ExtractMember(origpath, t)

      collection.AddVibFromXml(content, origdesc=origdesc, signature=signature,
                               validate=validate)

def _CollectReservedComponentIDs(relUnits, components):
   reservedCIDs = set()
   for ut in relUnits.values():
      reservedCIDs.update(set(ut.CollectReservedComponents(components)))
   return reservedCIDs

class Database(object):
   """Implements a database to track an Image Profile and its VIBs.

      Class Variables:
         * VISOR_PATH_LIMIT  - The file path length limit in VisorFS, see
                               VISORFS_ERROR_PATHLEN in visorfs_int.h.
      Attributes:
         * dbpath     - File path to the Database
         * vibs       - An instance of VibCollection class, representing the
                        VIBs in the database.
         * profiles   - An instance of ImageProfileCollection, representing the
                        host ImageProfile.
         * bulletins  - An instance of BulletinCollection, representing the
                        components in the database.
         * baseimages - An instance of SolutionCollection, representing the
                        base images in this database.
         * addons     - An instance of AddonCollection, representing the
                        addons in this database.
         * solutions  - An instance of SolutionCollection, representing the
                        solutions in the database.
         * manifests  - An instance of ManifestCollection, representing the
                        manifests in the database.
         * reservedComponents - An instance of ComponentCollection, representing
                                the components that are a part of the addons and
                                base images but not in the components in the
                                database.
   """
   VISOR_PATH_LIMIT = 127

   def _InitCollections(self):
      self.vibs = VibCollection.VibCollection()
      self.bulletins = Bulletin.BulletinCollection()
      self.profiles = ImageProfile.ImageProfileCollection()
      self.baseimages = ReleaseCollection.BaseImageCollection()
      self.addons = ReleaseCollection.AddonCollection()
      self.solutions = ReleaseCollection.SolutionCollection()
      self.manifests = ReleaseCollection.ManifestCollection()
      self.reservedComponents = Bulletin.ComponentCollection()
      self.reservedVibs = VibCollection.VibCollection()

   def __init__(self, dbpath, dbcreate=True, validate=False, addprofile=True):
      """Class constructor.
            Parameters:
               * dbpath   - The location of the esximage database directory.
               * dbcreate - If True, the database will be created if it does
                            not exist. Note that if database files exist in
                            the directory given by dbpath, but cannot be
                            properly read or parsed, an exception will be
                            raised regardless of this parameter's value.
               * validate - If True, profile and vib XML will be validated
                            against schemas.
               * addprofile - If True, will create an empty profile if database
                              is created.
            Returns: A new Database instance.
            Raises:
               * DatabaseIOError - If dbpath cannot be created; if dbpath does
                                   not exist and dbcreate is False; if dbpath
                                   exists but is not a directory; or if the
                                   database cannot be read from or written to
                                   the file system.
      """
      self._dbpath = dbpath
      self._dirs = _PopulateDirs(dbpath)

      self._validate = validate
      createprofile = False
      if not os.path.exists(dbpath):
         if not dbcreate:
            msg = "Database directory '%s' does not exist." % dbpath
            raise Errors.DatabaseIOError(dbpath, msg)
         else:
            try:
               for aDir in self._dirs.values():
                  os.makedirs(aDir)
               createprofile = True
            except Exception as e:
               msg = 'Failed to create empty Database directory: %s' % (e)
               raise Errors.DatabaseIOError(self._dbpath, msg)
      elif not os.path.isdir(dbpath):
         msg = "Database path '%s' exists, but it is not a directory." % dbpath
         raise Errors.DatabaseIOError(dbpath, msg)

      self._InitCollections()
      self._newdirs = {}

      if createprofile and addprofile:
         log.debug('Creating empty profile for db: %s' % (self._dbpath))
         self.profiles.AddProfile(ImageProfile.ImageProfile('EmptyProfile',
            'esximage-auto'))
         self.Save()

   @property
   def dbpath(self):
      return self._dbpath

   @property
   def profile(self):
      if len(self.profiles) == 1:
         return list(self.profiles.values())[0]
      else:
         return None

   @property
   def vibIDs(self):
      return self.profile and self.profile.vibIDs or set()

   def CollectReservedComponentIDs(self):
      ''' Collect the components in self.bulletins but not in either any
          base image or any addon in this database. Callers then generates the
          reserved component collection; further generate reserved VIB ID and
          VIB collection.

          The returned value is a list of (name, version) tuples since
          different versions may be reserved at the same time.
      '''
      comps = Bulletin.ComponentCollection(self.bulletins)
      reservedCIDs = set()
      reservedCIDs.update(_CollectReservedComponentIDs(self.baseimages, comps))
      reservedCIDs.update(_CollectReservedComponentIDs(self.addons, comps))
      return list(reservedCIDs)

   def Clear(self):
      """Clear the content of the database."""
      self.vibs.clear()
      self.bulletins.clear()
      self.profiles.clear()
      self.baseimages.clear()
      self.addons.clear()
      self.solutions.clear()
      self.manifests.clear()
      self.reservedComponents.clear()
      self.reservedVibs.clear()

   def Load(self):
      """Populates the bulletins and vibs attributes of this object with
         information from the database. It is safe to call this method
         repeatedly to update this object when the database has changed.
            Exceptions:
               * DatabaseFormatError - If vibs.xml or bulletins.zip cannot be
                                       parsed, or format is otherwise invalid.
               * DatabaseIOError     - If a file cannot be read.
      """
      self.Clear()
      self._LoadDB()

   def Save(self):
      """Writes the information currently in the object back to the database.
            Raises:
               * DatabaseIOError     - On a failure to write to a file.
      """
      self._WriteDB()
      self._Commit()

   def PopulateWith(self, database=None, imgProfile=None):
      """Populate this database using another database or an image profile.
         In case of a database object, all metadatas will be copied over.

         In case of an image profile, assume its VIBs/components/release units
         are already populated, copy them over, image profile colelction will
         contain only this image profile.
      """
      if database is None and imgProfile is None:
         return
      if database and imgProfile:
         raise ValueError('only one of database/imgProfile can be supplied')
      self.Clear()
      if database is not None:
         self.vibs += database.vibs
         self.bulletins += database.bulletins
         self.profiles += database.profiles
         self.baseimages += database.baseimages
         self.addons += database.addons
         self.solutions += database.solutions
         self.manifests += database.manifests
         self.reservedComponents += database.reservedComponents
         self.reservedVibs += database.reservedVibs
      else:
         self.vibs += imgProfile.vibs
         self.bulletins += imgProfile.bulletins
         self.profiles.AddProfile(imgProfile)
         image = imgProfile.baseimage
         if image:
            self.baseimages[image.releaseID] = image
         addon = imgProfile.addon
         if addon:
            self.addons[addon.releaseID] = addon
         self.manifests += imgProfile.manifests
         self.solutions += imgProfile.solutions
         self.reservedComponents += imgProfile.reservedComponents
         self.reservedVibs += imgProfile.reservedVibs

   def _LoadDB(self):
      _LoadVibs(self.vibs, self._dirs['vibs'], self._dbpath, self._validate)
      _LoadBulletins(self.bulletins, self._dirs['bulletins'], self._dbpath)
      _LoadProfiles(self.profiles, self._dirs['profiles'], self._dbpath,
                    self._validate)
      # Directories below do not exist on hosts that pre-date personality
      # manager.
      _LoadReleaseUnits(self.baseimages, self._dirs['baseimages'],
                        self._dbpath, 'BaseImage')
      _LoadReleaseUnits(self.addons, self._dirs['addons'], self._dbpath,
                        'Addons')
      _LoadReleaseUnits(self.solutions, self._dirs['solutions'], self._dbpath,
                        'Solutions')
      _LoadReleaseUnits(self.manifests, self._dirs['manifests'], self._dbpath,
                        'Manifests')
      _LoadBulletins(self.reservedComponents, self._dirs['reservedComponents'],
                     self._dbpath)
      _LoadVibs(self.reservedVibs, self._dirs['reservedVibs'], self._dbpath,
                self._validate, isReserved=True)

   def _WriteCollection(self, metaType, collection, dirName, errorType,
                        toDB=False, namingfunc=None):
      try:
         kwargs = dict()
         if toDB:
            kwargs['toDB'] = True
         if namingfunc is not None:
            kwargs['namingfunc'] = namingfunc
         collection.ToDirectory(dirName, **kwargs)
      except errorType as e:
         msg = 'Failed to save %s metadata to dir %s: %s' % \
               (metaType, dirName, e)
         raise Errors.DatabaseIOError(self._dbpath, msg)

   def _WriteDB(self):
      self._newdirs = {}
      for name in self._dirs:
         self._newdirs[name] = self._dirs[name] + '.new'

      try:
         for aDir in self._newdirs.values():
            self._createemptydir(aDir)
      except EnvironmentError as e:
         msg = 'Failed to create temporary DB dir: %s'  % (e)
         raise Errors.DatabaseIOError(self._dbpath, msg)

      self._WriteCollection('VIB', self.vibs, self._newdirs['vibs'],
                            Errors.VibIOError)
      self._WriteCollection('Bulletin', self.bulletins,
                            self._newdirs['bulletins'],
                            Errors.BulletinIOError,
                            namingfunc=Bulletin.getDatabaseComponentFileName)
      self._WriteCollection('Profile', self.profiles, self._newdirs['profiles'],
                            Errors.ProfileIOError, toDB=True)
      self._WriteCollection('BaseImage', self.baseimages,
                            self._newdirs['baseimages'],
                            Errors.ReleaseUnitIOError)
      self._WriteCollection('Addon', self.addons, self._newdirs['addons'],
                            Errors.ReleaseUnitIOError)
      self._WriteCollection('Solution', self.solutions,
                            self._newdirs['solutions'],
                            Errors.ReleaseUnitIOError)
      self._WriteCollection('Manifest', self.manifests,
                            self._newdirs['manifests'],
                            Errors.ReleaseUnitIOError)
      self._WriteCollection('Reserved components', self.reservedComponents,
                            self._newdirs['reservedComponents'],
                            Errors.BulletinIOError,
                            namingfunc=Bulletin.getDatabaseComponentFileName)
      self._WriteCollection('Reserved vibs', self.reservedVibs,
                            self._newdirs['reservedVibs'],
                            Errors.VibIOError)

   def _Commit(self):
      missingDir = []
      for dirType in self._newdirs:
         if not os.path.isdir(self._newdirs[dirType]):
            missingDir.append(dirType)

      if missingDir:
         raise Errors.DatabaseIOError('Database directories %s do not exist' %
                                      ', '.join(missingDir))
      try:
         for aDir in self._dirs.values():
            if os.path.exists(aDir):
               shutil.rmtree(aDir)
      except EnvironmentError as e:
         msg = 'Error in purging old directory: %s' % (e)
         raise Errors.DatabaseIOError(self._dbpath, msg)

      try:
         for name in self._dirs:
            os.rename(self._newdirs[name], self._dirs[name])
      except Exception as e:
         msg = 'Failed to commit the change to database: %s' % (e)
         raise Errors.DatabaseIOError(self._dbpath, msg)

   @staticmethod
   def _createemptydir(path):
      if os.path.isdir(path):
         shutil.rmtree(path)
      elif os.path.isfile(path):
         os.unlink(path)
      os.makedirs(path)

class TarDatabase(Database):
   """Implements an Image Profile and VIB database within a tar archive."""
   DB_PREFIX = ('var', 'db', 'esximg')

   def __init__(self, dbpath=None, dbcreate=True, validate=False):
      """Class constructor.
            Parameters:
               * dbpath   - An optional file name or file object containing a
                            targz'ed database. If specified, image profile and
                            VIB data will be loaded from this location.
               * dbcreate - If True, the database will be created if it does
                            not exist. Note that if database files exist in
                            the tar file given by dbpath, but cannot be
                            properly read or parsed, an exception will be
                            raised regardless of this parameter's value. This
                            parameter has no effect if dbpath is unspecified or
                            is a file object.
               * validate - If True, profile and vib XML will be validated
                            against schemas.
            Returns: A new TarDatabase instance.
            Raises:
               * DatabaseIOError - If dbpath is specified, but a location
                                   either does not exist or could not be
                                   parsed.
      """
      self._dbpath = dbpath
      # NOTE: tardatabase might be constructed in Windows, we need to use Unix-
      #       style paths. This is also true for _Save*() methods.
      self._dbroot = posixpath.join(*self.DB_PREFIX)
      self._dirs = _PopulateDirs(self._dbroot, forcePosixPath=True)

      self._validate = validate

      self._InitCollections()

      if (isString(dbpath) and not os.path.exists(dbpath)
          and dbcreate):
         log.debug('Creating tar database at: %s' % dbpath)
         self.Save(dbpath)

   def Load(self, dbpath=None, gzip=True):
      """Populates the bulletins and vibs attributes of this object with
         information from the database. It is safe to call this method
         repeatedly to update this object when the database has changed.
            Parameters:
               * dbpath   - An optional file name or file object containing a
                            targz'ed database. If specified, image profile and
                            VIB data will be loaded from this location. This
                            overrides any value specified in the constructor.
               * gzip     - Set to True when the tar file is a tgz, set to
                            False of uncompressed tar.
            Exceptions:
               * DatabaseFormatError - If VIB or Image Profile XML data cannot
                                       be parsed.
               * DatabaseIOError     - If a file cannot be read.
      """
      self.Clear()
      dbpath = dbpath is not None and dbpath or self._dbpath
      if dbpath is None:
         msg = "Cannot open tar database: No path specified."
         raise Errors.DatabaseIOError(None, msg)

      mode = 'r:gz' if gzip else 'r:'
      try:
         if isString(dbpath):
            dbpath = byteToStr(dbpath)
            t = tarfile.open(name=dbpath, mode=mode,
                             format=tarfile.GNU_FORMAT)
         else:
            t = tarfile.open(mode=mode, fileobj=dbpath,
                             format=tarfile.GNU_FORMAT)
      except Exception as e:
         msg = "Failed to open tar database: %s." % e
         raise Errors.DatabaseIOError(dbpath, msg)

      try:
         for info in t:
            if not info.isfile():
               continue

            try:
               f = t.extractfile(info)
               content = f.read()
               f.close()
            except Exception as e:
               msg = "Failed to read %s from tardatabase %s: %s" % (
                     info.name, self._dbpath, e)
               raise Errors.DatabaseIOError(dbpath, msg)

            try:
               if info.name.startswith(self._dirs['vibs']):
                  _AddVibXmlToCollection(self.vibs, info.name, t, content,
                                         self._validate)
               elif info.name.startswith(self._dirs['profiles']):
                  self.profiles.AddProfileFromXml(content, self._validate)
               elif info.name.startswith(self._dirs['bulletins']):
                  self.bulletins.AddBulletinFromXml(content)
               elif info.name.startswith(self._dirs['baseimages']):
                  self.baseimages.AddFromJSON(content)
               elif info.name.startswith(self._dirs['addons']):
                  self.addons.AddFromJSON(content)
               elif info.name.startswith(self._dirs['solutions']):
                  self.solutions.AddFromJSON(content)
               elif info.name.startswith(self._dirs['manifests']):
                  self.manifests.AddFromJSON(content)
               elif info.name.startswith(self._dirs['reservedComponents']):
                  self.reservedComponents.AddComponentFromXml(content)
               elif info.name.startswith(self._dirs['reservedVibs']):
                  _AddVibXmlToCollection(self.reservedVibs, info.name, t,
                                         content, self._validate)
               else:
                  log.info('Unrecognized file %s in tardatabase.' % (info.name))
            except Exception as e:
               msg = 'Error parsing VIB/ImageProfile from DB %s: %s - %s' % (
                     self._dbpath, e.__class__.__name__, e)
               raise Errors.DatabaseFormatError(dbpath, msg)
      finally:
         t.close()

   def _SaveVibs(self, vibs, vibsdir, tarObj, savesig):
      for v in vibs.values():
         xml = v.ToXml()
         c = etree.tostring(xml)
         fn = posixpath.join(vibsdir, vibs.FilenameForVib(v))
         self._addfile(tarObj, fn, c)
         c = v.GetSignature()
         sigfile = fn + Vib.EXTENSION_ORIG_SIG
         if c is not None and savesig:
            self._addfile(tarObj, sigfile, c)
         c = v.GetOrigDescriptor()
         origdescfile = fn + Vib.EXTENSION_ORIG_DESC
         if c is not None and savesig:
            if type(c) is str and sys.version_info[0] >= 3:
                c = c.encode()
            self._addfile(tarObj, origdescfile, c)

   def _SaveXMLCollection(self, collection, dirName, tarObj, fileFunc,
                          toDB=True):
      for obj in collection.values():
         if toDB:
            xml = obj.ToXml(toDB=True)
         else:
            xml = obj.ToXml()
         c = etree.tostring(xml)
         fn = posixpath.join(dirName, fileFunc(obj))
         self._addfile(tarObj, fn, c)

   def _SaveBulletins(self, bulletins, bulletinsdir, tarObj):
      self._SaveXMLCollection(bulletins, bulletinsdir, tarObj,
                              Bulletin.getDatabaseComponentFileName, toDB=False)

   def _SaveReleaseUnits(self, units, unitsDir, tarObj):
      for obj in units.values():
         jsonStr = obj.ToJSON()
         fn = posixpath.join(unitsDir, genReleaseUnitFileName(obj.releaseID))
         self._addfile(tarObj, fn, jsonStr.encode('utf-8'))

   def _SaveProfiles(self, tarObj):
      self._SaveXMLCollection(self.profiles, self._dirs['profiles'], tarObj,
                              self.profiles.FilenameForProfile)

   def Save(self, dbpath=None, savesig=False, gzip=True):
      """Writes the information currently in the object back to the database.
            Parameters:
               * dbpath  - If specified, must be either a string specifying a
                           file name, or a file object to write the database to.
                           If not specified, the location used in the class
                           constructor will be used.
               * savesig - If set to True, the original descriptor and its
                           signature will be written to the database.
               * gzip    - Set to True to write a tgz, otherwise write a
                           uncompressed tar.
            Raises:
               * DatabaseIOError - On a failure to write to a file.
      """
      dbpath = dbpath is not None and dbpath or self._dbpath
      if dbpath is None:
         msg = "Cannot write tar database: No path specified."
         raise Errors.DatabaseIOError(None, msg)

      mode = 'w:gz' if gzip else 'w:'
      try:
         if isString(dbpath):
            dbpath = byteToStr(dbpath)
            t = tarfile.open(name=dbpath + ".new", mode=mode,
                             format=tarfile.GNU_FORMAT)
         else:
            t = tarfile.open(mode=mode, fileobj=dbpath,
                             format=tarfile.GNU_FORMAT)
      except Exception as e:
         msg = "Error opening tar database: %s." % e
         raise Errors.DatabaseIOError(dbpath, msg)

      try:
         self._createemptydb(t)
      except Exception as e:
         msg = "Error creating tar database: %s." % e
         raise Errors.DatabaseIOError(dbpath, msg)

      try:
         self._SaveVibs(self.vibs, self._dirs['vibs'], t, savesig)
         self._SaveBulletins(self.bulletins, self._dirs['bulletins'], t)
         self._SaveProfiles(t)
         self._SaveReleaseUnits(self.baseimages, self._dirs['baseimages'], t)
         self._SaveReleaseUnits(self.addons, self._dirs['addons'], t)
         self._SaveReleaseUnits(self.solutions, self._dirs['solutions'], t)
         self._SaveReleaseUnits(self.manifests, self._dirs['manifests'], t)

         for sameNamedComps in self.reservedComponents.values():
            self._SaveBulletins(sameNamedComps,
                                self._dirs['reservedComponents'], t)

         self._SaveVibs(self.reservedVibs, self._dirs['reservedVibs'],
                        t, savesig)
      except Exception as e:
         msg = "Error writing tar database: %s."  % e
         raise Errors.DatabaseIOError(dbpath, msg)
      finally:
         t.close()

      if isString(dbpath):
         try:
            # Atomic rename is apparently too much to ask for from Windows.
            if os.path.exists(dbpath):
               os.unlink(dbpath)
            os.rename(dbpath + ".new", dbpath)
         except Exception as e:
            msg = "Error renaming temporary tar database: %s." % e
            raise Errors.DatabaseIOError(dbpath, msg)

   @classmethod
   def _addfile(cls, tar, path, content):
      #padding leading root
      if len(path) + 1 >= cls.VISOR_PATH_LIMIT:
         msg = ('database file path is too long (%d),  the limit is %d.\n%s' %
               (len(path) + 1, cls.VISOR_PATH_LIMIT, path))
         raise Errors.DatabaseIOError(msg)

      info = tarfile.TarInfo(path)
      info.mtime = time.time()
      info.mode = 0o644
      info.gid = 0
      info.uid = 0
      info.type = tarfile.REGTYPE
      info.size = len(content)
      tar.addfile(info, BytesIO(content))

   @classmethod
   def _adddir(cls, tar, path):
      info = tarfile.TarInfo(path)
      info.mtime = time.time()
      info.mode = 0o755
      info.type = tarfile.DIRTYPE
      tar.addfile(info)

   def _createemptydb(self, t):
      # Add parent directories which are needed to populate VisorFS
      p = ''
      for d in self.DB_PREFIX:
         if p:
            p = '/'.join([p, d])
         else:
            p = d
         self._adddir(t, p)

      for aDir in self._dirs.values():
         self._adddir(t, aDir)
