Setup sequencer side A

This commit is contained in:
Eric Espie
2026-04-01 17:02:17 +02:00
parent fb8b4a07b3
commit 45e0cee1ee
7 changed files with 359 additions and 411 deletions

View File

@@ -10,11 +10,11 @@ class SetupAudit extends AbstractSetupAudit
private string $sEnvBefore;
private string $sEnvAfter;
public function __construct(string $sEnvBefore)
public function __construct(string $sEnvBefore, ?string $sEnvAfter = null)
{
parent::__construct();
$this->sEnvBefore = $sEnvBefore;
$this->sEnvAfter = "$sEnvBefore-build";
$this->sEnvAfter = $sEnvAfter ?? "$sEnvBefore-build";
}
public function ComputeClasses(): void

View File

@@ -25,6 +25,7 @@
*/
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
@@ -308,7 +309,7 @@ class RunTimeEnvironment
$sModule = $oModule->GetName();
$bIsExtra = $this->GetExtensionMap()->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE);
if (array_key_exists($sModule, $aAvailableModules)) {
if (($aAvailableModules[$sModule]['installed_version'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect'
if (($aAvailableModules[$sModule]['installed_version'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect'
$aRet[$oModule->GetName()] = $oModule;
}
}
@@ -327,9 +328,10 @@ class RunTimeEnvironment
$bSelected = $oPhpExpressionEvaluator->ParseAndEvaluateBooleanExpression($oModule->GetAutoSelect());
if ($bSelected) {
$aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module
$bModuleAdded = true;
$bModuleAdded = true;
}
} catch (ModuleFileReaderException $e) {
}
catch (ModuleFileReaderException $e) {
//do nothing. logged already
}
}
@@ -345,56 +347,6 @@ class RunTimeEnvironment
return $aRet;
}
/**
* Compile the data model by imitating the given environment
* The list of modules to be installed in the build environment is:
* - the list of modules present in the "source_dir" (defined by the source environment) which are marked as "installed" in the source environment's database
* - plus the list of modules present in the "extra" directory of the build environment: data/<build_environment>-modules/
*
* @param string $sSourceEnv The name of the source environment to 'imitate'
* @param bool $bUseSymLinks Whether to create symbolic links instead of copies
*
* @return string[]
*/
public function CompileFrom($sSourceEnv, $bUseSymLinks = null)
{
$oSourceConfig = new Config(utils::GetConfigFilePath($sSourceEnv));
$sSourceDir = $oSourceConfig->Get('source_dir');
$sSourceDirFull = APPROOT.$sSourceDir;
// Do load the required modules
//
$oFactory = new ModelFactory($sSourceDirFull);
$aModulesToCompile = $this->GetMFModulesToCompile($sSourceEnv, $sSourceDir);
$oModule = null;
foreach ($aModulesToCompile as $oModule) {
if ($oModule instanceof MFDeltaModule) {
// Just before loading the delta, let's save an image of the datamodel
// in case there is no delta the operation will be done after the end of the loop
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'.xml');
}
$oFactory->LoadModule($oModule);
}
if (!is_null($oModule) && ($oModule instanceof MFDeltaModule)) {
// A delta was loaded, let's save a second copy of the datamodel
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'-with-delta.xml');
} else {
// No delta was loaded, let's save the datamodel now
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'.xml');
}
$sBuildDir = APPROOT.'env-'.$this->sBuildEnv;
self::MakeDirSafe($sBuildDir);
$bSkipTempDir = ($this->sFinalEnv != $this->sBuildEnv); // No need for a temporary directory if sBuildEnv is already a temporary directory
$oMFCompiler = new MFCompiler($oFactory, $this->sFinalEnv);
$oMFCompiler->Compile($sBuildDir, null, $bUseSymLinks, $bSkipTempDir);
MetaModel::ResetAllCaches($this->sBuildEnv);
return array_keys($aModulesToCompile);
}
/**
* Helper function to create the database structure
*
@@ -509,7 +461,7 @@ class RunTimeEnvironment
}
}
foreach ($aPredefinedObjects as $iRefId => $aObjValues) {
if (! array_key_exists($iRefId, $aDBIds)) {
if (!array_key_exists($iRefId, $aDBIds)) {
$oNewObj = MetaModel::NewObject($sClass);
$oNewObj->SetKey($iRefId);
foreach ($aObjValues as $sAttCode => $value) {
@@ -650,7 +602,8 @@ class RunTimeEnvironment
{
try {
$aSelectInstall = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig);
} catch (MySQLException $e) {
}
catch (MySQLException $e) {
// No database or erroneous information
$this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name'));
$this->log_error('Exception '.$e->getMessage());
@@ -708,14 +661,17 @@ class RunTimeEnvironment
{
SetupLog::Error($sText);
}
protected function log_warning($sText)
{
SetupLog::Warning($sText);
}
protected function log_info($sText)
{
SetupLog::Info($sText);
}
protected function log_ok($sText)
{
SetupLog::Ok($sText);
@@ -747,10 +703,16 @@ class RunTimeEnvironment
if ($oLatestDM == null) {
return '0.0.0';
}
return $oLatestDM->Get('version');
}
public function Commit()
/**
* Move the build env to the final env if all the steps went ok
*
* @return void
*/
public function Commit(): void
{
if ($this->sFinalEnv != $this->sBuildEnv) {
if (file_exists(utils::GetDataPath().$this->sBuildEnv.'.delta.xml')) {
@@ -808,12 +770,13 @@ class RunTimeEnvironment
/**
* Overwrite or create the destination file
*
* @param $sSource
* @param $sDest
* @param string $sSource
* @param string $sDest
* @param bool $bSourceMustExist
* @throws Exception
*
* @throws \Exception
*/
protected function CommitFile($sSource, $sDest, $bSourceMustExist = true)
protected function CommitFile(string $sSource, string $sDest, bool $bSourceMustExist = true): void
{
if (file_exists($sSource)) {
SetupUtils::builddir(dirname($sDest));
@@ -847,6 +810,7 @@ class RunTimeEnvironment
* @param $sDest
* @param boolean $bSourceMustExist
* @param boolean $bRemoveSource If true $sSource will be removed, otherwise $sSource will just be emptied
*
* @throws Exception
*/
protected function CommitDir($sSource, $sDest, $bSourceMustExist = true, $bRemoveSource = true)
@@ -875,9 +839,11 @@ class RunTimeEnvironment
/**
* Call the given handler method for all selected modules having an installation handler
*
* @param array[] $aAvailableModules
* @param string $sHandlerName
* @param string[]|null $aSelectedModules
* @param string[]|null $aSelectedModules
*
* @throws CoreException
*/
public function CallInstallerHandlers($aAvailableModules, $sHandlerName, $aSelectedModules = null)
@@ -915,15 +881,16 @@ class RunTimeEnvironment
if (is_callable($aCallSpec)) {
try {
call_user_func_array($aCallSpec, $aArgs);
} catch (Exception $e) {
}
catch (Exception $e) {
$sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID] ?? "";
$sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler";
$aExceptionContextData = [
'ModulelId' => $sModuleId,
'ModuleInstallerClass' => $sModuleInstallerClass,
'ModulelId' => $sModuleId,
'ModuleInstallerClass' => $sModuleInstallerClass,
'ModuleInstallerHandler' => $sHandlerName,
'ExceptionClass' => get_class($e),
'ExceptionMessage' => $e->getMessage(),
'ExceptionClass' => get_class($e),
'ExceptionMessage' => $e->getMessage(),
];
throw new CoreException($sErrorMessage, $aExceptionContextData, '', $e);
}
@@ -932,9 +899,10 @@ class RunTimeEnvironment
/**
* Load data from XML files for the selected modules (structural data and/or sample data)
*
* @param array[] $aAvailableModules All available modules and their definition
* @param bool $bSampleData Wether or not to load sample data
* @param null|string[] $aSelectedModules List of selected modules
* @param null|string[] $aSelectedModules List of selected modules
*/
public function LoadData($aAvailableModules, $bSampleData, $aSelectedModules = null)
{
@@ -1011,9 +979,11 @@ class RunTimeEnvironment
/**
* Merge two arrays of file names, adding the relative path to the files provided in the array to merge
*
* @param string[] $aSourceArray
* @param string $sBaseDir
* @param string[] $aFilesToMerge
*
* @return string[]
*/
protected static function MergeWithRelativeDir($aSourceArray, $sBaseDir, $aFilesToMerge)
@@ -1022,14 +992,16 @@ class RunTimeEnvironment
foreach ($aFilesToMerge as $sFile) {
$aToMerge[] = $sBaseDir.'/'.$sFile;
}
return array_merge($aSourceArray, $aToMerge);
}
/**
* Check the MetaModel for some common pitfall (class name too long, classes requiring too many joins...)
* The check takes about 900 ms for 200 classes
* @throws Exception
*
* @return string
* @throws Exception
*/
public function CheckMetaModel()
{
@@ -1043,7 +1015,7 @@ class RunTimeEnvironment
$oSearch = new DBObjectSearch($sClass);
$oSearch->SetShowObsoleteData(false);
$oSQLQuery = $oSearch->GetSQLQueryStructure(null, false);
$oSQLQuery = $oSearch->GetSQLQueryStructure([], false);
$sViewName = MetaModel::DBGetView($sClass);
if (strlen($sViewName) > 64) {
throw new Exception("Class name too long for class: '$sClass'. The name of the corresponding view ($sViewName) would exceed MySQL's limit for the name of a table (64 characters).");
@@ -1062,4 +1034,226 @@ class RunTimeEnvironment
return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration * 1000.0);
}
} // End of class
public function DataToCleanupAudit()
{
$oSetupAudit = new SetupAudit('production', $this->sBuildEnv);
//Make sure the MetaModel is started before analysing for issues
$sConfFile = utils::GetConfigFilePath($this->sBuildEnv);
MetaModel::Startup($sConfFile, false, false, false, $this->sBuildEnv);
$oSetupAudit->GetIssues(true);
$iCount = $oSetupAudit->GetDataToCleanupCount();
if ($iCount > 0) {
throw new Exception("$iCount elements require data adjustments or cleanup in the backoffice prior to upgrading iTop", DataAuditSequencer::DATA_AUDIT_FAILED);
}
}
public function CopySetupFiles(): void
{
$sSourceEnv = 'production';
$sDestinationEnv = $this->sBuildEnv;
if ($sDestinationEnv != $sSourceEnv) {
SetupUtils::CopyFile(utils::GetDataPath().$sSourceEnv.'.delta.xml', utils::GetDataPath().$sDestinationEnv.'.delta.xml');
SetupUtils::copydir(utils::GetDataPath().$sSourceEnv.'-modules/', utils::GetDataPath().$sDestinationEnv.'-modules/');
// Copy the config file
//
$sFinalConfig = APPCONF.$sDestinationEnv.'/config-itop.php';
if (is_file($sFinalConfig)) {
chmod($sFinalConfig, 0770); // In case it exists: RWX for owner and group, nothing for others
}
SetupUtils::copydir(APPCONF.$sSourceEnv, APPCONF.$sDestinationEnv);
if (is_file($sFinalConfig)) {
chmod($sFinalConfig, 0440); // Read-only for owner and group, nothing for others
}
MetaModel::ResetAllCaches($sDestinationEnv);
}
}
/**
* Compile the data model by imitating the given environment
* The list of modules to be installed in the build environment is:
* - the list of modules present in the "source_dir" (defined by the source environment) which are marked as "installed" in the source environment's database
* - plus the list of modules present in the "extra" directory of the build environment: data/<build_environment>-modules/
*
* @param string $sSourceEnv The name of the source environment to 'imitate'
* @param null $bUseSymLinks Whether to create symbolic links instead of copies
*
* @return string[]
* @throws \ConfigException
* @throws \CoreException
*/
public function CompileFrom($sSourceEnv, $bUseSymLinks = null)
{
$oSourceConfig = new Config(utils::GetConfigFilePath($sSourceEnv));
$sSourceDir = $oSourceConfig->Get('source_dir');
$sSourceDirFull = APPROOT.$sSourceDir;
// Do load the required modules
//
$oFactory = new ModelFactory($sSourceDirFull);
$aModulesToCompile = $this->GetMFModulesToCompile($sSourceEnv, $sSourceDir);
$oModule = null;
foreach ($aModulesToCompile as $oModule) {
if ($oModule instanceof MFDeltaModule) {
// Just before loading the delta, let's save an image of the datamodel
// in case there is no delta the operation will be done after the end of the loop
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'.xml');
}
$oFactory->LoadModule($oModule);
}
if (!is_null($oModule) && ($oModule instanceof MFDeltaModule)) {
// A delta was loaded, let's save a second copy of the datamodel
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'-with-delta.xml');
} else {
// No delta was loaded, let's save the datamodel now
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'.xml');
}
$sBuildDir = APPROOT.'env-'.$this->sBuildEnv;
self::MakeDirSafe($sBuildDir);
$bSkipTempDir = ($this->sFinalEnv != $this->sBuildEnv); // No need for a temporary directory if sBuildEnv is already a temporary directory
$oMFCompiler = new MFCompiler($oFactory, $this->sFinalEnv);
$oMFCompiler->Compile($sBuildDir, null, $bUseSymLinks, $bSkipTempDir);
MetaModel::ResetAllCaches($this->sBuildEnv);
return array_keys($aModulesToCompile);
}
/**
* @param array $aRemovedExtensionCodes
* @param array $aSelectedModules
* @param string $sSourceDir
* @param string $sExtensionDir
* @param boolean $bUseSymbolicLinks
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*
*/
public function DoCompile(array $aRemovedExtensionCodes, array $aSelectedModules, string $sSourceDir, string $sExtensionDir, bool $bUseSymbolicLinks = false): void
{
SetupLog::Info('Compiling data model.');
$sEnvironment = $this->sBuildEnv;
$sBuildPath = $this->GetBuildDir();
$sSourcePath = APPROOT.$sSourceDir;
$aDirsToScan = [$sSourcePath];
$sExtensionsPath = APPROOT.$sExtensionDir;
if (is_dir($sExtensionsPath)) {
// if the extensions dir exists, scan it for additional modules as well
$aDirsToScan[] = $sExtensionsPath;
}
$sExtraPath = APPROOT.'/data/'.$sEnvironment.'-modules/';
if (is_dir($sExtraPath)) {
// if the extra dir exists, scan it for additional modules as well
$aDirsToScan[] = $sExtraPath;
}
if (!is_dir($sSourcePath)) {
throw new Exception("Failed to find the source directory '$sSourcePath', please check the rights of the web server");
}
if (!is_dir($sBuildPath)) {
if (!mkdir($sBuildPath)) {
throw new Exception("Failed to create directory '$sBuildPath', please check the rights of the web server");
} else {
// adjust the rights if and only if the directory was just created
// owner:rwx user/group:rx
chmod($sBuildPath, 0755);
}
} elseif ($this->IsInItop($sBuildPath)) {
// If the directory is under the root folder - as expected - let's clean-it before compiling
SetupUtils::tidydir($sBuildPath);
}
$oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan);
// Removed modules are stored as static for FindModules()
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes);
$oFactory = new ModelFactory($aDirsToScan);
$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
$oFactory->LoadModule($oDictModule);
$sDeltaFile = APPROOT.'core/datamodel.core.xml';
if (file_exists($sDeltaFile)) {
$oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile);
$oFactory->LoadModule($oCoreModule);
}
$sDeltaFile = APPROOT.'application/datamodel.application.xml';
if (file_exists($sDeltaFile)) {
$oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile);
$oFactory->LoadModule($oApplicationModule);
}
$aModules = $oFactory->FindModules();
foreach ($aModules as $oModule) {
$sModule = $oModule->GetName();
if (in_array($sModule, $aSelectedModules)) {
$oFactory->LoadModule($oModule);
}
}
// Dump the "reference" model, just before loading any actual delta
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'.xml');
$sDeltaFile = utils::GetDataPath().$sEnvironment.'.delta.xml';
if (file_exists($sDeltaFile)) {
$oDelta = new MFDeltaModule($sDeltaFile);
$oFactory->LoadModule($oDelta);
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'-with-delta.xml');
}
$oMFCompiler = new MFCompiler($oFactory, $sEnvironment);
$oMFCompiler->Compile($sBuildPath, null, $bUseSymbolicLinks, false, false);
SetupLog::Info("Data model successfully compiled to '$sBuildPath'.");
$sCacheDir = APPROOT.'/data/cache-'.$sEnvironment.'/';
SetupUtils::builddir($sCacheDir);
SetupUtils::tidydir($sCacheDir);
// Set an "Instance UUID" identifying this machine based on a file located in the data directory
$sInstanceUUIDFile = utils::GetDataPath().'instance.txt';
SetupUtils::builddir(utils::GetDataPath());
if (!file_exists($sInstanceUUIDFile)) {
$sInstanceUUID = utils::CreateUUID('filesystem');
file_put_contents($sInstanceUUIDFile, $sInstanceUUID);
}
}
protected function IsInItop(string $sPath): bool
{
$sFileRealPath = realpath($sPath);
if ($sFileRealPath === false) {
return false;
}
$sRealBasePath = realpath(APPROOT); // avoid problems when having '/' on Windows for example
if (!self::StartsWith($sFileRealPath, $sRealBasePath)) {
return false;
}
return true;
}
protected static function StartsWith(string $sHaystack, string $sNeedle): bool
{
if (strlen($sNeedle) > strlen($sHaystack)) {
return false;
}
return substr_compare($sHaystack, $sNeedle, 0, strlen($sNeedle)) === 0;
}
}

View File

@@ -45,14 +45,22 @@ class ApplicationInstallSequencer extends StepSequencer
protected Config $oConfig;
protected RunTimeEnvironment $oRunTimeEnvironment;
/**
* @param \Parameters $oParams
*
* @throws \ConfigException
* @throws \CoreException
*/
public function __construct(Parameters $oParams)
public function __construct(Parameters $oParams, ?RunTimeEnvironment $oRunTimeEnvironment = null)
{
if (is_null($oRunTimeEnvironment)) {
$sEnvironment = $oParams->Get('target_env', 'production');
$oRunTimeEnvironment = new RunTimeEnvironment($sEnvironment, false);
}
$this->oRunTimeEnvironment = $oRunTimeEnvironment;
$this->oParams = $oParams;
$aParamValues = $oParams->GetParamForConfigArray();
@@ -99,26 +107,7 @@ class ApplicationInstallSequencer extends StepSequencer
return $oConfig;
}
protected function DoLogParameters($sPrefix = 'install-', $sOperation = 'Installation')
{
// Log the parameters...
$oDoc = new DOMDocument('1.0', 'UTF-8');
$oDoc->preserveWhiteSpace = false;
$oDoc->formatOutput = true;
$this->oParams->ToXML($oDoc, null, 'installation');
$sXML = $oDoc->saveXML();
$sSafeXml = preg_replace("|<pwd>([^<]*)</pwd>|", "<pwd>**removed**</pwd>", $sXML);
SetupLog::Info("======= ".$sOperation." starts =======\nParameters:\n$sSafeXml\n");
// Save the response file as a stand-alone file as well
$sFileName = $sPrefix.date('Y-m-d');
$index = 0;
while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) {
$index++;
$sFileName = $sPrefix.date('Y-m-d').'-'.$index;
}
file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml);
}
/**
* Executes the next step of the installation and reports about the progress
@@ -137,6 +126,14 @@ class ApplicationInstallSequencer extends StepSequencer
$this->EnterReadOnlyMode();
switch ($sStep) {
case '':
if (in_array('log-parameters', $this->oParams->Get('optional_steps', []))) {
return $this->GetNextStep('log-parameters', 'Log parameters', 0);
}
return $this->GetNextStep('copy', 'Copying data model files', 5);
case 'log-parameters':
$this->DoLogParameters('data-audit-', 'Data Audit');
return $this->GetNextStep('copy', 'Copying data model files', 5);
$this->DoLogParameters();
@@ -374,26 +371,6 @@ class ApplicationInstallSequencer extends StepSequencer
SetupUtils::ExitReadOnlyMode();
}
protected function DoCopy($aCopies)
{
$aReports = [];
foreach ($aCopies as $aCopy) {
$sSource = $aCopy['source'];
$sDestination = APPROOT.$aCopy['destination'];
SetupUtils::builddir($sDestination);
SetupUtils::tidydir($sDestination);
SetupUtils::copydir($sSource, $sDestination);
$aReports[] = "'{$aCopy['source']}' to '{$aCopy['destination']}' (OK)";
}
if (count($aReports) > 0) {
$sReport = "Copies: ".count($aReports).': '.implode('; ', $aReports);
} else {
$sReport = "No file copy";
}
return $sReport;
}
/**
* @param string $sBackupFileFormat
* @param string $sSourceConfigFile
@@ -416,160 +393,6 @@ class ApplicationInstallSequencer extends StepSequencer
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
}
/**
* @param array $aRemovedExtensionCodes
* @param array $aSelectedModules
* @param string $sSourceDir
* @param string $sExtensionDir
* @param boolean $bUseSymbolicLinks
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*
* @since 3.1.0 N°2013 added the aParamValues param
*/
protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null, $bEnterMaintenanceMode = true)
{
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
SetupLog::Info("Compiling data model.");
require_once(APPROOT.'setup/modulediscovery.class.inc.php');
require_once(APPROOT.'setup/modelfactory.class.inc.php');
require_once(APPROOT.'setup/compiler.class.inc.php');
$aParamValues = $this->oParams->GetParamForConfigArray();
$sEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
if (empty($sSourceDir) || empty($sTargetDir)) {
throw new Exception("missing parameter source_dir and/or target_dir");
}
$sSourcePath = APPROOT.$sSourceDir;
$aDirsToScan = [$sSourcePath];
$sExtensionsPath = APPROOT.$sExtensionDir;
if (is_dir($sExtensionsPath)) {
// if the extensions dir exists, scan it for additional modules as well
$aDirsToScan[] = $sExtensionsPath;
}
$sExtraPath = APPROOT.'/data/'.$sEnvironment.'-modules/';
if (is_dir($sExtraPath)) {
// if the extra dir exists, scan it for additional modules as well
$aDirsToScan[] = $sExtraPath;
}
$sTargetPath = APPROOT.$sTargetDir;
if (!is_dir($sSourcePath)) {
throw new Exception("Failed to find the source directory '$sSourcePath', please check the rights of the web server");
}
$bIsAlreadyInMaintenanceMode = SetupUtils::IsInMaintenanceMode();
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
$sConfigFilePath = utils::GetConfigFilePath($sEnvironment);
if (is_file($sConfigFilePath)) {
$oConfig = new Config($sConfigFilePath);
$oConfig->UpdateFromParams($aParamValues);
if ($bEnterMaintenanceMode) {
SetupUtils::EnterMaintenanceMode($oConfig);
}
}
}
try {
if (!is_dir($sTargetPath)) {
if (!mkdir($sTargetPath)) {
throw new Exception("Failed to create directory '$sTargetPath', please check the rights of the web server");
} else {
// adjust the rights if and only if the directory was just created
// owner:rwx user/group:rx
chmod($sTargetPath, 0755);
}
} elseif (substr($sTargetPath, 0, strlen(APPROOT)) == APPROOT) {
// If the directory is under the root folder - as expected - let's clean-it before compiling
SetupUtils::tidydir($sTargetPath);
}
$oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan);
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes);
$oFactory = new ModelFactory($aDirsToScan);
$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
$oFactory->LoadModule($oDictModule);
$sDeltaFile = APPROOT.'core/datamodel.core.xml';
if (file_exists($sDeltaFile)) {
$oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile);
$oFactory->LoadModule($oCoreModule);
}
$sDeltaFile = APPROOT.'application/datamodel.application.xml';
if (file_exists($sDeltaFile)) {
$oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile);
$oFactory->LoadModule($oApplicationModule);
}
$aModules = $oFactory->FindModules();
foreach ($aModules as $oModule) {
$sModule = $oModule->GetName();
if (in_array($sModule, $aSelectedModules)) {
$oFactory->LoadModule($oModule);
}
}
// Dump the "reference" model, just before loading any actual delta
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'.xml');
$sDeltaFile = utils::GetDataPath().$sEnvironment.'.delta.xml';
if (file_exists($sDeltaFile)) {
$oDelta = new MFDeltaModule($sDeltaFile);
$oFactory->LoadModule($oDelta);
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'-with-delta.xml');
}
$oMFCompiler = new MFCompiler($oFactory, $sEnvironment);
$oMFCompiler->Compile($sTargetPath, null, $bUseSymbolicLinks, false, $bEnterMaintenanceMode);
//$aCompilerLog = $oMFCompiler->GetLog();
//SetupLog::Info(implode("\n", $aCompilerLog));
SetupLog::Info("Data model successfully compiled to '$sTargetPath'.");
$sCacheDir = APPROOT.'/data/cache-'.$sEnvironment.'/';
SetupUtils::builddir($sCacheDir);
SetupUtils::tidydir($sCacheDir);
} catch (Exception $e) {
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
SetupUtils::ExitMaintenanceMode();
}
throw $e;
}
// Special case to patch a ugly patch in itop-config-mgmt
$sFileToPatch = $sTargetPath.'/itop-config-mgmt-1.0.0/model.itop-config-mgmt.php';
if (file_exists($sFileToPatch)) {
$sContent = file_get_contents($sFileToPatch);
$sContent = str_replace("require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", "//\n// The line below is no longer needed in iTop 2.0 -- patched by the setup program\n// require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", $sContent);
file_put_contents($sFileToPatch, $sContent);
}
// Set an "Instance UUID" identifying this machine based on a file located in the data directory
$sInstanceUUIDFile = utils::GetDataPath().'instance.txt';
SetupUtils::builddir(utils::GetDataPath());
if (!file_exists($sInstanceUUIDFile)) {
$sIntanceUUID = utils::CreateUUID('filesystem');
file_put_contents($sInstanceUUIDFile, $sIntanceUUID);
}
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode && $bEnterMaintenanceMode) {
SetupUtils::ExitMaintenanceMode();
}
}
protected function GetModelInfoPath(string $sEnv): string
{
return APPROOT."data/beforecompilation_".$sEnv."_modelinfo.json";

View File

@@ -25,9 +25,6 @@ require_once APPROOT.'setup/feature_removal/SetupAudit.php';
require_once(APPROOT.'setup/sequencers/StepSequencer.php');
require_once(APPROOT.'setup/sequencers/ApplicationInstallSequencer.php');
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
class DataAuditSequencer extends ApplicationInstallSequencer
{
public const DATA_AUDIT_FAILED = 100;
@@ -35,12 +32,14 @@ class DataAuditSequencer extends ApplicationInstallSequencer
protected function GetTempEnv()
{
$sTargetEnv = $this->GetTargetEnv();
return $sTargetEnv.'-build';
}
protected function GetTargetDir()
{
$sTargetEnv = $this->GetTempEnv();
return 'env-'.$sTargetEnv;
}
@@ -56,102 +55,56 @@ class DataAuditSequencer extends ApplicationInstallSequencer
public function ExecuteStep($sStep = '', $sInstallComment = null)
{
try {
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
$fStart = microtime(true);
SetupLog::Info("##### STEP {$sStep} start");
switch ($sStep) {
case '':
$this->DoLogParameters('data-audit-', 'Data Audit');
return $this->GetNextStep('copy', 'Copying data model files', 5);
$aResult = [
'status' => self::OK,
'message' => '',
'percentage-completed' => 20,
'next-step' => 'compile',
'next-step-label' => 'Compiling the data model',
];
break;
case 'copy':
$this->oRunTimeEnvironment->CopySetupFiles();
return $this->GetNextStep('compile', 'Compiling the data model', 20, 'Copying...');
case 'compile':
$aSelectedModules = $this->oParams->Get('selected_modules');
$sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
$sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
$aMiscOptions = $this->oParams->Get('options', []);
$aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
$this->DoCompile(
$bUseSymbolicLinks = $aMiscOptions['symlinks'] ?? false;
$sMessage = $bUseSymbolicLinks ? '' : 'Using symbolic links instead of copying data model files (for developers only!)';
$this->oRunTimeEnvironment->DoCompile(
$aRemovedExtensionCodes,
$aSelectedModules,
$sSourceDir,
$sExtensionDir,
false,
false
$bUseSymbolicLinks
);
return $this->GetNextStep('setup-audit', 'Checking data consistency with the new data model', 70, $sMessage);
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'write-config',
'next-step-label' => 'Writing audit config',
'percentage-completed' => 40,
];
break;
case 'write-config':
$this->DoWriteConfig();
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'setup-audit',
'next-step-label' => 'Checking data consistency with the new data model',
'percentage-completed' => 60,
];
break;
case 'setup-audit':
$this->DoSetupAudit();
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'cleanup',
'next-step-label' => 'Temporary folders cleanup',
'percentage-completed' => 80,
];
break;
case 'cleanup' :
$this->DoCleanup();
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => '',
'next-step-label' => 'Completed',
'percentage-completed' => 100,
];
break;
default:
$aResult = [
'status' => self::ERROR,
'message' => '',
'next-step' => '',
'next-step-label' => "Unknown setup step '$sStep'.",
'percentage-completed' => 100,
];
break;
}
} catch (Exception $e) {
$aResult = [
'status' => self::ERROR,
'message' => $e->getMessage(),
'next-step' => '',
'next-step-label' => '',
'percentage-completed' => 100,
'error_code' => $e->getCode(),
];
$this->oRunTimeEnvironment->DataToCleanupAudit();
return $this->GetNextStep('', 'Completed', 100);
default:
return $this->GetNextStep('', "Unknown setup step '$sStep'.", 100, '', self::ERROR);
}
}
catch (Exception $e) {
$this->ReportException($e);
} finally {
$aResult = $this->GetNextStep('', '', 100, $e->getMessage(), self::ERROR);
$aResult['error_code'] = $e->getCode();
return $aResult;
}
finally {
$fDuration = round(microtime(true) - $fStart, 2);
SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s");
}
return $aResult;
}
protected function DoWriteConfig()
@@ -167,57 +120,7 @@ class DataAuditSequencer extends ApplicationInstallSequencer
return $oConfig->WriteToFile($sTempConfigFileName);
}
return false;
}
protected function DoSetupAudit()
{
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
$sPreviousEnvironment = $this->GetTargetEnv();
$oSetupAudit = new SetupAudit($sPreviousEnvironment);
//Make sure the MetaModel is started before analysing for issues
$sConfFile = utils::GetConfigFilePath($sPreviousEnvironment);
MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sPreviousEnvironment);
$oSetupAudit->GetIssues(true);
$iCount = $oSetupAudit->GetDataToCleanupCount();
if ($iCount > 0) {
throw new Exception("$iCount elements require data adjustments or cleanup in the backoffice prior to upgrading iTop", static::DATA_AUDIT_FAILED);
}
}
protected function DoCleanup()
{
$sEnv = $this->GetTempEnv();
//keep this folder empty
SetupUtils::tidydir(APPROOT."/env-$sEnv");
$aFolders = [
APPROOT."/data/$sEnv-modules",
APPROOT."/data/cache-$sEnv",
APPROOT."/conf/$sEnv",
];
foreach ($aFolders as $sFolder) {
SetupUtils::tidydir($sFolder);
SetupUtils::rmdir_safe($sFolder);
}
$sFiles = [
APPROOT."/data/datamodel-$sEnv.xml",
APPROOT."/data/$sEnv.delta.prev.xml",
];
foreach ($sFiles as $sFile) {
if (is_file($sFile)) {
@unlink($sFile);
}
}
}
}

View File

@@ -102,5 +102,37 @@ abstract class StepSequencer
return ($iOverallStatus == self::OK);
}
protected function GetNextStep(string $sNextStep, string $sNextStepLabel, int $iPercentComplete, string $sMessage = '', int $iStatus = self::OK): array
{
return [
'status' => $iStatus,
'message' => $sMessage,
'next-step' => $sNextStep,
'next-step-label' => $sNextStepLabel,
'percentage-completed' => $iPercentComplete,
];
}
protected function DoLogParameters($sPrefix = 'install-', $sOperation = 'Installation')
{
// Log the parameters...
$oDoc = new DOMDocument('1.0', 'UTF-8');
$oDoc->preserveWhiteSpace = false;
$oDoc->formatOutput = true;
$this->oParams->ToXML($oDoc, null, 'installation');
$sXML = $oDoc->saveXML();
$sSafeXml = preg_replace('|<pwd>([^<]*)</pwd>|', '<pwd>**removed**</pwd>', $sXML);
SetupLog::Info('======= '.$sOperation." starts =======\nParameters:\n$sSafeXml\n");
// Save the response file as a stand-alone file as well
$sFileName = $sPrefix.date('Y-m-d');
$index = 0;
while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) {
$index++;
$sFileName = $sPrefix.date('Y-m-d').'-'.$index;
}
file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml);
}
abstract public function ExecuteStep($sStep = '', $sComment = null);
}

View File

@@ -817,6 +817,11 @@ class SetupUtils
}
}
public static function CopyFile(string $sSource, string $sDest, bool $bUseSymbolicLinks = false): bool
{
return self::copydir($sSource, $sDest, $bUseSymbolicLinks);
}
/**
* Helper to copy a directory to a target directory, skipping .SVN files (for developer's comfort!)
* Returns true if successful
@@ -826,11 +831,13 @@ class SetupUtils
* @return bool
* @throws Exception
*/
public static function copydir($sSource, $sDest, $bUseSymbolicLinks = false)
public static function copydir(string $sSource, string $sDest, bool $bUseSymbolicLinks = false): bool
{
if (is_dir($sSource)) {
if (!is_dir($sDest)) {
mkdir($sDest, 0777 /* Default */, true);
} else {
SetupUtils::tidydir($sDest);
}
$aFiles = scandir($sSource);
if (sizeof($aFiles) > 0) {
@@ -839,24 +846,13 @@ class SetupUtils
// Skip
continue;
}
if (is_dir($sSource.'/'.$sFile)) {
// Recurse
self::copydir($sSource.'/'.$sFile, $sDest.'/'.$sFile, $bUseSymbolicLinks);
} else {
if ($bUseSymbolicLinks) {
if (function_exists('symlink')) {
if (file_exists($sDest.'/'.$sFile)) {
unlink($sDest.'/'.$sFile);
}
symlink($sSource.'/'.$sFile, $sDest.'/'.$sFile);
} else {
throw(new Exception("Error, cannot *copy* '$sSource/$sFile' to '$sDest/$sFile' using symbolic links, 'symlink' is not supported on this system."));
}
symlink($sSource.'/'.$sFile, $sDest.'/'.$sFile);
} else {
if (is_link($sDest.'/'.$sFile)) {
unlink($sDest.'/'.$sFile);
}
copy($sSource.'/'.$sFile, $sDest.'/'.$sFile);
}
}
@@ -865,11 +861,7 @@ class SetupUtils
return true;
} elseif (is_file($sSource)) {
if ($bUseSymbolicLinks) {
if (function_exists('symlink')) {
return symlink($sSource, $sDest);
} else {
throw(new Exception("Error, cannot *copy* '$sSource' to '$sDest' using symbolic links, 'symlink' is not supported on this system."));
}
return symlink($sSource, $sDest);
} else {
return copy($sSource, $sDest);
}

View File

@@ -67,6 +67,10 @@ abstract class AbstractWizStepInstall extends WizardStep
'copies' => $aCopies,
// 'backup' => see below
],
'optional_steps' => [
''
// 'backup' => see below
],
'source_dir' => str_replace(APPROOT, '', $sSourceDir),
'datamodel_version' => $this->oWizard->GetParameter('datamodel_version'), //TODO: let the installer compute this automatically...
'previous_configuration_file' => $sPreviousConfigurationFile,