#!/usr/bin/env python
#######################################################################
# Copyright (C) 2005,2006 VMWare, Inc.
# All Rights Reserved
########################################################################
#
# esxupdate
#
#    Install and manage updates and patches on ESX Server.
#    See the esxupdate man page for more details.
#
#   HIDDEN OPTIONS & COMMANDS
#    --nobackup      Do not backup packages and system state for rollbacks
#
#    restore         Used only for continuing pre-installs
#    import descfile   Import descriptor into patch DB, used for ISO installs
#    rollback        Rolls back last bundle transaction (doesn't work)
#    db import/dump/addstate  See ManageDbCommand()
#
#    Multiple sessions of esxupdate may be needed to finish an install.
#    Each session installs all the bundles possible until a reboot is
#    absolutely necessary to install more bundles.
#
# TODO: KeyboardInterrupt handler
#
"""
To install ESX patches and updates:
  esxupdate [options] update

  -b <bundle ID>       : Install this bundle. May be a wildcard.
                         May be repeated.  Defaults to '*'.
  -r/--repo <url>      : Install bundle at url; default is cwd
  --test               : Download RPMs and run test transaction only
  -n/--noreboot        : Do not reboot after install
  --nosigcheck         : Do not check signatures of the depot files
  -x/--exclude <pkg>   : Exclude pkg during install; use one -x per pkg.
  -f/--force           : Force install of older and existing packages

To scan for available updates in a depot:
  esxupdate -d <depotURL> [-b <bundlespec>][options] scan

  --explain            : Explain in detail why a bundle is not applicable

To query installed update bundles:
  esxupdate [-a|-o] [-l] query
  esxupdate [-l] info <bundleID1> [<bundleID2> ...]

  -a/--listall             : List all bundles including obsoleted ones
  -o/--onlyobsolete        : List only obsoleted bundles
  -l/--listrpms        : (info) List every rpm in patch
                         (query) Diff RPMDB against installed updates

To query update bundle(s) at URL or in a depot:
  esxupdate [-r <URL> | -d <depotURL> [-b <bundleID>] ...] [-l] info

Universal options:
  -d <depot URL>       : Depot containing contents.xml file is here
  --flushcache         : Force a flush of the local depot cache
  -v/--verbose <level> : Set output verbosity (default=20)
                         10=debug / 20=info / 30=warning / 40=error

A reboot will occur after an install finishes by default, unless
no bundles require a reboot or the --noreboot option is passed.
"""

import os
import sys
import getopt

#
# Note: Setting the PYTHONPATH environment variable overrides the path below
#
sys.path.append('/usr/lib/vmware/python2.2/site-packages')
try:
    from vmware.Utility import python_required
    from vmware.system import LogSetup, LogCommandError, LogSetLevel
    from vmware.Lock import Lock, LockError
except ImportError, e:
    print "Sorry, the esxupdate library cannot be found\n\t(%s)" % (str(e))
    print "amongst the search path %s" % (sys.path)
    print "Try setting the PYTHONPATH environment variable."
    sys.exit(1)

from vmware.descriptor import Descriptor
from vmware.esxupdate import errors
from vmware.esxupdate import InstallState
from vmware.esxupdate import InstallSession
from vmware.esxupdate import insthelper
from vmware.esxupdate import PatchDB
from vmware.esxupdate import Depot
from vmware.esxupdate import scan
from vmware.esxupdate import ha
from vmware.esxupdate import cmdline

import logging


#
# Default path for patch database
#
DB_DIR = '/etc/vmware/patchdb'

LOCKFILE = '/var/run/esxupdate.pid'
KEYRING_DIR = '/etc/vmware/keying'

########################################################################
#
# Make a new or find an old install session
#

def CheckDepotURL(options):
    """ If depoturl is not is options and the current working dir is not a bundle dir, use the current working dir as depoturl, return depoturl; otherwise return None"""
    res = None
    if not 'depoturl' in options and not os.path.isfile('./descriptor.xml'):
        from urllib import quote
        options['depoturl'] = 'file://' + quote(os.getcwd())
        log.info('No depot URL specified, going with %s' % (options['depoturl']))
        res = options['depoturl']
    return res


def CheckRepoURLs(repourls):
    """ If repourls is empty, add the current working dir as an URL to it """
    if len(repourls) == 0:
        from urllib import quote
        repourls.append('file://' + quote(os.getcwd()))
        log.info('No repository URL specified, going with %s' % (repourls[-1]))        


def SingleBundleSpec(options):
    """ Checks options for specifying a single bundle.
    options may be modified. """

    if len( options['bundleIDs'] ) > 1:
        log.error('only one bundleID (-b option) can be used when updating.')
        BadArgsHelp() #exits

    bundleID = options['bundleIDs'][0]
    if "*" in bundleID:
        log.error('wildcards cannot be used in the bundleID (%s) '\
                  'when updating.' % bundleID)
        BadArgsHelp() #exits

def MultiBundleSpec(options):
    """ Checks options for specifying multiple bundles
    """
    pass

def NewRepoSession(db, options, repourls):
    """ Return a new InstallSession instance
    using the current directory as the URL if none if given.
    db          - PatchDB instance
    options     - dict of install options
    repourls    - list of URL strings
    """
    CheckRepoURLs(repourls)
    options['bundleurl'] = repourls.pop(0)
    if 'old_modules' in options:
        options['url'] = options['bundleurl']
    if repourls:
        options['baseurls'] = repourls

    #
    # Create new install session with modified options
    #
    s = InstallSession(db, options=options)
    return s


def NewDepotSession(db, options):
    """ Return a new InstallSession instance
    db          - PatchDB instance
    options     - dict of install options
    """
    if len( options['bundleIDs'] ) == 0:
        log.info('No -b specified, selecting all bundles in depot.')
        options['bundleIDs'].append('*')
        
    if 'old_modules' in options:
        options['depot'] = options['depoturl']
        if len( options['bundleIDs'] ) == 0:
            log.error('-b option must be specified for the update command.')
            BadArgsHelp() #exits
        options['bundleID'] = options['bundleIDs'][0]
        if '*' in options['bundleID']:
            log.error('Wildcards cannot be used with old esxupdate modules.')
            BadArgsHelp()
    
    return InstallSession(db, options=options)
    

def GetLastSession(db, options):
    """ Recover the last install session and return it as an InstallSession
    instance.  If no prev sessions exist, return a blank new one.
    """
    lastSessHash = db.RestoreSession()
    return InstallSession(db, options=options, dbhash=lastSessHash)


########################################################################
#
# query, info, import, addstate, scan commands
# these are expected to return None or to sys.exit() with an errorcode
#
def PatchInfoCommand(db, options, args, plock):
    """ Print out detailed info for either an installed patch or
    from the descriptor of a patch repository.

    Can potentially sys.exit with an error code
    """
    if options.has_key('listrpms'):
        listRpms = options['listrpms']
    else:
        listRpms = 0

    #
    # Obtain hash from DB, exit if entry doesn't exist
    #
    if len(args) > 1:
        exitCode = 0
        for bundle in args[1:]:
            h = db.GetPatchInfo(bundle)
            if h:
                print insthelper.FormatHashVerbose(h, listRpms)
            else:
                log.error('No bundle [%s] exists in database' % (bundle))
                exitCode = errors.Codes.NO_SUCH_REL
        sys.exit(exitCode)
    #
    # No patchlevel specified, just dump descriptor from URL
    #
    else:
        h = {}
        h['installed'] = {}
        if not 'depoturl' in options and len(options['repourls']) == 0:
            CheckDepotURL(options)

        depotArgs = {}
        if 'proxy' in options:
            depotArgs['proxy'] = options['proxy']
        if 'flushcache' in options:
            depotArgs['flushCache'] = options['flushcache']
        if 'bundleIDs' in options:
            depotArgs['scanlist'] = options['bundleIDs']
        depotArgs['keyringDir'] = options['keyringDir']

        #
        # Prepare repourl
        #
        if 'depoturl' in options:
            depotClass = Depot.Depot
            baseURL = options['depoturl']
        else:
            CheckRepoURLs(options['repourls'])
            depotClass = Depot.DepotInBundle
            baseURL = options['repourls'][0]

        #
        # Create a depot obj, populate depot contents
        # TODO: refractor Depot setup and basic checking code
        #
        h['options'] = {'url': baseURL}
        try:
            depot = depotClass(baseURL, **depotArgs)
            depot.SetBlacklist(['*.rpm'])
            depot.SyncFromRemote()
            depot.SanityCheck()
            if not options.get('nosigcheck'):
                depot.CheckIntegrity()
        except (errors.DepotAccessError,
                errors.DepotDownloadError,
                errors.BadDepotDescriptorError,
                errors.BadDepotMetadataError,
                errors.IntegrityError,
                errors.FileError), e:
            e.Report( log, halog )
            sys.exit( e.exitCode )

        bundles = depot.bundles.keys()
        bundles.sort()
        for bundleID in bundles:
            dbh = db.GetPatchInfo(bundleID)
            if dbh:
                print insthelper.FormatHashVerbose(dbh, listRpms)
                continue
            if 'depoturl' in options:
                if baseURL.endswith('/'):
                    repourl = baseURL + bundleID
                else:
                    repourl = baseURL + '/' + bundleID
                h['options']['url'] = repourl
            h['desc']= depot.bundles[bundleID]['desc']
            # Translate hash and descriptor fields into a string
            print insthelper.FormatHashVerbose(h, listRpms)

    if not listRpms:
        print "For a detailed list of rpms, use the -l/--listrpms option."


def PatchQueryCommand(db, options, args, plock):
    """ Print out a table summarizing the installed patches """

    if options.has_key('listrpms'):
        listRpms = options['listrpms']
    else:
        listRpms = 0

    installed = db.GetInstalledPatches(options.get('listall', False),
                                       options.get('onlyobsolete', False))
    releases = db.LasttimeIndex(installed)

    print "Installed software bundles:"
    print insthelper.FormatShortHeaders()
    for rel in releases:
        print insthelper.FormatHashShort(installed[rel])

    if listRpms:
        hdrs = insthelper.GetRpmdbHeaders()
        expected = insthelper.ExpectedRpmsFromDb(db)
        new, missing, extras, matches = insthelper.DiffHeaders(hdrs, expected)
        if len(new):
            print "\nNew packages:"
            print insthelper.FormatRpmlist(new)
        if len(missing):
            print "\nMissing packages:"
            print insthelper.FormatRpmlist(missing)
        if len(extras):
            print "\nWrong or duplicate package versions:"
            print insthelper.FormatExtras(extras, matches, expected)
    else:
        print "For a differential list of rpms, use the -l/--listrpms option."


def ImportDescCommand(db, options, args, plock):
    """ Import descriptor from ISO into patch DB """
    ManageDbCommand(db, options, ['db'] + args, plock)


def ManageDbCommand(db, options, args, plock):
    """ Manage the patch database.  There are a couple subcommands:
    db import <desc>      : imports a descriptor into the db
    db dump <release>     : dumps the DB hash of given release
    db addstate <state>   : calls PatchDB.AddSystemState(state)
    """
    if len(args) > 1:
        dbcmd = args[1]
        dbargs = args[2:]
    else:
        BadArgsHelp()

    if dbcmd == "import":
        if not dbargs:
            BadArgsHelp()
        try:
            desc = Descriptor.Descriptor(dbargs[0])
        except EnvironmentError, (enum, msg):
            log.error("Cannot read descriptor file %s (error %d): %s" % (
                descfile, enum, msg))
            sys.exit(errors.Codes.IO)

        if not db.HasEntry(desc.GetRelease()) or 'force' in options:
             db.AddIsoEntry(desc)
             db.Sync()
        else:
            log.info("Bundle %s exists in patchDB, skipping import" % 
                             desc.GetRelease())


    elif dbcmd == "addstate":
        if not dbargs:
            BadArgsHelp()
        state = dbargs[0]
        db.AddSystemState(state)
        db.Sync()

    elif dbcmd == "dump":
        from pprint import pprint
        if not dbargs:
            BadArgsHelp()
        dbhash = db.GetEntry(dbargs[0])
        pprint(dbhash)

    else:
        log.error("Unknown db command %s" % (dbcmd))
        sys.exit(errors.Codes.BAD_ARGS)

    
def ScanCommand(db, options, args, plock):
    """ Perform the scan command on a depot.
    Only *.xml from each bundle dir will be downloaded.
    """
    if 'depoturl' not in options:
        if not CheckDepotURL(options):
            log.error('Current working dir is likely a bundle dir.')
            log.error('-d/--depot option must be specified for the scan command;'+
                    ' or change current working dir to a depot dir.')
            BadArgsHelp()

    depotArgs = {}
    if options.has_key('flushcache'):
        depotArgs['flushCache'] = options['flushcache']
    if options.has_key('bundleIDs'):
        depotArgs['scanlist'] = options['bundleIDs']

    depotArgs['keyringDir'] = options['keyringDir']
    
    try:
        depot = Depot.Depot(options['depoturl'], **depotArgs)
        depot.SetBlacklist(['*.rpm'])
        depot.SyncFromRemote()
        depot.SanityCheck()
        if not options.get('nosigcheck'):
            depot.CheckIntegrity()
    except (errors.DepotAccessError,
            errors.DepotDownloadError,
            errors.BadDepotDescriptorError,
            errors.BadDepotMetadataError,
            errors.IntegrityError,
            errors.FileError), e:
        e.Report( log, halog )
        sys.exit( e.exitCode )
        
    if len(depot.bundles) == 0:
        log.error('No bundles in depot satisfy pattern %s',options['bundleIDs'])
        sys.exit(errors.Codes.DOWNLOAD)

    try:
        if 'hostagent' in options:
            scanhash = scan.ScanDepot(depot, db)
        else:
            scanhash = scan.ScanDepot(depot, db, groupMode=True)
    except scan.DBScanError, e:
        log.error('Error scanning repository: %s' % (e))
        sys.exit(errors.Codes.BAD_DB)

    if 'hostagent' in options:
        print ha.ScanOutput(scanhash)
        sys.exit(0)

    else:
        if 'test' in options:
            from pprint import pprint
            pprint( scanhash )
        else:
            explain = False
            if 'explain' in options:
                explain = True
            cmdline.PrintScanSummary(scanhash, explain)
        sys.exit(0)
    

########################################################################
#
# update, restore, rollback commands
# these are expected to return a (acquirelock, session, initstate) tuple
#


def UpdateCommand(db, options, args, plock):
    acquirelock = 1

    if len(args) > 1:
        log.error('No options accepted after update command')
        BadArgsHelp()

    if 'depoturl' in options and len(options['repourls']) > 0:
        log.error('-d/--depot option cannot be used with -r option.')
        BadArgsHelp()
    elif 'depoturl' not in options and len(options['repourls']) == 0:
        if not CheckDepotURL(options):
            CheckRepoURLs(options['repourls'])

    if 'hostagent' in options and 'maintenancemode' not in options:
        log.error('--maintenancemode=0/1 is required when using --HA.')
        BadArgsHelp()

    try:
        if 'depoturl' in options:
            session = NewDepotSession(db, options)
            initstate = InstallState.ConfigState
        elif len(options['repourls']) > 0:
            session = NewRepoSession(db, options, options['repourls'])
            if 'old_modules' in options:
                initstate = InstallState.LegacyDashRConfigState
            else:
                initstate = InstallState.ConfigState
    except EnvironmentError, e:
        log.error('Error creating new install session: ' + str(e))
        sys.exit(errors.Codes.IO)

    return acquirelock, session, initstate


def RestoreCommand(db, options, args, plock):
    acquirelock = 1
    session = GetLastSession(db, options)
    lastState = session.GetState()
    if lastState == 'PreInstDoneState':
        initstate = InstallState.ConfigState
        #
        # Continuing an installation after PreInstall
        # Use the existing lock if possible -- our PID
        # should be the same as before
        #
        if plock.ReadPID() == os.getpid():
            acquirelock = 0
    else:
        log.error('Cannot restore from state %s.  Please re-run esxupdate.' %
                  (lastState))
        sys.exit(errors.Codes.BAD_STATE)

    return acquirelock, session, initstate


def RollbackCommand(db, options, args, plock):
    acquirelock = 1
    #
    # Replace the install options with our current ones, and
    # update the package state to see what's been installed.
    #
    session = GetLastSession(db, options)
    session.options = {}
    session.options.update(options)
    if session.desc:
        session.UpdatePendingFromHost()
    initstate = InstallState.RollbackState
    if not session.repackaged:
        log.error('Rollback was not enabled for the last installation.')
        sys.exit(errors.Codes.NO_BACKUPS)
    if len(session.installed) == 0:
        log.info('No packages were installed - nothing to rollback.')
        sys.exit(errors.Codes.NOP)

    return acquirelock, session, initstate


########################################################################
#
#  Main()
#    Process command line options
#    Farm out the work
#
def Usage():
    print __doc__
    sys.exit(errors.Codes.BAD_ARGS)

def BadArgsHelp():
    print "Enter %s by itself to see a summary of commands and options." % (sys.argv[0])
    sys.exit(errors.Codes.BAD_ARGS)

def Main():
    excludes = []
    options = {}
    #
    # Disable rollbacks & repackage until/unless we fix rollbacks
    # This saves disk space & installation time
    #
    options['enableRollback'] = 0
    options['bundleIDs'] = []
    options['repourls'] = []
    options['keyringDir'] = KEYRING_DIR
    options['pullDeps'] = 1

    plock = Lock(LOCKFILE)
    acquirelock = 1
    
    #
    # Parse input options using getopt
    #
    sumlog = logging.getLogger('summary')
    sumlog.debug(' '.join(sys.argv))
    try:
        opts, args = getopt.getopt(sys.argv[1:], "r:fnv:x:laod:b:h",
                                   ["repo=", "depot=", "force", "noreboot",
                                    "nosigcheck", "flushcache",
                                    "exclude=", "test", "verbose=",
                                    "listrpms", "listall", "onlyobsolete",
                                    "nobackup", "HA", "maintenancemode=",
                                    "proxy=", "explain","help"])
    except getopt.GetoptError, e:
        print e.msg
        BadArgsHelp()

    #
    # getopt doesn't handle the case when you expect an argument
    # and supply none, eg 'makerepo.py -b -d blah'  returns '-d' as the arg of -b.
    # Let's filter this case out
    #
    for o, a in opts:
        if o in ("-r", "--repo") and a[0] != '-':
            options['repourls'].append(a)
        elif o in ("-d", "--depot") and a[0] != '-':
            options['depoturl'] = a
        elif o in ("-b",) and a[0] != '-':
            options['bundleIDs'].append(a)
        elif o in ("-v", "--verbose") and a[0] != '-':
            try:
                options['verbosity'] = int(a)
            except ValueError:
                log.error('A number must follow --verbose.')
                BadArgsHelp()
            if options['verbosity'] not in [10, 20, 30, 40]:
                log.error("""Enter the valid verbose level number.
 ( 10=debug / 20=info / 30=warning / 40=error )""")
                BadArgsHelp()
            LogSetLevel(options['verbosity'])
        elif o in ("--nosigcheck",):
            options['nosigcheck'] = 1
        elif o in ("--flushcache",):
            options['flushcache'] = 1
        elif o in ("-n", "--noreboot"):
            options['noreboot'] = 1
        elif o in ("-f", "--force"):
            options['force'] = 1
        elif o in ("-x", "--exclude"):
            excludes.append(a)
        elif o in ("-l", "--listrpms"):
            options['listrpms'] = 1
        elif o in ("-a", "--listall"):
            options['listall'] = 1
        elif o in ("-o", "--onlyobsolete"):
            options['listall'] = 1
            options['onlyobsolete'] = 1
        elif o in ("--test",):
            options['test'] = 1
        elif o in ("--nobackup",):
            options['enableRollback'] = 0
        elif o in ("--HA",):
            log.debug('Enabling hostagent interface')
            LogSetLevel( logging.WARNING ) #this should disable output on stdout
            halog.setLevel( logging.WARNING ) #this should enable logging on halog
            options['hostagent'] = 1
            options['pullDeps'] = 0
        elif o in ("--maintenancemode",) and a[0] != '-':
            try:
                options['maintenancemode'] = int(a)
            except ValueError:
                log.error('--maintenancemode only accepts 0 or 1.')
                BadArgsHelp()
        elif o in ("--explain",):
            options[ o[2:] ] = 1
        elif o in ("-h", "--help"):
            options['help'] = 1
        else:
            log.error('Bad arguments %s %s' % (o, a))
            BadArgsHelp()

    if excludes:
        options['excludes'] = excludes

    #
    # Process command and args, or print help if no command given.
    #
    if len(args) == 0 or options.get('help'):
        Usage()
        
    cmd = args[0]
    cmdDispatchTable = {
                        'update':   UpdateCommand,
                        'info':     PatchInfoCommand,
                        'query':    PatchQueryCommand,
                        'scan':     ScanCommand,
                        'restore':  RestoreCommand,
                        'rollback': RollbackCommand,
                        'import':   ImportDescCommand,
                        'db':       ManageDbCommand,
                       }

    db = PatchDB(DB_DIR)
    try:
        retval = cmdDispatchTable[cmd](db, options, args, plock)
    except KeyError, e:
        log.error('Unknown command %s' % (cmd))
        log.debug(str(e))
        BadArgsHelp()

    if not retval:
        #the command did it's job fully - no need to call Run()
        sys.exit(0)

    acquirelock, session, initstate = retval
    log.debug('Options %s  Command %s' % (str(session.options), cmd))

    finalState = Run( acquirelock, plock, session, initstate )
    
    ParseFinalStateAndExit( session, finalState )


def Run( acquirelock, plock, session, initstate ):
    """Conditionally lock, then run through the session state machine
    returns the finalState

    can potentially sys.exit() with error codes"""

    #
    # Before starting an installation, lock esxupdate.pid file
    # to prevent concurrent installs.
    #
    if acquirelock:
        try:
            plock.Lock(os.getpid())
        except LockError, e:
            log.error('Another esxupdate installation (PID=%s) is running.\n'
                      'Please wait for that installation to finish first.\n'
                      'PID:%s' % (e.pid, e.pid) )
            sys.exit(errors.Codes.LOCK)
        except EnvironmentError, e:
            log.error('Error locking %s: %s' % (LOCKFILE, str(e)))
            sys.exit(errors.Codes.IO)
    
    #
    # Go through install session state machine starting with initstate
    #
    try:
        session.RunStateMachine(initstate(session))
        plock.Unlock()
    #
    # All the expected exceptions have been handled by the InstallState
    # instances themselves, and the exception obj saved.  Here are errant
    # uncaught exceptions.
    #
    except LogCommandError, e:
        insthelper.Cleanup()
        log.error("Error (%d) executing [%s]\n%s" % (e.res>>8, e.cmd, e.output))
        sys.exit(errors.Codes.SHELL)
    except errors.TimeoutError, e:
        #
        # Cleanup after hang and let users restart install
        #
        insthelper.DumpChildren()
        insthelper.Cleanup()
        log.error("A reboot is recommended to clean up hung processes.")
        log.error("The installation can be restarted afterwards.")
        sys.exit(errors.Codes.IO)

    finalState = session.GetState()
    log.debug('Final state: %s' % (finalState) )

    return finalState

    
def ParseFinalStateAndExit( session, finalState ):
    #
    # Parse final state / last error to decide on exit code
    #
    e = session.GetLastErr()
    if finalState in ('SuccessState', 'TestCompletedState',
                      'RebootState', 'RollbackDone'):
        sys.exit(0)
    elif finalState == 'SuccessNeedsRebootState':
        sys.exit(errors.Codes.NEEDS_REBOOT)
    elif finalState == 'SuccessNeedsHostdRestart':
        sys.exit(errors.Codes.HOSTD_RESTART)
    elif e and hasattr(e, 'Report'):
        #
        # Universal error reporter.  Most errors will have a Report
        # method and end up here.
        #
        e.Report( log, halog )
        if hasattr( e, 'exitCode' ):
            sys.exit( e.exitCode )
        else:
            #
            # Make sure a nonzero exit code is used;  and a
            # warning is better than a stack trace
            log.warning('No error code exists - this is a BUG')
            sys.exit(errors.Codes.BAD_STATE)
    elif isinstance(e, EnvironmentError):
        log.error(str(e))
        sys.exit(errors.Codes.IO)
    elif finalState == 'ConfigFailed':
        if isinstance(e, errors.NotApplicableError):
            log.info('Sorry, but the bundle(s) cannot be installed on this host.')
            log.error('NotApplicableError Message:%s' % (e.msg))
            sys.exit(errors.Codes.GENERIC)
        else:
            log.error('Unrecognized error %s' % (e.__class__))
            sys.exit(errors.Codes.GENERIC)
    elif finalState in ('PrepFailed', 'DownloadFailed',
                        'TestFailed'):
        log.error("Error preparing for installation: " + str(e))
        sys.exit(errors.Codes.GENERIC)
    elif finalState == 'ForceFailed':
        log.error('RPM Force Failed. Error class: %s' % (e.__class__))
        sys.exit(errors.Codes.RPM)
    elif finalState == 'UpgradeFailed':
        log.error('Upgrade Failed. Error class:  %s' % (e.__class__))
        sys.exit(errors.Codes.YUM)
    elif finalState == 'RpmDependencyUnmet':
        if isinstance(e, errors.RpmDependencyError):
            log.info(e.msg)
            log.error('Deps:%s' % e.nodeps)
        else:
            log.error('Unexpected error %s' % (e.__class__))
        sys.exit(errors.Codes.RPM_DEP)
    elif finalState == 'PostInstallFailed':
        log.error( e )
        sys.exit(errors.Codes.POST)
    elif finalState == 'RollbackFailed':
        if isinstance(e, LogCommandError):
            log.info('If the rollback failed due to RPM dependencies, --force may be')
            log.info('used with the rollback command to override RPM dependencies,')
            log.info('but this may break your installed software.')
            sys.exit(errors.Codes.ROLLBACK)
        elif isinstance(e, errors.RollbackError):
            sys.exit(errors.Codes.ROLLBACK)
        else:
            sys.exit(errors.Codes.IO)
    else:
        log.error('Should not be ending up in state %s' % (finalState))
        sys.exit(errors.Codes.BAD_STATE)

#
#
#
if __name__ == "__main__":
    if not python_required('2.2'):
        print "Python version 2.2 or greater is needed to run esxupdate"
        sys.exit(99)
    if os.geteuid() != 0:
        print "Sorry, esxupdate can only be run as root"
        sys.exit(errors.Codes.NOT_ROOT)
    log = LogSetup('/var/log/vmware/esxupdate.log', 1048576)
    halog = logging.getLogger('hostagent')
    halog.setLevel(logging.CRITICAL)

    Main()
    logging.shutdown()
