#!/usr/bin/python

import os, sys
import logging, logging.handlers

ESXUPDATEPATH = ['/usr/lib/vmware/python2.4/site-packages',
                 '/lib/python2.5-visor', '/lib/python2.5-visor/lib-dynload']
if os.environ.has_key('ESXUPDATEPATH'):
   ESXUPDATEPATH[0:0] = os.environ['ESXUPDATEPATH'].split(':')
sys.path[0:0] = ESXUPDATEPATH

FMT_LOGFILE = '[%(asctime)s] %(levelname)7s: %(name)10.10s: %(message)s'
FMT_DATETIME = '%Y-%m-%d %H:%M:%S'
LOCKFILE = '/var/run/esxupdate.pid'
LOGGING_LEVELS = {'debug': logging.DEBUG,
          'info': logging.INFO,
          'warning': logging.WARNING,
          'error': logging.ERROR,
          'critical': logging.CRITICAL}

# Note: RESPONSE_VERSION duplicates the value of SCAN_RESPONSE_VER in Iface.py
#  to make sure esxupdate has XML output for HA mode under some error conditions.
RESPONSE_VERSION = '1.30'
HA_MODE = '--HA' in sys.argv

# PrintException is used to print out XML response under some error circumstances:
# errors in early init, iface is not available, and etc.
def PrintException(e, hamode=False, errmsg=''):
   errname = e.__class__.__name__

   if hasattr(e, 'attrs') and hasattr(e, 'errno') and hasattr(e, 'description'):
      errcode = str(e.errno)
      errdesc = str(e.description)
      if 'message' in e.attrs and hasattr(e, 'message'):
         errdesc += '\n' + '  ' + '\n  '.join(str(e.message).splitlines())
   else:
      errcode = '99'
      errdesc = str(e)

   if hamode:
      sys.stdout.write('<esxupdate-response>\n'
            '  <version>%s</version>\n  <error errorClass="%s">\n'
            '    <errorCode>%s</errorCode>\n'
            '    <errorDesc>%s</errorDesc>\n  </error>\n'
            '</esxupdate-response>\n' % (RESPONSE_VERSION, errname, errcode, errdesc))
   else:
      if errmsg:
         sys.stderr.write('%s\n' % (errmsg))
      sys.stderr.write(' Encountered error: %s\n' % (errname))
      sys.stderr.write(' Error message: %s\n' % (errdesc))

# Set up logging early.
try:
   from vmware.esx45update.const import *
   import vmware.esx45update.errors as errors
   import vmware.esx45update.config as config

   if os.getuid():
      raise errors.NotRootError()

   defconfig = config.GetConf(CONFIGFILE)
   if defconfig.log.file:
      logdir = os.path.dirname(defconfig.log.file)
      if not os.path.isdir(logdir):
         os.makedirs(logdir)
      try:
         maxBytes = long(defconfig.log.size)
      except:
         maxBytes = 0
      handler = logging.handlers.RotatingFileHandler(filename = defconfig.log.file,
                                                   maxBytes = maxBytes,
                                                   backupCount = 1)
      handler.setFormatter(logging.Formatter(FMT_LOGFILE, FMT_DATETIME))
   else:
      handler = logging.handlers.SysLogHandler(address='/dev/log')
      formatter = 'esxupdate: %(name)s: %(levelname)s: %(message)s'
      handler.setFormatter(logging.Formatter(formatter))

   rootlogger = logging.getLogger()
   for h in rootlogger.handlers:
      rootlogger.removeHandler(h)
   rootlogger.addHandler(handler)
   # set this back to logging.INFO closer to release.
   rootlogger.setLevel(logging.DEBUG)

except ImportError, e:
   PrintException(e, HA_MODE,
         'Failed to import one or more critical modules.\n'
         'Please report this as a bug.')
   sys.exit(99)
except errors.ConfigError, e:
   progname = os.path.basename(sys.argv[0])
   PrintException(e, HA_MODE,
         'Failed to run %s due to an error parsing configuration '
         'data:' % (progname))
   sys.exit(e.errno)
except errors.NotRootError, e:
   PrintException(e, HA_MODE)
   sys.exit(e.errno)
except Exception, e:
   PrintException(e, HA_MODE,
         'Unable to set up logging:')
   sys.exit(99)

logger = logging.getLogger('esxupdate')

try:
   from vmware.Lock import Lock, LockError

   # Acquire process lock before platform is imported
   plock = Lock(LOCKFILE)
   try:
      plock.Lock(os.getpid())
   except LockError, e:
      logger.error('Another esxupdate installation (PID=%s) is running.\n'
                  'Please wait for that installation to finish first.\n'
                  'PID:%s' % (e.pid, e.pid) )
      raise errors.LockingError('Another esxupdate installation (PID %s) is '
                                'currently running' % e.pid)
   except EnvironmentError, e:
      logger.error('Error locking %s: %s' % (LOCKFILE, str(e)))
      raise errors.LockingError('Unable to open lock file (%s)' % e.strerror)

   import optparse
   import vmware.esx45update.cmdline as cmdline
   import vmware.esx45update.util as util

   parser = optparse.OptionParser()

   parser.add_option('-b', action='append', type='string',
                     dest='bundles', metavar='BULLETIN',
                     help='a bulletin ID on which to run the command.  '
                          'May be specified multiple times.')
   parser.add_option('-m', '--meta', action='append', type='string', dest='meta',
                     help='a metadata file on which to run the command.  May '
                          'be specified multiple times.')
   parser.add_option('--bundle', action='append', type='string',
                     dest='bundlezips', metavar='BundleZipUrl',
                     help='An offline bundle .zip file to work with.  May '
                          'be specified multiple times.')
   parser.add_option('--loglevel', action='store', type='string',
                     dest='loglevel',
                     help='enable more verbose log file output.  '
                          'May be a number (1-50), '
                          'or one of DEBUG|INFO|WARNING|ERROR|CRITICAL. '
                          'Defaults to INFO (20).')
   parser.add_option('--http_proxy', action='store', type='string',
                     dest='proxyurl', metavar='Url:Port',
                     help='use the proxy server at Url and Port')
   parser.add_option('--timeout', action='store', type='float',
                     dest='timeout',
                     help='The timeout value for HTTP, HTTPS and FTP '
                          'connections.')
   parser.add_option('--retry', action='store', type='int',
                     dest='retry',
                     help='The number of times to retry HTTP, HTTPS and FTP '
                          'connections.')

   # Hidden options
   parser.add_option('--HA', action='store_true', help=optparse.SUPPRESS_HELP)
   parser.add_option('--vib-view', action='store_true', dest='vibview',
                     help=optparse.SUPPRESS_HELP)
   parser.add_option('--maintenancemode', action='store_true',
                     help=optparse.SUPPRESS_HELP)
   parser.add_option('--force', action='store_true', help=optparse.SUPPRESS_HELP)

   # Per-command options
   #
   # options not in the EsxupdateSpec 1.2 are commented out
   # they will be evaluated further before release and implemented
   # as time permits
#   parser.add_option('-t', '--test', action='store_true', dest='test',
#                     help='Test update transaction only--do not change sytem.')
   parser.add_option('-a', '--all', action='store_true', dest='all',
                     help='Display all bulletins.  Default is to '
                     'display only the applicable updates.')
#   parser.add_option('-l', '--long', action='store_true', dest='long',
#                     help='Produce more detailed response in query or info '
#                          'output.')
#   parser.add_option('--noobsoletes', action='store_true', dest='noobsoletes',
#                     help='Ignore obsoletions during update.  (Enables '
#                          'downgrading to older bundles.)')
#   parser.add_option('--reinstall', action='store_true', dest='reinstall',
#                     help='Re-install a bundle that is already installed.')
   parser.add_option('--nodeps', action='store_true', dest='nodeps',
                     help=optparse.SUPPRESS_HELP)
   parser.add_option('--nosigcheck', action='store_true', dest='nosigcheck',
                     help=optparse.SUPPRESS_HELP)
   parser.add_option('--nocache', action='store_true', dest='nocache',
                     help=optparse.SUPPRESS_HELP)
   parser.add_option('--olderversion', action='store_true', dest='olderversion',
                     help=optparse.SUPPRESS_HELP)
   parser.add_option('--cachesize', action='store', type='int',
                     dest='cachesize', help=optparse.SUPPRESS_HELP)
   parser.add_option('--cleancache', action='store_true', dest='cleancache',
                     help=optparse.SUPPRESS_HELP)
   parser.add_option('--compliant', action='store_true', dest='compliant',
                     help=optparse.SUPPRESS_HELP)

   parser.set_defaults(loglevel=config.GetConf().log.level)
   parser.formatter.max_help_position = 30 # PR 381133

   try:
      timeout = float(config.GetConf().default.timeout)
      parser.set_defaults(timeout=timeout)
   except Exception, e:
      pass

   try:
      retry = int(config.GetConf().default.retry)
      parser.set_defaults(retry=retry)
   except Exception:
      pass

   user_commands = [k for k, v in cmdline.COMMANDS.items() if not v.hidden]
   user_commands.sort()
   parser.set_usage('%%prog [options] (%s)' %
                    '|'.join(user_commands))

   argv = sys.argv[1:]
   if HA_MODE:
      stdin_data = util.readstdin().strip()
      argv.extend(line.strip() for line in stdin_data.splitlines())

   (options, args) = parser.parse_args(argv)
   if len(args) < 1:
      parser.print_help(file=sys.stderr)
      parser.exit(2, '\n%s: error: Command must be specified\n' %
                      parser.get_prog_name())
   elif len(args) > 1:
      parser.error('Only one command may be specified')

   if options.loglevel is not None:
      logopt = options.loglevel.strip().lower()
      if logopt in LOGGING_LEVELS:
         level = LOGGING_LEVELS[logopt]
      else:
         try:
            level = abs(int(logopt))
         except:
            parser.error('Invalid log level (%s) specified for %s.' %
                         (str(logopt), parser.get_option('--loglevel')))
      rootlogger.setLevel(level)

   command = args[0].strip()

   if command not in cmdline.COMMANDS.keys():
      parser.error('Command must be one of: %s' % ', '.join(user_commands))

   cmdclass = cmdline.COMMANDS[command]

   cmd = cmdclass(command, parser, options)
   cmd.CheckArgs()
   logger.info('--\nCommand: %s\nArgs: %s\nOptions: %s' % (
      command, args, options))

   cmd.Run()
   logger.info('All done!')
   raise SystemExit(0)

except SystemExit:
   if 'cmd' in globals() and hasattr(cmd, 'iface'):
      cmd.iface.Flush()
   raise
except KeyboardInterrupt:
   sys.stderr.write('Exiting on keyboard interrupt.\n')
   logger.info('Exiting on keyboard interrupt.\n')
except Exception, e:
   if isinstance(e, EnvironmentError) and 'errors' in globals():
      #
      # All uncaught EnvironmentErrors become FileIOErrors.
      # Much more efficient this way than catching everywhere
      # and re-raising it.
      #
      e = errors.FileIOError(e.filename, 'I/O Error (%d) on file %s: %s'
                             % (e.errno or '', e.filename or '', str(e)))
   if 'cmd' in globals() and hasattr(cmd, 'iface'):
      cmd.iface.ShowException(e)
      cmd.iface.Flush()
   else:
      PrintException(e, HA_MODE)

   if 'errors' in globals() and isinstance(e, errors.EsxupdateError):
      if not isinstance(e, errors.NormalExit):
         logger.exception('An esxupdate error exception was caught:')
      raise SystemExit(e.errno)
   else:
      logger.exception('An unexpected exception was caught:')
      raise SystemExit(99)
