diff --git a/setup/unattended-install/.htaccess b/setup/unattended-install/.htaccess new file mode 100644 index 000000000..782472c78 --- /dev/null +++ b/setup/unattended-install/.htaccess @@ -0,0 +1,13 @@ +# Apache 2.4 + +Require all denied + + +# Apache 2.2 + +deny from all +Satisfy All + + +# Apache 2.2 and 2.4 +IndexIgnore * diff --git a/setup/unattended-install/README.md b/setup/unattended-install/README.md new file mode 100644 index 000000000..d4ca4249b --- /dev/null +++ b/setup/unattended-install/README.md @@ -0,0 +1,5 @@ +# Unattended-install + +This script allows to install and update iTop via CLI. + +For more information, see the official Wiki : [Automated installation [iTop Documentation]](https://www.itophub.io/wiki/page?id=latest:advancedtopics:automatic_install) diff --git a/setup/unattended-install/unattended-install.php b/setup/unattended-install/unattended-install.php new file mode 100644 index 000000000..09af751ec --- /dev/null +++ b/setup/unattended-install/unattended-install.php @@ -0,0 +1,307 @@ +Get('mode'); + +$sTargetEnvironment = $oParams->Get('target_env', ''); +if ($sTargetEnvironment == '') +{ + $sTargetEnvironment = 'production'; +} + +//unattended run based on db settings coming from response_file (XML file) +$aDBXmlSettings = $oParams->Get('database', array()); +$sDBServer = $aDBXmlSettings['server']; +$sDBUser = $aDBXmlSettings['user']; +$sDBPwd = $aDBXmlSettings['pwd']; +$sDBName = $aDBXmlSettings['name']; +$sDBPrefix = $aDBXmlSettings['prefix']; + +if ($sMode == 'install') +{ + echo "Installation mode detected.\n"; + + $bClean = utils::ReadParam('clean', false, true /* CLI allowed */); + if ($bClean) + { + echo "Cleanup mode detected.\n"; + + // Configuration file + $sConfigFile = APPCONF.$sTargetEnvironment.'/'.ITOP_CONFIG_FILE; + if (file_exists($sConfigFile)) + { + echo "Trying to delete the configuration file: '$sConfigFile'.\n"; + @chmod($sConfigFile, 0770); // RWX for owner and group, nothing for others + unlink($sConfigFile); + } + else + { + echo "No config file to delete ($sConfigFile does not exist).\n"; + } + + // Starting with iTop 2.7.0, a failed setup leaves some lock files, let's remove them + $aLockFiles = array( + 'data/.readonly' => 'read-only lock file', + 'data/.maintenance' => 'maintenance mode lock file', + ); + foreach($aLockFiles as $sFile => $sDescription) + { + $sLockFile = APPROOT.$sFile; + if (file_exists($sLockFile)) + { + echo "Trying to delete the $sDescription: '$sLockFile'.\n"; + unlink($sLockFile); + } + } + + // Starting with iTop 2.6.0, let's remove the cache directory as well + // Can cause some strange issues in the setup (apparently due to the Dict class being automatically loaded ??) + $sCacheDir = APPROOT.'data/cache-'.$sTargetEnvironment; + if (file_exists($sCacheDir)) + { + if (is_dir($sCacheDir)) + { + echo "Emptying the cache directory '$sCacheDir'.\n"; + SetupUtils::tidydir($sCacheDir); + } + else + { + die("ERROR the cache directory '$sCacheDir' exists, but is NOT a directory !!!\nExiting.\n"); + } + } + + // env-xxx directory + $sTargetDir = APPROOT.'env-'.$sTargetEnvironment; + if (file_exists($sTargetDir)) + { + if (is_dir($sTargetDir)) + { + echo "Emptying the target directory '$sTargetDir'.\n"; + SetupUtils::tidydir($sTargetDir); + } + else + { + die("ERROR the target dir '$sTargetDir' exists, but is NOT a directory !!!\nExiting.\n"); + } + } + else + { + echo "No target directory to delete ($sTargetDir does not exist).\n"; + } + + if ($sDBPrefix != '') + { + die("Cleanup not implemented for a partial database (prefix= '$sDBPrefix')\nExiting."); + } + + $oMysqli = new mysqli($sDBServer, $sDBUser, $sDBPwd); + if ($oMysqli->connect_errno) + { + die("Cannot connect to the MySQL server (".$oMysqli->connect_errno . ") ".$oMysqli->connect_error."\nExiting"); + } + else + { + if ($oMysqli->select_db($sDBName)) + { + echo "Deleting database '$sDBName'\n"; + $oMysqli->query("DROP DATABASE `$sDBName`"); + } + else + { + echo "The database '$sDBName' does not seem to exist. Nothing to cleanup.\n"; + } + } + } +} +else +{ + //use settings from itop conf + $sTargetEnvironment = $oParams->Get('target_env', ''); + if ($sTargetEnvironment == '') + { + $sTargetEnvironment = 'production'; + } + $sTargetDir = APPROOT.'env-'.$sTargetEnvironment; +} + +$bHasErrors = false; +$aChecks = SetupUtils::CheckBackupPrerequisites(APPROOT.'data'); // mmm should be the backup destination dir + +$aSelectedModules = $oParams->Get('selected_modules'); +$sSourceDir = $oParams->Get('source_dir', 'datamodels/latest'); +$sExtensionDir = $oParams->Get('extensions_dir', 'extensions'); +$aChecks = array_merge($aChecks, SetupUtils::CheckSelectedModules($sSourceDir, $sExtensionDir, $aSelectedModules)); + +foreach($aChecks as $oCheckResult) +{ + switch ($oCheckResult->iSeverity) + { + case CheckResult::ERROR: + $bHasErrors = true; + $sHeader = "Error"; + break; + + case CheckResult::WARNING: + $sHeader = "Warning"; + break; + + case 3: // CheckResult::TRACE added in iTop 3.0.0 + // does nothing : those are old debug traces, see N°2214 + $sHeader = 'Trace'; + break; + + case CheckResult::INFO: + default: + $sHeader = "Info"; + break; + } + echo $sHeader.": ".$oCheckResult->sLabel; + if (strlen($oCheckResult->sDescription)) + { + echo ' - '.$oCheckResult->sDescription; + } + echo "\n"; +} + +if ($bHasErrors) +{ + echo "Encountered stopper issues. Aborting...\n"; + $sLogMsg = "Encountered stopper issues. Aborting..."; + echo "$sLogMsg\n"; + SetupLog::Error($sLogMsg); + die; +} + +$bFoundIssues = false; + +$bInstall = utils::ReadParam('install', true, true /* CLI allowed */); +if ($bInstall) +{ + echo "Starting the unattended installation...\n"; + $oWizard = new ApplicationInstaller($oParams); + $bRes = $oWizard->ExecuteAllSteps(); + if (!$bRes) + { + echo "\nencountered installation issues!"; + $bFoundIssues = true; + } + else + { + $oMysqli = new mysqli($sDBServer, $sDBUser, $sDBPwd); + if (!$oMysqli->connect_errno) + { + if ($oMysqli->select_db($sDBName)) + { + // Check the presence of a table to record information about the MTP (from the Designer) + $sDesignerUpdatesTable = $sDBPrefix.'priv_designer_update'; + $sSQL = "SELECT id FROM `$sDesignerUpdatesTable`"; + if ($oMysqli->query($sSQL) !== false) + { + // Record the Designer Udpates in the priv_designer_update table + $sDeltaFile = APPROOT.'data/'.$sTargetEnvironment.'.delta.xml'; + if (is_readable($sDeltaFile)) + { + // Retrieve the revision + $oDoc = new DOMDocument(); + $oDoc->load($sDeltaFile); + $iRevision = 0; + $iRevision = $oDoc->firstChild->getAttribute('revision_id'); + if ($iRevision > 0) // Safety net, just in case... + { + $sDate = date('Y-m-d H:i:s'); + $sSQL = "INSERT INTO `$sDesignerUpdatesTable` (revision_id, compilation_date, comment) VALUES ($iRevision, '$sDate', 'Deployed using unattended.php.')"; + if ($oMysqli->query($sSQL) !== false) + { + echo "\nDesigner update (MTP at revision $iRevision) successfully recorded.\n"; + } + else + { + echo "\nFailed to record designer updates(".$oMysqli->error.").\n"; + } + } + else + { + echo "\nFailed to read the revision from $sDeltaFile file. No designer update information will be recorded.\n"; + + } + } + else + { + echo "\nNo $sDeltaFile file (or the file is not accessible). No designer update information to record.\n"; + } + } + } + } + } +} +else +{ + echo "No installation requested.\n"; +} +if (!$bFoundIssues && $bCheckConsistency) +{ + echo "Checking data model consistency.\n"; + ob_start(); + $sCheckRes = ''; + try + { + MetaModel::CheckDefinitions(false); + $sCheckRes = ob_get_clean(); + } + catch(Exception $e) + { + $sCheckRes = ob_get_clean()."\nException: ".$e->getMessage(); + } + if (strlen($sCheckRes) > 0) + { + echo $sCheckRes; + echo "\nfound consistency issues!"; + $bFoundIssues = true; + } +} + +if (!$bFoundIssues) +{ + // last line: used to check the install + // the only way to track issues in case of Fatal error or even parsing error! + $sLogMsg = "installed!"; + SetupLog::Info($sLogMsg); + echo "\n$sLogMsg"; + exit(0); +} + +$sLogMsg = "installation failed!"; +SetupLog::Error($sLogMsg); +echo "\n$sLogMsg"; +exit(-1); diff --git a/setup/unattended-install/web.config b/setup/unattended-install/web.config new file mode 100644 index 000000000..58c9c3ac3 --- /dev/null +++ b/setup/unattended-install/web.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php b/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php new file mode 100644 index 000000000..ee4bdd1d3 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php @@ -0,0 +1,71 @@ +Get('app_root_url'); + curl_setopt($ch, CURLOPT_URL, "$sUrl/setup/unattended-install/unattended-install.php"); + curl_setopt($ch, CURLOPT_POST, 1);// set post data to true + curl_setopt($ch, CURLOPT_POSTFIELDS, []); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + // Force disable of certificate check as most of dev / test env have a self-signed certificate + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + + $sJson = curl_exec($ch); + curl_close ($ch); + return $sJson; + } + public function testCallUnattendedInstallFromHttp(){ + $sJson = $this->callUnattendedFromHttp(); + if (false !== strpos($sJson, "403 Forbidden")){ + //.htaccess / webconfig effect + $aFiles = [ + 'web.config', + '.htaccess', + ]; + foreach ($aFiles as $sFile){ + $sPath = APPROOT."setup/unattended-install/$sFile"; + if (is_file("$sPath")) { + rename($sPath, "$sPath.back"); + } + } + + $sJson = $this->callUnattendedFromHttp(); + } + + $this->assertEquals("Mode CLI only", $sJson, "even without HTTP protection, script should NOT be called directly by HTTP"); + } + + public function testCallUnattendedInstallFromCLI() { + $cliPath = realpath(APPROOT."/setup/unattended-install/unattended-install.php"); + $res = exec("php ".$cliPath); + + $this->assertEquals("Param file `default-params.xml` doesn't exist ! Exiting...", $res); + } +}