Esempio n. 1
0
class InstallManager(object):
    """
    This is the main install manager
    """

    def __init__ (self, logger, product, sysVarDir, sysRunDir, dataVarDir, rpmsDir, 
                  appDir, appDirReal, bootDevPath, grubConf, prevRfsDir=None, vitalDir=None, systemRoot="/", useFakeChrootMode=False):
        """
        product : Name of product we handle, e.g. 'qb'.
        sysVarDir : path to sys dir relative to root, e.g. '/opt/qb/sys/oscar/install/0/var'. 
        sysRunDir : path to sys dir relative to root, e.g. '/opt/qb/sys/oscar/install/0/run'. 
        dataVarDir : path to data dir relative to root, e.g. '/opt/qb/data/oscar/install/0/var'. 
        rpmsDir : path to rpms dir relative to root, e.g. '/opt/qb/rpms'. 
        appDir : path to app dir relative to root, e.g. '/opt/qb/app'. 
        appDirReal : real path to app dir relative to root, e.g. '/opt/qb/sys/rfs/sys/0/var/app'. 
        bootDevPath : Path to boot device, e.g. '/boot'
        grubConf : name of grub.conf file relative to boot device, e.g. 'grub/grub.conf'.
        systemRoot : For unit tests, specifies location of the file-system root.
        useFakeChrootMode : For unit tests, True to use fakechroot instead of chroot (via rpm --root or yum --installroot)
        prevRfsDir : Location to copy some RFS data to during OS install
        vitalDir : Path to vital dir.
        """

        self._raiseIfNotAbsPath(systemRoot, "systemRoot")
        self._log = logger.createLogger("sys-install", "install-manager")
        self._log.setInstance(product)
        self._utils = Utils(self._log)
        self._product = product
        self._bootDevPath = bootDevPath
        self._grubConf = grubConf
        self._systemRoot = systemRoot
        self._useFakeChrootMode = useFakeChrootMode
        self._vitalDir = vitalDir

        self._rpmsDir = os.path.join(systemRoot, self._chopLeadingSlashIfFound(rpmsDir))

        self._sysVarDir = os.path.join(systemRoot, self._chopLeadingSlashIfFound(sysVarDir))
        self._sysRunDir = os.path.join(systemRoot, self._chopLeadingSlashIfFound(sysRunDir))
        self._dataVarDir = os.path.join(systemRoot, self._chopLeadingSlashIfFound(dataVarDir))
        self._logDir=os.path.join(self._dataVarDir, 'log')
        self._stateDir = os.path.join(self._sysVarDir, "state")
        self._yumConfActiveRfsDir = os.path.join(self._sysVarDir, "yum-conf-active-rfs")
        self._yumConfNewVersionDir = os.path.join(self._sysVarDir, "yum-conf-new-version")
        self._appDir = os.path.join(systemRoot, self._chopLeadingSlashIfFound(appDir))
        self._appDirReal = os.path.join(systemRoot, self._chopLeadingSlashIfFound(appDirReal))
        self._installDir = os.path.join(self._sysVarDir,"sub-pkgs")
        self._workDir=os.path.join(self._sysVarDir, 'work')
        self._apiDir=os.path.join(self._sysVarDir, 'api')
        self._startupScriptsDir=os.path.join(self._sysVarDir, 'startup')
        self._startupScriptsPrevDir=os.path.join(self._startupScriptsDir, 'prev')
        self._rpmKeysDir = os.path.join(self._sysVarDir, "rpm/keys")
        self._tempDir = os.path.join(self._sysRunDir, "tmp")
        self._installTempDir = os.path.join(self._tempDir, "sub-pkgs")  # install-temp can be cleared between runs
        self._subPkgManager = SubPkgManager(self._log)
        self._subPkgManager.setDirs(self._systemRoot, self._installDir, self._installTempDir, self._rpmKeysDir, self._useFakeChrootMode)

        self._stateFile = os.path.join(self._stateDir, product+"-state")
        self._stateManager = StateManager(self._log)
        self._stateManager.setFile(self._stateFile)
        self._pilotHints=""
        self._prevRfsDir = prevRfsDir
        self._preparedPackageFilePath = None

        self._log("init").info("InstallManager::__init__() called, self._product=%s, self._sysVarDir=%s, self._sysRunDir=%s, "\
                               "self._dataVarDir=%s, self._rpmsDir=%s, self._appDir=%s, self._bootDevPath=%s, "\
                               "self._grubConf=%s, self._prevRfsDir=%s, self._vitalDir=%s", 
                               self._product, self._sysVarDir, self._sysRunDir, self._dataVarDir, self._rpmsDir, 
                               self._appDir, self._bootDevPath, self._grubConf, self._prevRfsDir, self._vitalDir)

        # If true, we will not actually install RPMs. This helps modify this module in-box and testing the 
        # changes, to avoid having to create a package file each time
        self._debugNoInstallMode=False

    def unit_test_initializeSystemFirstInstall (self, curVersion, curBuild, keys):
        """
        Should be called once after first install to initialize the state file and keys.
        Used only by unit tests. In real life, the extract_pkg script does this.
        
        curVersion : Name of the current version
        curBuild : Name of the current build
        keys : listof paths to ascii armoured files with gpg keys for rpm validation
        """
        self.initializeSystemFirstInstall(curVersion, curBuild)
        
        # Import rpm keys
        rpm=self._createRpmForActiveRfs()
        for key in keys:
           rpm.doImport(key)


    def initializeSystemFirstInstall (self, curVersion, curBuild):
        """
        Should be called once after first install to initialize the state file and keys.
        
        curVersion : Name of the current version
        curBuild : Name of the current build
        """
        self._log("initial-set-cur-version").info("initialSetCurVersion() called, curVersion=%s", curVersion)
        
        # Create initial dirs
        self._utils.runCommandRaiseIfFail("mkdir -p "+self._stateDir)
        
        # Set initial state
        self._stateManager.initialSetCurVersion(self.combineVersion(curVersion, curBuild))



    def init (self):
        """
        Must be called after creating this object, to initialize it, on a system that has been initialized 
        (i.e. has a state file on disk)
        """
        self._log("init").info("init() called")
        
        # Make sure these dirs exist. If not, create them.
        self._utils.runCommandRaiseIfFail("mkdir -p "+self._sysRunDir)
        self._utils.runCommandRaiseIfFail("mkdir -p "+self._logDir)
        self._utils.runCommandRaiseIfFail("mkdir -p "+self._workDir)
        self._utils.runCommandRaiseIfFail("mkdir -p "+self._apiDir)        
        self._utils.runCommandRaiseIfFail("mkdir -p "+self._startupScriptsDir)
        self._utils.runCommandRaiseIfFail("mkdir -p "+self._startupScriptsPrevDir)

        #TODO(orens): catch InstallException that might be raised during load, reset state and restart oscar
        self._log("init").info("init() loading state")
        self._stateManager.load()

        self._initPlatformBasic()
        self._initSdUtils()
        self._initBootUtils()

    def isPendingRestart (self):
        """
        Returns true if 'switch' was executed, we are not waiting for system to restart.
        """
        return self._stateManager.isLocked()

    def setPilotHints (self, hints):
        self._log("setPilotHints").info("setPilotHints(hints=%s) called", hints)
        self._pilotHints = hints

    def startup (self, logDir = None):
        """
        Must be called during startup of an installed system, i,e, upon every Oscar start.
        Call this after callint init()
        When logDir is passed, the start-up script writes its logs into that directory, otherwise default directory is used.
        """
        self._log("startup").info("startup() called")

        #TODO(orens): In the future, when we do not need to support upgrading from EFT versions,
        # remove this call, and all the setVersionToActivateOnStartup() support from StateManager
        self._stateManager.startup()

        # Run all scripts from the startup dir, move them to 'prev' later
        self._log("startup-running").info("startup(): Scanning directory %s for startup scripts", self._startupScriptsDir)
        files=os.listdir(self._startupScriptsDir)
        for file_ in files:
            fullPath=os.path.join(self._startupScriptsDir, file_)

            if logDir is not None:
                startupCommand = fullPath + " --log-dir " + logDir
            else:
                startupCommand = fullPath

            # Do not run .tmp files or directories :-)
            if (not fullPath.endswith(".tmp")) and os.path.isfile(fullPath):
                self._log("startup-running").info("startup(): running startup script '%s'", startupCommand)
                # If script fails, raise and get out of here. This should fail oscar start
                self._utils.runCommandRaiseIfFail(startupCommand)
                # Move used script to 'prev' dir, to lay there forever shamed.
                self._utils.runCommandRaiseIfFail("mv -f %s %s" % (fullPath, self._startupScriptsPrevDir))

    def getActiveVersionAndBuild (self):
        """
        Returns the current active (version, build)
        """
        self._log("get-active-version").info("getActiveVersionAndBuid() called")
        versionAndBuild=self._stateManager.getActiveVersion()
        (version_, build) = self.splitVersion(versionAndBuild)
        self._log("get-active-version").info("getActiveVersionAndBuid() got %s, returning %s, %s", 
                                             versionAndBuild, version_, build)
        return (version_, build)

    def getReadyVersion (self):
        """
        Returns (version, build) of currently ready version. 
        If no version is ready, returns None
        """
        versionCombined = self._stateManager.getReadyVersion()
        if versionCombined==None:
            return None

        (version_, build) = self.splitVersion(versionCombined, raiseIfInvalid=True)
        return (version_, build)

    def getReadyVersion2 (self):
        """
        Returns (version, build, switch type, install direction) of currently ready version. 
        If no version is ready, returns None
        """
        versionCombined = self._stateManager.getReadyVersion()
        if versionCombined==None:
            return None
        switchType = self._stateManager.getSwitchType(versionCombined)

        (version_, build) = self.splitVersion(versionCombined, raiseIfInvalid=True)
        return (version_, build, switchType)

    def getStateManager (self):
        return self._stateManager

    def getPreparedPackageFilePath (self):
        return self._preparedPackageFilePath
        
    def isPackageValid (self, fileName):
        """
        Checks that an RPM file is a signed, and contains a proper version for our product.
        Returns None if invalid, or (version, build) if valid
        """
        self._log("is-valid").info("isPackageValid(fileName=%s) called", fileName)
        if not self._isPackageSigned(fileName):
            self._log("is-valid").warning("isPackageValid() returning false, fileName=%s not signed", fileName)
            return None
        versionCombined = self._subPkgManager.findVersionInFile(self._product, fileName)
        if versionCombined is None:
            self._log("is-valid").warning("isPackageValid() returning false, no version of product %s found in it", self._product)
            return None
        (version_, build) = self.splitVersion(versionCombined)
        if version_ is None:
            self._log("is-valid").warning("isPackageValid() returning false, no version of product %s found in it", self._product)
            return None
        return (version_, build)

    def addSubPkgs (self, fileName):
        """
        Adds a package file to the repos. Raises InvalidPackageFileException if not a valid package file for this product

        Returns (version, build)
        """

        self._log("add").info("addSubPkgs(fileName=%s) called", fileName)
        self._raiseIfNotAbsPath(fileName, "fileName")

        valid=self._isPackageSigned(fileName)
        if not valid:
            self._log("add-bad-rpm").error("addSubPkgs() got bad package file, not properly signed", self._product)
            raise InvalidPackageFileException()

        versionCombined = self._subPkgManager.findVersionInFile(self._product, fileName)
        if versionCombined is None:
            self._log("add-bad-version").error("addSubPkgs() got bad package file, no version of product %s found in it", self._product)
            raise InvalidPackageFileException()
        (version_, build) = self.splitVersion(versionCombined)
        if version_ is None:
            self._log("add-bad-version").error("addSubPkgs() got versionInFile '%s', cannot split.", versionCombined)
            raise InvalidPackageFileException()

        # Check if version is active or ready. If yes, raise exception
        self._raiseIfVersionActiveOrReady(versionCombined)

        # Extract package file into sub-pkgs dir
        (subPkgs, newSubPkgs)=self._subPkgManager.add(fileName)
        self._log("add-got-subpkgs").info("addSubPkgs() returning newSubPkgs=%s", newSubPkgs)

        self._stateManager.addVersion(versionCombined, subPkgs=subPkgs, subPkgsToRemove=newSubPkgs)

        # Remember the file path
        self._preparedPackageFilePath = fileName

        self._log("add").info("addSubPkgs() returning version=%s, build=%s", version_, build)
        return (version_, build)

    def addSubPkgsFromHttp (self, repoHttpAddr, version_, build):
        """
        Adds a package file to the repos. Raises InstallException if package not found.
        """

        self._log("add-from-http").info("addSubPkgsFromHttp(repoHttpAddr=%s, version_=%s, build=%s) called", repoHttpAddr, version_, build)

        versionCombined = self.combineVersion(version_, build)
        if versionCombined is None:
            self._log("add-from-http-bad-version").error("addSubPkgsFromHttp() got bad version(=%s) or build(=%s) parameters", version_, build)
            raise InstallException("Invalid version '%s', build '%s' specificaion" % (version_, build))

        # Check if version is active or ready. If yes, raise exception
        self._raiseIfVersionActiveOrReady(versionCombined)

        packageName = "%s-%s" % (self._product, versionCombined)
        self._log("add-from-http-package").info("addSubPkgsFromHttp(): installing package %s", packageName)

        # Extract package file into sub-pkgs dir
        (subPkgs, newSubPkgs)=self._subPkgManager.add(httpRepo=repoHttpAddr, packageName=packageName)
        self._log("add-from-http-got-subpkgs").info("addSubPkgsFromHttp() got newSubPkgs=%s", newSubPkgs)

        self._stateManager.addVersion(versionCombined, subPkgs=subPkgs, subPkgsToRemove=newSubPkgs)

        self._log("add-from-http-done").info("addSubPkgsFromHttp() done")

    def removeVersionAndSubPkgs (self, version_, build, removeSubPackages=True):
        """
        Removes sub-pkgs added by a version during 'prepare', and the version itself.
        Version must have state 'on-disk'
        """
        self._log("remove-subpkgs").info("removeVersionAndSubPkgs(version=%s, build=%s) called", version_, build)

        versionCombined = self.combineVersion(version_, build)
        versionState = self._stateManager.getVersionState(versionCombined)
        if (versionState != StateManager.kStateOnDisk):
            msg = "removeVersionAndSubPkgs(): Version %s is not on-disk, it is in state '%s'" % (versionCombined, versionState)
            self._log("remove-subpkgs").error(msg)
            raise InstallException(msg)

        if removeSubPackages:
            subPkgsToRemove = self._stateManager.getSubPkgsToRemoveOnRemoval(versionCombined)
            self._log("remove-subpkgs").info("Removing sub-packages: %s", subPkgsToRemove)
            if len(subPkgsToRemove):
                self._subPkgManager.removeSubPackages(subPkgsToRemove)

        self._log("remove-subpkgs").info("Removing version %s from state", versionCombined)
        self._stateManager.removeVersion(versionCombined)

    def removeUnneededVersionsFromDisk (self):
        """
        Removes old versions from disk.
        Scans all versions maintained in the state, and for each version which is 'on-disk', if it is not needed (i.e. 
        for a possible rollback command), removes sub-packages and RPMs installed by it and not required by the active versions
        """

        self._log("remove-undeeded-vers-called").info("removeUnneededVersionsFromDisk() called")
        # Find things needed by the current active and ready versions
        # Currently no rollback support, hence nothing else is needed.
        versions = self._stateManager.getVersions()
        versionsToRemove=[]
        versionsToKeep=[]
        for ver in versions:
            state=self._stateManager.getVersionState(ver)
            # TODO(orens): When we add rollback support, keep also the 'on-disk' version we may rollback into.
            if state==StateManager.kStateOnDisk:
                versionsToRemove.append(ver)
            else:
                versionsToKeep.append(ver)

        # Find list of RPMs and list of subpackages we must keep
        (rpmsToKeep, subPkgsToKeep)=self._getRpmsAndSubPkgsOfVersions(versionsToKeep)

        # Find list of RPMs used by versions we remove
        (rpmsToRemove, subPkgsToRemove)=self._getRpmsAndSubPkgsOfVersions(versionsToRemove)

        # The crucial moment
        rpmsToRemove -= rpmsToKeep
        subPkgsToRemove -= subPkgsToKeep

        # Filter rpms in order to check which ones are not installed- only installed ones should be removed
        self._log("ruv-filtering1").info("removeUnneededVersionsFromDisk(): Filtering RPMs %s for not installed packages", rpmsToRemove)
        rpm=self._createRpmForActiveRfs()
        rpmsToRemove = filter(rpm.isInstalled, rpmsToRemove)

        # Check if pilot wants to modify RPM list
        activeVersion = self._stateManager.getActiveVersion()
        self._log("ruv-filtering3").info("removeUnneededVersionsFromDisk(): activeVersion is %s", activeVersion)
        (pilot, _) = self._createPilot(activeVersion, None)
        if hasattr(pilot, "hookModifyRpmsForRemovalOld"):
            rpmsToRemove=pilot.hookModifyRpmsForRemovalOld(rpmsToRemove)
            self._log("ruv-filtering5").info("removeUnneededVersionsFromDisk(): pilot hookModifyRpmsForRemovalOld() returned %s", rpmsToRemove)
        else:
            self._log("ruv-filtering6").info("removeUnneededVersionsFromDisk(): pilot hookModifyRpmsForRemovalOld() not implemented")

        # We do not need to filter sub-pkgs for existance - only existing ones would be removed

        self._log("ruv-removing-ver").info("removeUnneededVersionsFromDisk(): Removing RPMs %s", rpmsToRemove)
        self._log("ruv-removing-ver").info("removeUnneededVersionsFromDisk(): Removing sub-pkgs %s", subPkgsToRemove)

        if len(rpmsToRemove)>0:
            yum=self._createYumWithAllRepos()
            yum.doRemove(" ".join(rpmsToRemove))
        self._subPkgManager.removeSubPackages(subPkgsToRemove)
        self._log("ruv-removing-ver").info("removeUnneededVersionsFromDisk(): Removing versions %s from state", versionsToRemove)
        for ver in versionsToRemove:
            self._stateManager.removeVersion(ver)

    def _getRpmsAndSubPkgsOfVersions(self, versions):
        """
        Returns a tuple with two sets: (rpmsOfVersions, subPkgsOfVersions)
        """
        rpmsOfVersions=set()
        subPkgsOfVersions=set()
        for ver in versions:
            self._log("get-rpms-and-subpkgs").info("_getRpmsAndSubPkgsOfVersions() Handling version %s", ver)
            leaders=self._stateManager.getLeaders(ver)
            self._log("get-rpms-and-subpkgs").info("_getRpmsAndSubPkgsOfVersions(): leaders=%s", leaders)
            for leader in leaders.values():
                rpmsOfVersions.add(leader)

            subPkgs=self._stateManager.getSubPkgs(ver)
            self._log("get-rpms-and-subpkgs").info("_getRpmsAndSubPkgsOfVersions(): subPkgs=%s", subPkgs)
            for subPkg in subPkgs:
                subPkgsOfVersions.add(subPkg)

            # The pilot of a version is always called like this:
            rpmsOfVersions.add(self.calcPilotName(ver))

        # Expand set by adding all required packages
        rpmsOfVersions=self._expandRpmsUsingReqires(rpmsOfVersions)
        self._log("get-rpms-and-subpkgs").info("_getRpmsAndSubPkgsOfVersions(): Returning rpmsOfVersions=%s", rpmsOfVersions)
        self._log("get-rpms-and-subpkgs").info("_getRpmsAndSubPkgsOfVersions(): Returning subPkgsOfVersions=%s", subPkgsOfVersions)
        return (rpmsOfVersions, subPkgsOfVersions)

    def _expandRpmsUsingReqires (self, packages):
        """
        Gets an iterable with a list of packages (Some may not be installed it is OK).
        Returns a set with the same packages, plus all installed packages required by these installed packages
        """

        self._log("expand-requires").info("_expandRpmsUsingReqires(packages=%s) called, ", packages)
        rpm=self._createRpmForActiveRfs()

        doIt=True
        ret=set(packages)
        while doIt:
            self._log("expand-requires").info("_expandRpmsUsingReqires() ret=%s", ret)
            doIt=False
            addedPackages=set()
            for package in packages:
                requires=rpm.doListRequires(package)
                for (capName, capCondition, capVersion) in requires:
                    # Ignore 'rpmxxx' capabilities
                    if capName.startswith('rpm'):
                        continue
                    # Ignore capabilities which are not '='.
                    if capCondition != '=':
                        continue
                    addedPackages.add(capName+'-'+capVersion)

            # Found something new ? Add to result and continue iterating
            self._log("expand-requires").info("_expandRpmsUsingReqires() addedPackages=%s, ret=%s", addedPackages, ret)
            newPackages = addedPackages-ret
            self._log("expand-requires").info("_expandRpmsUsingReqires() newPackages=%s", newPackages)
            if len(newPackages) > 0:
                doIt=True
                ret |= newPackages
                packages = newPackages
        self._log("expand-requires").info("_expandRpmsUsingReqires() returning %s", ret)
        return ret


    def prepare (self, version_, build, usePilotVersion=None):
        """
        Prepares a version for installation.
        If a version is already prepared, raises InstallException().

        usePilotVersion: If != None, sets pilot version to be used instead of auto-selecting.
        """
        self._log("prepare").info("prepare(version=%s, build=%s) called", version_, build)

        if self._stateManager.getReadyVersion() != None:
            msg="prepare(): A version is ready"
            self._log("prepare").error(msg)
            raise InstallException(msg)

        if build == kFakeBuildNumber:
            msg="prepare(): Invalid build number. Build '%s' is reserved for fake builds" % kFakeBuildNumber
            self._log("prepare").error(msg)
            raise InstallException(msg)

        newVersion=self.combineVersion(version_, build)
        activeVersion=self._stateManager.getActiveVersion()

        self._installPilot(newVersion)
        self._log("prepare").info("Creating pilot for new version %s", newVersion)
        (newVersionPilot, newVersionPs) = self._createPilot(newVersion, newVersion)
        self._log("prepare").info("Creating pilot for active version %s", activeVersion)
        (activeVersionPilot, activeVersionPs) = self._createPilot(activeVersion, newVersion)

        # Teach each pilot services about the other pilot
        newVersionPs.setOtherPilot(activeVersionPilot)
        activeVersionPs.setOtherPilot(newVersionPilot)

        if usePilotVersion != None:
            self._log("prepare").info("Creating pilot for version %s, manually selected", usePilotVersion)
            if (usePilotVersion != newVersion) and (usePilotVersion != activeVersion):
                msg="Manually selected pilot should be one of %s or %s, not %s" % (newVersion, activeVersion, usePilotVersion)
                self._log("prepare").error("prepare(): %s", msg)
                raise InstallException(msg)
            selectedVersion=usePilotVersion
        else:
            self._log("prepare").info("Running pilot selection")
            # old pilot selection logic uses 'selectPilot'
            # new pilot selection logic uses 'getPilotGeneration'
            # If new version pilot supports new logic, use it.
            # If new version pilot does not support new logic, then this is downgrade, we choose our pilot.
            if hasattr(newVersionPilot, "getPilotGeneration"):
                newPilotGeneration=newVersionPilot.getPilotGeneration()
                activePilotGeneration=activeVersionPilot.getPilotGeneration()
                self._log("prepare").info("prepare(): pilot selection: newVersion=%s, activeVersion=%s, newPilotGeneration=%s, activePilotGeneration=%s",
                                          newVersion, activeVersion, newPilotGeneration, activePilotGeneration)
        
                if newPilotGeneration > activePilotGeneration:
                    selectedVersion = newVersion
                elif newPilotGeneration < activePilotGeneration:
                    selectedVersion = activeVersion
                else:   # Pilot generations are equal, choose according to version numbers
                    selectedVersion=self._selectLatestVersion(newVersion, activeVersion)
            else: # Downgrading, choose our pilot
                selectedVersion = activeVersion

        if selectedVersion == newVersion:
            pilot = newVersionPilot
        elif selectedVersion == activeVersion:
            pilot = activeVersionPilot
        else:
            msg="Bad pilot: newVersion=%s, activeVersion=%s, selectedVersion=%s" % (newVersion, activeVersion, selectedVersion)
            self._log("prepare").error("prepare(): pilot selection: %s", msg)
            raise InstallException(msg)

        # Version is selected, We have a pilot, let's take off
        self._log("prepare").info("prepare(): Selected pilot version is %s", selectedVersion)

        self._stateManager.setPilotName(newVersion, selectedVersion)

        rpm = self._createRpmForActiveRfs()
        installedRpmsBefore=rpm.doGetInstalledPackages()
        self._log("prepare").info("prepare(): Installed RPMs before prepare() are: %s", installedRpmsBefore)

        # As last, Run pilot 'prepare' function !

        # Pilot is executed in try block. In case of failure, we clear pilot name.
        self._log("prepare").info("prepare(): running pilot...")
        try:
            # Old pilots do not have doPrepare2(), but we should never call old pilots from new code,
            # because the active version pilot should win.
            pilot.doPrepare2(newVersion, newVersionPilot, activeVersionPilot)
        except:
            self._stateManager.setPilotName(newVersion, None)
            raise

        installedRpmsAfter=rpm.doGetInstalledPackages()
        self._log("prepare").info("prepare(): Installed RPMs after prepare() are: %s", installedRpmsBefore)
        newRpms = sorted(list(set(installedRpmsAfter)-set(installedRpmsBefore)))

        self._log("prepare").info("prepare(): New RPMs: %s", newRpms)
        self._stateManager.setRpmsToRemoveOnRemoval(newVersion, newRpms)

        # Pilot prepare went ok - time to mark this version as 'ready' !
        self._log("prepare").info("prepare(): pilot did good, marking version as ready")
        self._stateManager.setReadyVersion(newVersion)

    def cancel (self, version_, build):
        self._log("cancel").info("cancel(version=%s, build=%s) called", version_, build)

        versionCombined = self.combineVersion(version_, build)
        versionState = self._stateManager.getVersionState(versionCombined)
        if (versionState != StateManager.kStateReady):
            msg = "cancel(): Version %s is not ready, it is in state '%s'" % (versionCombined, versionState)
            self._log("cancel").error(msg)
            raise InstallException(msg)

        self._log("cancel").info("Creating pilot for version %s", versionCombined)
        (pilot,_)=self._createPilot(self._stateManager.getPilotName(versionCombined), versionCombined)
        if hasattr(pilot, "hookCancelBeforeRemove"):
            self._log("cancel").info("Calling pilot hookCancelBeforeRemove()")
            pilot.hookCancelBeforeRemove(versionCombined)

        rpmsToRemove = self._stateManager.getRpmsToRemoveOnRemoval(versionCombined)
        rpm = self._createRpmForActiveRfs()
        self._log("cancel").info("Removing rpms: %s", rpmsToRemove)
        if len(rpmsToRemove):
            rpm.doRemove(" ".join(rpmsToRemove))

        if hasattr(pilot, "hookCancelAfterRemove"):
            self._log("cancel").info("Calling pilot hookCancelAfterRemove()")
            pilot.hookCancelAfterRemove(versionCombined)

        self._log("cancel").info("Changing state of version %s to 'on-disk'", versionCombined)
        self._stateManager.setVersionStateOnDisk(versionCombined)

    def switch (self, version_, build):
        """
        version_, build are parameters of the ready version

        On success, returns pilot doSwitch() return code.
        On failure, raises something
        """
        self._log("switch-called").info("switch(version=%s, build=%s) called", version_, build)
        versionCombined = self.combineVersion(version_, build)
        versionState = self._stateManager.getVersionState(versionCombined)
        if (versionState != StateManager.kStateReady):
            msg = "cancel(): Version %s is not ready, it is in state '%s'" % (versionCombined, versionState)
            self._log("cancel").error(msg)
            raise InstallException(msg)

        self._log("switch").info("switch() creating pilot for version %s", versionCombined)
        pilotName=self._stateManager.getPilotName(versionCombined)
        (pilot,_)=self._createPilot(pilotName, versionCombined)

        self._log("switch").info("switch() forgetting things to remove on removal for version %s", versionCombined)
        self._stateManager.setSubPkgsToRemoveOnRemoval(versionCombined, None)
        self._stateManager.setRpmsToRemoveOnRemoval(versionCombined, None)

        # Get params needed to determine which user-log message to emit, and emit it
        activeVersion=self._stateManager.getActiveVersion()
        switchType = self._stateManager.getSwitchType(versionCombined)
        installDirection = self._stateManager.getInstallDirection(versionCombined)
        self._log("switch").info("switch() got switchType='%s', installDirection='%s'", switchType, installDirection)

        if (switchType == "OS Restart") and (installDirection == "upgrade"):
            a.infra.process.logUserMessage(a.api.user_log.msg.install.OsUpgrade(activeVersion, versionCombined))
        elif (switchType == "OS Restart") and (installDirection == "downgrade"):
            a.infra.process.logUserMessage(a.api.user_log.msg.install.OsDowngrade(activeVersion, versionCombined))
        elif (switchType == "Application Restart") and (installDirection == "upgrade"):
            a.infra.process.logUserMessage(a.api.user_log.msg.install.AppUpgrade(activeVersion, versionCombined))
        elif (switchType == "Application Restart") and (installDirection == "downgrade"):
            a.infra.process.logUserMessage(a.api.user_log.msg.install.AppDowngrade(activeVersion, versionCombined))

        self._log("switch").info("switch() calling pilot doSwitch(%s)", versionCombined)

        # Returned value from pilot should signal caller something (Probably exit code).
        ret = pilot.doSwitch(versionCombined)

        # State is now locked. Let's check it, in case pilot was sloppy.
        if not self._stateManager.isLocked():
            msg="ERROR: switch() expecting state to be locked after pilot doSwitch()"
            self._log("switch").error(msg)
            raise InstallException(msg)

        return ret
        

    def verify (self):
        """
        Verifies all installed RPMs.
        Returns list of problems, empty list if OK
        """
        self._log("verify-called").info("verify() called")
        rpm=self._createRpmForActiveRfs()
        problems = rpm.doVerifyAllInstalled()
        self._log("verify-returning").info("verify() returning %s", problems)
        # We skip problems about some files which normally change after being installed
        filteredProblems = []
        for problem in problems:
            # /etc and /var files onbviously should not be checked
            if " /etc/" in problem:
                continue
            if " /var/" in problem:
                continue
            # .ini and .cfg files in /opt/dell seem to be changing
            if (" /opt/dell/" in problem) and (problem.endswith(".ini") or problem.endswith(".cfg")):
                continue
            # .pyc files in Oscar seem to be changing
            if (" /opt/qb/rpms/oscar-" in problem) and problem.endswith(".pyc"):
                continue
            # We over-ride firmware files when upgrading the kernel
            if " /lib/firmware/" in problem:
                continue
            # Handle a few specific files
            if (" /opt/MegaRAID/MegaCli/MegaCli" in problem) or (" /root/.bash_profile" in problem):
                continue
            filteredProblems.append(problem)
        return filteredProblems

    def getRpmsDir (self):
        return self._rpmsDir

    def getAppDir (self):
        return self._appDir

    def getAppNextDir (self):
        return self._appDir+".next"

    def getAppPrevDir (self):
        return self._appDir+".prev"

    def getAppDirReal (self):
        return self._appDirReal

    def getBootDevPath (self):
        return self._bootDevPath

    def getGrubConf (self):
        return self._grubConf

    def getVitalDir (self):
        return self._vitalDir

    def getSubPkgManager (self):
        return self._subPkgManager

    def debugSetNoInstallMode (self):
        self._log("debug-no-install-mode").notice("debugSetNoInstallMode() called, NO RPMS WILL BE INSTALLED")
        self._debugNoInstallMode=True

    def unit_test_ExtractPackageFile (self, fileName):
        self._subPkgManager.add(r2pmFile=fileName)

    def unit_test_InstallPilot (self, version_, build):
        versionCombined=self.combineVersion(version_, build)
        self._installPilot(versionCombined)

    def unit_test_InstallRpm (self, name):
        yum=self._createYumWithAllRepos()
        self._log("debug-install-rpm").info("debugInstallRpm(): installing %s", name)
        if not self._debugNoInstallMode:
            yum.doInstall(name)

    def unit_test_GetInstallTempDir (self):
        """Helper for unit tests"""
        return self._installTempDir

    def unit_test_GetStateFile (self):
        """Helper for unit tests"""
        self._log("get-state-file").info("unit_test_GetStateFile(): returning %s", self._stateFile)
        return self._stateFile


    def _initPlatformBasic (self):
        self._log("init-platform-basic").info("initializing the platform-basic")
        self._platformBasic = a.infra.process.getPlatformBasicDataOrCrash()


    def _initSdUtils (self):
        self._log("init-sd-utils").info("initializing the sd-utils")        
        self._sdUtils = SdUtils(self._log)


    def _initBootUtils (self):
        self._log("init-boot-utils").info("initializing the boot-utils")
        self._bootUtils = BootUtils.s_getFromOsefOrCrash(a.infra.process.getOsef())


    def _installPilot(self, versionCombined):
        yum=self._createYumWithAllRepos()
        pilotPackageName = self.calcPilotName(versionCombined)
        self._log("install-pilot").info("_installPilot(): pilotPackageName=%s", pilotPackageName)
        if not self._debugNoInstallMode:
            yum.doInstall(pilotPackageName)

    def _createPilot (self, versionCombined, newVersionCombined):
        """
        Returns (pilot, pilotServices)

        versionCombined - name of pilot to create
        newVersionCombined - name of version being installed ('v-b')
        """
        self._log("create-pilot").info("_createPilot() called, versionCombined = %s", versionCombined)
        pilotRpmDirName = self.calcPilotName(versionCombined)
        pilotDirName = os.path.join(self._rpmsDir, pilotRpmDirName)
        pilotFileName = os.path.join(pilotDirName, "pilot.py")
        self._log("create-pilot").info("_createPilot(): pilotFileName = %s", pilotFileName)
        if not os.path.exists(pilotFileName):
            self._log("create-pilot").error("_createPilot() Could not find pilot file %s", pilotFileName)
            raise InstallException("Could not find pilot file %s" % pilotFileName)

        # Keep a copy of sys.path
        oldSysPath=list(sys.path)
        try:
            sys.path.append(pilotDirName)
            self._log("create-pilot").info("_createPilot(): about to import, sys.path=%s", sys.path)
            import pilot
            reload(pilot)
            p=pilot.Pilot()
        except:
            self._log("create-pilot").error("_createPilot(): Got exception when importing and creating pilot")
            raise
        finally:
            # Restore old sys.path
            sys.path=oldSysPath
            self._log("create-pilot").info("_createPilot(): Restored sys.path to %s", sys.path)

        self._log("create-pilot").info("_createPilot(): creating pilot services, pilotDirName=%s", pilotDirName)
        pilotServices=self._createPilotServices(versionCombined, pilotDirName, newVersionCombined)
        p.setPilotServices(pilotServices)
        self._log("create-pilot-returning").info("_createPilot() returning pilot")
        return (p, pilotServices)
        

    def calcPilotName (self, versionCombined):
        pilotName="pilot-%s-%s" % (self._product, versionCombined)
        return pilotName

    def _isPackageSigned (self, fileName):
        """
        Checks that an RPM file is a signed
        """
        rpm=self._createRpmForActiveRfs()
        return rpm.doValidateRpm(fileName)

    def combineVersion (self, version_, build):
        """Combines version and build into a single string "v-b". version and buil cannot contain '-' chars. """
        if version_.find("-") != -1:
            self._log("combine-version-bad").error("combineVersion() got version='%s', invalid", version_)
            raise InstallException("combineVersion(): bad version '%s'" % version_)
        if build.find("-") != -1:
            self._log("combine-version-bad").error("combineVersion() got build='%s', invalid", build)
            raise InstallException("combineVersion(): bad build '%s'" % build)
        return version_+"-"+build

    def splitVersion (self, versionCombined, raiseIfInvalid=False):
        """
        Splits versionCombined into a version and a build. versionCombined must contain exactly 1 '-' char. 
        Returns (version, build), or (None, None) if versionCombined is invalid
        if raiseIfInvalid is True, then instead of returning (None, None), raises InstallException
        """
        if versionCombined.count("-") != 1:
            self._log("version-split-bad").error("splitVersion() got '%s', cannot split", versionCombined)
            if raiseIfInvalid:
                raise InstallException("splitVersion(): bad versionCombined '%s'" % versionCombined)
            return (None, None)
        (version_,build) = versionCombined.split("-")
        return (version_,build)

    def _createPilotServices (self, pilotVersion, pilotDir, newVersionCombined): 
        self._log("create-pilot-services").info("_createPilotServices(): Called, pilotVersion = %s, newVersionCombined = %s", 
                                                pilotVersion, newVersionCombined)
        rpmActiveRfs=self._createRpmForActiveRfs()
        yumActiveRfs=self._createYumWithAllRepos()
        yumNewVersionOnly=self._createYumWithAllRepos(repoFilter=newVersionCombined)

        self._log("create-pilot-services").info("_createPilotServices(): creating yum.conf for active RFS and yum.conf "\
                                                "for new version %s", newVersionCombined)

        yumConfActiveRfs=yumActiveRfs.createYumConfAt(self._yumConfActiveRfsDir)
        yumConfNewVersionOnly=yumNewVersionOnly.createYumConfAt(self._yumConfNewVersionDir)

        self._log("create-pilot-services").info("_createPilotServices(): got yum.conf at %s, yum.conf for new versiom only at %s", 
                                                yumConfActiveRfs, yumConfNewVersionOnly)
        pilotServices=PilotServices(self._log, self._stateManager, self, pilotVersion, pilotDir, self._logDir, self._workDir, self._apiDir,
                                    self._startupScriptsDir, rpmActiveRfs, yumActiveRfs, yumConfActiveRfs, yumConfNewVersionOnly,
                                    self._prevRfsDir, self._sdUtils, self._bootUtils)
        pilotServices.setPilotHints(self._pilotHints)
        return pilotServices 

    def _createRpmForActiveRfs (self):
        rpm=Rpm(self._log)
        rpm.setRoot(self._systemRoot, useFakeChrootMode=self._useFakeChrootMode)
        return rpm

    def _createYumWithAllRepos (self, repoFilter=None):
        """
        Returns a yum object configured to see all repos

        if repoFilter != None, yum is configured to see only repos which contain the subs-string repoFilter:
        """

        self._log("create-yum-with-all-repos").info("createYumWithAllRepos() called, repoFilter=%s", repoFilter)
        if self._useFakeChrootMode:
            yum=Yum(self._log, self._tempDir.replace(self._systemRoot, ""))
            yum.setRoot(self._systemRoot, useFakeChrootMode=True)
        else:
            yum=Yum(self._log, os.path.join(self._tempDir, "yum"))
        self._subPkgManager.addReposToYum(yum, repoFilter)
        return yum

    def _chopLeadingSlashIfFound (self, path_):
        if path_.startswith('/'):
            return path_[1:]
        return path_

    def _selectLatestVersion(self, version1, version2):
        # Split versions to version name and build number
        self._log("select-latest-version").info("_selectLatestVersion() called, version1=%s, version2=%s", version1, version2)

        # Versions must look like this: "v-b"

        vb1=version1.split('-')
        if len(vb1) != 2:
            raise InstallException("Invalid version number: %s is invalid" % version1)
        vb2=version2.split('-')
        if len(vb2) != 2:
            raise InstallException("Invalid version number: %s is invalid" % version2)

        ver1=vb1[0]
        b1=vb1[1]
        ver2=vb2[0]
        b2=vb2[1]

        # A version is official if it is made from numbers and dots only
        onlyDigitsAndDots="^[\d\.]+$"
        ver1IsOfficial=bool(re.match(onlyDigitsAndDots, version1))
        ver2IsOfficial=bool(re.match(onlyDigitsAndDots, version2))

        # Official versions beat private versions
        if ver1IsOfficial != ver2IsOfficial:
            if ver1IsOfficial:
                return version1
            return version2

        # Now we have either two official versions, or two private versions.
        if ver1 != ver2:
            # Versions are not identical. Split version into elements
            v1=ver1.split(".")
            v2=ver2.split(".")
            len1=len(v1)
            len2=len(v2)
    
            # Numerically compare the numbers, as long as both versions have them
            minLen=min(len1, len2)
            for i in range(minLen):
                # Both version elements are the same ? no need to compare
                if v1[i] == v2[i]:
                    continue
    
                # Need to compare version elements. Convert to integers.
                try:
                    ve1=int(v1[i])
                    ve2=int(v2[i])
                except:
                    # Not an integer ? Cannot compare. Choose first version arbitrarily
                    return version1
    
                # Different numbers ? We have a winner !
                if ve1<ve2:
                    return version2
                if ve1>ve2:
                    return version1
                # Note that we may get here despite the 'if' that prevented comparing the same strings.
                # For example, 04 and 4 are the same.
    
            # Out of the loop with no decision ? Let's see if the version lengths want to talk
            # 
            # Longer length wins: 2.1.0.0 is 'later' than 2.1.0

            if len1<len2:
                return version2
            if len1>len2:
                return version1

            # If we are here, then versions are not identical, but all their components are the same
            # AND they are the same length ? This could happen with versions like 2.04 vs 2.4.
            # Choose arbitrarily
            return version1

        # Versions are the same. We need to decide based on build numbers
         
        # Make sure build number is an integer
        try:
            b1n=int(b1)
            b2n=int(b2)
        except:
            raise InstallException("Invalid build number: Both '%s' and '%s' should be numeric" % (b1, b2))

        # Decide according to the build number
        if b1n<b2n:
            return version2
        return version1

    def _raiseIfVersionActiveOrReady(self, versionCombined):
        # Do not allow adding an existing version
        curState=self._stateManager.getVersionState(versionCombined)
        if curState == StateManager.kStateActive:
            self._log("add-existing-version").error("_raiseIfVersionActiveOrReady() Found existing active version '%s'", versionCombined)
            raise InstallException("Version %s is the currently active version" % versionCombined)
        if curState == StateManager.kStateReady:
            self._log("add-existing-version").error("_raiseIfVersionActiveOrReady() Found existing ready version '%s'", versionCombined)
            raise InstallException("Version %s is already ready" % versionCombined)

    def _raiseIfAbsPath (self, pathToCheck, name):
        if pathToCheck.startswith(os.path.sep):
            raise ValueError("Parameter '%s' must not start with '/', value given is '%s'" % (name, pathToCheck))

    def _raiseIfNotAbsPath (self, pathToCheck, name):
        if not pathToCheck.startswith(os.path.sep):
            raise ValueError("Parameter '%s' must start with '/', value given is '%s'" % (name, pathToCheck))