瀏覽代碼

New functionality: data backup / restore. By default, performs a daily backup, keeping the five last backups. Allows manual backups too.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3402 a333f486-631f-4898-b8df-5754b55c2be0
romainq 10 年之前
父節點
當前提交
eb51a4acea

+ 161 - 0
datamodels/2.x/itop-backup/ajax.backup.php

@@ -0,0 +1,161 @@
+<?php
+// Copyright (C) 2014 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Backup from an interactive session
+ *
+ * @copyright   Copyright (C) 2013 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__));
+require_once(__DIR__.'/../../approot.inc.php');
+require_once(APPROOT.'/application/application.inc.php');
+require_once(APPROOT.'/application/webpage.class.inc.php');
+require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
+
+require_once(APPROOT.'core/mutex.class.inc.php');
+
+try
+{
+	$sOperation = utils::ReadParam('operation', '');
+
+	switch($sOperation)
+	{
+		case 'backup':
+		require_once(APPROOT.'/application/startup.inc.php');
+		require_once(APPROOT.'/application/loginwebpage.class.inc.php');
+		LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
+
+		$oPage = new ajax_page("");
+		$oPage->no_cache();
+		$oPage->SetContentType('text/html');
+
+		try
+		{
+			set_time_limit(0);
+			$oBB = new BackupExec(APPROOT.'data/backups/manual/', 0 /*iRetentionCount*/);
+			$sRes = $oBB->Process(time() + 36000); // 10 hours to complete should be sufficient!
+		}
+		catch (Exception $e)
+		{
+			$oPage->p('Error: '.$e->getMessage());
+		}
+		$oPage->output();
+		break;
+
+		case 'restore_get_token':
+		require_once(APPROOT.'/application/startup.inc.php');
+		require_once(APPROOT.'/application/loginwebpage.class.inc.php');
+		LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
+
+		$oPage = new ajax_page("");
+		$oPage->no_cache();
+		$oPage->SetContentType('text/html');
+
+		$sEnvironment = utils::ReadParam('environment', 'production', false, 'raw_data');
+		$oRestoreMutex = new iTopMutex('restore.'.$sEnvironment);
+		if ($oRestoreMutex->TryLock())
+		{
+			$oRestoreMutex->Unlock();
+			$sFile = utils::ReadParam('file', '', false, 'raw_data');
+			$sToken = str_replace(' ', '', (string)microtime());
+			$sTokenFile = APPROOT.'/data/restore.'.$sToken.'.tok';
+			file_put_contents($sTokenFile, $sFile);
+	
+			$oPage->add_ready_script(
+<<<EOF
+	$("#restore_token").val('$sToken');
+EOF
+			);
+		}
+		else
+		{
+			$oPage->p(Dict::S('bkp-restore-running'));
+		}
+		$oPage->output();
+		break;
+
+
+		case 'restore_exec':
+		require_once(APPROOT."setup/runtimeenv.class.inc.php");
+		require_once(APPROOT.'/application/utils.inc.php');
+		require_once(APPROOT.'/setup/backup.class.inc.php');
+		require_once(dirname(__FILE__).'/dbrestore.class.inc.php');
+
+		IssueLog::Enable(APPROOT.'log/error.log');
+
+		$oPage = new ajax_page("");
+		$oPage->no_cache();
+		$oPage->SetContentType('text/html');
+
+		$sEnvironment = utils::ReadParam('environment', 'production', false, 'raw_data');
+		$oRestoreMutex = new iTopMutex('restore.'.$sEnvironment);
+		$oRestoreMutex->Lock();
+		try
+		{
+			set_time_limit(0);
+
+			// Get the file and destroy the token (single usage)
+			$sToken = utils::ReadParam('token', '', false, 'raw_data');
+			$sTokenFile = APPROOT.'/data/restore.'.$sToken.'.tok';
+			$sFile = file_get_contents($sTokenFile);
+			unlink($sTokenFile);
+
+			$sMySQLBinDir = utils::ReadParam('mysql_bindir', '', false, 'raw_data');
+			$sDBHost = utils::ReadParam('db_host', '', false, 'raw_data');
+			$sDBUser = utils::ReadParam('db_user', '', false, 'raw_data');
+			$sDBPwd = utils::ReadParam('db_pwd', '', false, 'raw_data');
+			$sDBName = utils::ReadParam('db_name', '', false, 'raw_data');
+			$sDBSubName = utils::ReadParam('db_subname', '', false, 'raw_data');
+
+			$oDBRS = new DBRestore($sDBHost, $sDBUser, $sDBPwd, $sDBName, $sDBSubName);
+			$oDBRS->SetMySQLBinDir($sMySQLBinDir);
+
+			$sBackupDir = APPROOT.'data/backups/';
+			$sBackupFile = $sBackupDir.$sFile;
+			$sRes = $oDBRS->RestoreFromZip($sBackupFile, $sEnvironment);
+
+			$oRestoreMutex->Unlock();
+		}
+		catch (Exception $e)
+		{
+			$oRestoreMutex->Unlock();
+			$oPage->p('Error: '.$e->getMessage());
+		}
+		$oPage->output();
+		break;
+
+		case 'download':
+		require_once(APPROOT.'/application/startup.inc.php');
+		require_once(APPROOT.'/application/loginwebpage.class.inc.php');
+		LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
+
+		$sFile = utils::ReadParam('file', '', false, 'raw_data');
+		$oBackup = new DBBackupScheduled();
+		$sBackupDir = APPROOT.'data/backups/';
+		$oBackup->DownloadBackup($sBackupDir.$sFile);
+		break;
+	}
+}
+catch (Exception $e)
+{
+	IssueLog::Error($e->getMessage());
+}
+
+?>

+ 54 - 0
datamodels/2.x/itop-backup/backup.params.distrib

@@ -0,0 +1,54 @@
+# Parameters file for backup.php and check-backup.php
+#
+# Usage:
+# backup.php --param_file=<this file>[,<another one>]
+# or
+# http://.../itop-backup/backup.php?param_file=<this file>[,<another one>]
+#
+# If a parameter is given both in the file and in the arguments,
+# then the value given as argument is retained
+#
+# Note: most of the default values provided here should work fine
+#       if you have created sample data with the setup program
+
+# MySQL coming with Easy PHP (Windows)
+mysql_bindir = C:\Program Files\EasyPHP-5.3.6.0\mysql\bin
+
+
+# Authentication
+auth_user = admin
+auth_pwd = admin
+
+# Target file - path and filename (optional)
+#
+# Formatting rules:
+# %Y-%m-%d => 2011-01-25... see PHP documentation of strftime()
+# Placeholders:
+#    __HOST__      MySQL server
+#    __DB__        Database name
+#    __SUBNAME__   Tables prefix
+# 
+backup_file = /var/log/__DB__-%Y-%m-%d
+	
+# Check thresholds (check-backup.php)
+#
+check_size_min = 20000 # bytes
+check_size_reduction_max = 10 # percentage
+
+# Ticket creation (check-backup.php)
+#
+# If the backup has failed, a ticket will be created
+# This process relies on the SOAP service "CreateIncident"
+#
+# Root URL of an instance of iTop, into which the ticket will be created
+check_ticket_itop = http://localhost/myiTop 
+# Any of the above paramaters are mandatory
+check_ticket_login = admin # must have the right to create an Incident Ticket
+check_ticket_pwd = admin
+check_ticket_title = Backup check failed
+check_ticket_customer = Demo
+check_ticket_service = Computers and peripherals
+check_ticket_service_subcategory = Repair
+check_ticket_workgroup = Hardware support
+check_ticket_impacted_server = dbserver1.demo.com
+	

+ 196 - 0
datamodels/2.x/itop-backup/backup.php

@@ -0,0 +1,196 @@
+<?php
+// Copyright (C) 2014 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__));
+require_once(__DIR__.'/../../approot.inc.php');
+require_once(APPROOT.'application/application.inc.php');
+require_once(APPROOT.'application/webpage.class.inc.php');
+require_once(APPROOT.'application/csvpage.class.inc.php');
+require_once(APPROOT.'application/clipage.class.inc.php');
+require_once(APPROOT.'application/ajaxwebpage.class.inc.php');
+
+require_once(APPROOT.'core/log.class.inc.php');
+
+require_once(APPROOT.'application/startup.inc.php');
+
+class MyDBBackup extends DBBackup
+{
+	protected function LogInfo($sMsg)
+	{
+		$this->oPage->p($sMsg);
+	}
+
+	protected function LogError($sMsg)
+	{
+		$this->oPage->p('Error: '.$sMsg);
+		ToolsLog::Error($sMsg);
+	}
+
+	protected $oPage;
+	public function __construct($oPage)
+	{
+		$this->oPage = $oPage;
+		parent::__construct();
+	}
+}
+
+
+/**
+ * Checks if a parameter (possibly empty) was specified when calling this page
+ */
+function CheckParam($sParamName)
+{
+	global $argv;
+	
+	if (isset($_REQUEST[$sParamName])) return true; // HTTP parameter either GET or POST
+	if (!is_array($argv)) return false;
+	foreach($argv as $sArg)
+	{
+		if ($sArg == '--'.$sParamName) return true; // Empty command line parameter, long unix style
+		if ($sArg == $sParamName) return true; // Empty command line parameter, Windows style
+		if ($sArg == '-'.$sParamName) return true; // Empty command line parameter, short unix style
+		if (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) return true; // Command parameter with a value
+	}
+	return false;
+}
+
+function Usage($oP)
+{
+	$oP->p('Perform a backup of the iTop database by running mysqldump');
+	$oP->p('Parameters:');
+	if (utils::IsModeCLI())
+	{
+		$oP->p('auth_user: login, must be administrator');
+		$oP->p('auth_pwd: ...');
+	}
+	$oP->p('backup_file [optional]: name of the file to store the backup into. Follows the PHP strftime format spec. The following placeholders are available: __HOST__, __DB__, __SUBNAME__');
+	$oP->p('simulate [optional]: set to check the name of the file that would be created');
+	$oP->p('mysql_bindir [optional]: specify the path for mysqldump');
+
+	if (utils::IsModeCLI())
+	{
+		$oP->p('Example: php -q backup.php --auth_user=admin --auth_pwd=myPassw0rd');
+		$oP->p('Known limitation: the current directory must be the directory of backup.php');
+	}
+	else
+	{
+		$oP->p('Example: .../backup.php?backup_file=/tmp/backup.__DB__-__SUBNAME__.%Y-%m');
+	}
+}
+
+function ExitError($oP, $sMessage)
+{
+	ToolsLog::Error($sMessage);
+	$oP->p($sMessage);
+	$oP->output();
+	exit;
+}
+
+
+function ReadMandatoryParam($oP, $sParam)
+{
+	$sValue = utils::ReadParam($sParam, null, true /* Allow CLI */, 'raw_data');
+	if (is_null($sValue))
+	{
+		ExitError($oP, "ERROR: Missing argument '$sParam'");
+	}
+	return trim($sValue);
+}
+
+
+/////////////////////////////////
+// Main program
+
+set_time_limit(0);
+
+if (utils::IsModeCLI())
+{
+	$oP = new CLIPage("iTop - Database Backup");
+}
+else
+{
+	$oP = new WebPage("iTop - Database Backup");
+}
+
+try
+{
+	utils::UseParamFile();
+}
+catch(Exception $e)
+{
+	ExitError($oP, $e->GetMessage());
+}
+
+if (utils::IsModeCLI())
+{
+	$oP->p(date('Y-m-d H:i:s')." - running backup utility");
+	$sAuthUser = ReadMandatoryParam($oP, 'auth_user');
+	$sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd');
+	$bDownloadBackup = false;
+	if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd))
+	{
+		UserRights::Login($sAuthUser); // Login & set the user's language
+	}
+	else
+	{
+		ExitError($oP, "Access restricted or wrong credentials ('$sAuthUser')");
+	}
+}
+else
+{
+	require_once(APPROOT.'application/loginwebpage.class.inc.php');
+	LoginWebPage::DoLogin(); // Check user rights and prompt if needed
+	$bDownloadBackup = utils::ReadParam('download', false);
+}
+
+if (!UserRights::IsAdministrator())
+{
+	ExitError($oP, "Access restricted to administors");
+}
+
+if (CheckParam('?') || CheckParam('h') || CheckParam('help'))
+{
+	Usage($oP);
+	$oP->output();
+	exit;
+}
+
+
+$sDefaultBackupFileName = SetupUtils::GetTmpDir().'/'."__DB__-%Y-%m-%d";
+$sBackupFile =  utils::ReadParam('backup_file', $sDefaultBackupFileName, true, 'raw_data');
+
+// Interpret strftime specifications (like %Y) and database placeholders
+$oBackup = new MyDBBackup($oP);
+$oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
+$sBackupFile = $oBackup->MakeName($sBackupFile);
+$sZipArchiveFile = $sBackupFile.'.zip';
+
+$bSimulate = utils::ReadParam('simulate', false, true);
+$res = false;
+if ($bSimulate)
+{
+	$oP->p("Simulate: would create file '$sZipArchiveFile'");
+}
+else
+{
+	$oBackup->CreateZip($sZipArchiveFile);
+}
+if ($res && $bDownloadBackup)
+{
+	$oBackup->DownloadBackup($sZipArchiveFile);
+}
+$oP->output();
+?>

+ 267 - 0
datamodels/2.x/itop-backup/check-backup.php

@@ -0,0 +1,267 @@
+<?php
+// Copyright (C) 2014 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+// Purpose: check that the backup has been successfully executed
+//          this script is aimed at being invoked in CLI mode only
+
+// Developer's notes:
+//   Duplicated code: sys_get_temp_dir, the computation of the target filename, etc.
+
+// Recommended usage in CRON
+// /usr/bin/php -q /var/www/combodo/modules/itop-backup/backup.php --backup_file=/home/backups/combodo-crm-%Y-%m-%d
+
+if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__));
+require_once(__DIR__.'/../../approot.inc.php');
+require_once(APPROOT.'/application/utils.inc.php');
+require_once(APPROOT.'/core/config.class.inc.php');
+
+
+function ReadMandatoryParam($sParam)
+{
+	$value = utils::ReadParam($sParam, null, true /* Allow CLI */, 'raw_data');
+	if (is_null($value))
+	{
+		throw new Exception("Missing argument '$sParam'");
+	}
+	return $value; 
+}
+
+if (!function_exists('sys_get_temp_dir'))
+{
+	// Based on http://www.phpit.net/
+	// article/creating-zip-tar-archives-dynamically-php/2/
+	function sys_get_temp_dir()
+	{
+		// Try to get from environment variable
+		if (!empty($_ENV['TMP']))
+		{
+			return realpath($_ENV['TMP']);
+		}
+		else if (!empty($_ENV['TMPDIR']))
+		{
+			return realpath($_ENV['TMPDIR']);
+		}
+		else if (!empty($_ENV['TEMP']))
+		{
+			return realpath($_ENV['TEMP']);
+		}
+		// Detect by creating a temporary file
+		else
+		{
+			// Try to use system's temporary directory
+			// as random name shouldn't exist
+			$temp_file = tempnam(md5(uniqid(rand(), TRUE)), '');
+			if ($temp_file)
+			{
+				$temp_dir = realpath(dirname($temp_file));
+				unlink($temp_file);
+				return $temp_dir;
+			}
+			else
+			{
+				return FALSE;
+			}
+		}
+	}
+}
+
+
+
+function MakeArchiveFileName($iRefTime = null)
+{
+	$sDefaultBackupFileName = sys_get_temp_dir().'/'."__DB__-%Y-%m-%d";
+	$sBackupFile =  utils::ReadParam('backup_file', $sDefaultBackupFileName, true, 'raw_data');
+	
+	$oConfig = new Config(APPCONF.'production/config-itop.php');
+	
+	$sBackupFile = str_replace('__HOST__', $oConfig->GetDBHost(), $sBackupFile);
+	$sBackupFile = str_replace('__DB__', $oConfig->GetDBName(), $sBackupFile);
+	$sBackupFile = str_replace('__SUBNAME__', $oConfig->GetDBSubName(), $sBackupFile);
+	
+	if (is_null($iRefTime))
+	{
+		$sBackupFile = strftime($sBackupFile);
+	}
+	else
+	{
+		$sBackupFile = strftime($sBackupFile, $iRefTime);
+	}
+
+	return $sBackupFile.'.zip';
+}
+
+
+
+function RaiseAlarm($sMessage)
+{
+	echo "$sMessage\n";
+
+	try
+	{
+		$sTicketLogin = ReadMandatoryParam('check_ticket_login');
+		$sTicketPwd = ReadMandatoryParam('check_ticket_pwd');
+		$sTicketTitle = ReadMandatoryParam('check_ticket_title');
+		$sTicketCustomer = ReadMandatoryParam('check_ticket_customer');
+		$sTicketService = ReadMandatoryParam('check_ticket_service');
+		$sTicketSubcategory = ReadMandatoryParam('check_ticket_service_subcategory');
+		$sTicketWorkgroup = ReadMandatoryParam('check_ticket_workgroup');
+		$sTicketImpactedServer = ReadMandatoryParam('check_ticket_impacted_server');
+	}
+	catch (Exception $e)
+	{
+		echo "The ticket could not be created: ".$e->GetMessage()."\n";
+		return;
+	}
+
+   $sMessage = "Server: [[Server:".$sTicketImpactedServer."]]\n".$sMessage;
+
+	require_once(APPROOT.'webservices/itopsoaptypes.class.inc.php');
+	
+	//$sItopRootDefault = 'http'.((isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS']!='off')) ? 's' : '').'://'.$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].dirname($_SERVER['SCRIPT_NAME']).'/../..';
+	//$sItopRoot = utils::ReadParam('check_ticket_itop', $sItopRootDefault);
+	$sItopRoot = ReadMandatoryParam('check_ticket_itop');
+
+	$sWsdlUri = $sItopRoot.'/webservices/itop.wsdl.php';
+	//$sWsdlUri .= '?service_category=';
+	
+	$aSOAPMapping = SOAPMapping::GetMapping();
+	
+	ini_set("soap.wsdl_cache_enabled","0");
+	$oSoapClient = new SoapClient(
+		$sWsdlUri,
+		array(
+			'trace' => 1,
+			'classmap' => $aSOAPMapping, // defined in itopsoaptypes.class.inc.php
+		)
+	);
+	
+	try
+	{
+		$oRes = $oSoapClient->CreateIncidentTicket
+		(
+			$sTicketLogin, /* login */
+			$sTicketPwd, /* password */
+			$sTicketTitle, /* title */
+			$sMessage, /* description */
+			null, /* caller */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', $sTicketCustomer))), /* customer */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', $sTicketService))), /* service */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', $sTicketSubcategory))), /* service subcategory */
+			'', /* product */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', $sTicketWorkgroup))), /* workgroup */
+			array(
+				new SOAPLinkCreationSpec(
+					'Server',
+					array(new SOAPSearchCondition('name', $sTicketImpactedServer)),
+					array()
+				),
+			), /* impacted cis */
+			'1', /* impact */
+			'1' /* urgency */
+		);
+	}
+	catch(Exception $e)
+	{
+		echo "The ticket could not be created: SOAP Exception = '".$e->getMessage()."'\n";
+	}
+
+	//echo "<pre>\n";
+	//print_r($oRes);
+	//echo "</pre>\n";
+
+	if ($oRes->status)
+	{
+		$sTicketName = $oRes->result[0]->values[1]->value;
+		echo "Created ticket: $sTicketName\n";
+	}
+	else
+	{
+		echo "ERROR: Failed to create the ticket in target iTop ($sItopRoot)\n";
+		foreach ($oRes->errors->messages as $oMessage)
+		{
+			echo $oMessage->text."\n";
+		}
+	}	
+}
+
+
+//////////
+// Main
+
+try
+{
+	utils::UseParamFile();
+}
+catch(Exception $e)
+{
+	echo "Error: ".$e->GetMessage()."\n";
+	exit;
+}
+
+$sZipArchiveFile = MakeArchiveFileName();
+echo date('Y-m-d H:i:s')." - Checking file: $sZipArchiveFile\n";
+
+if (file_exists($sZipArchiveFile))
+{
+	if ($aStat = stat($sZipArchiveFile))
+	{
+		$iSize = (int) $aStat['size'];
+		$iMIN = utils::ReadParam('check_size_min', 0);
+		if ($iSize > $iMIN)
+		{
+			echo "Found the archive\n";
+			$sOldArchiveFile = MakeArchiveFileName(time() - 86400); // yesterday's archive
+			if (file_exists($sOldArchiveFile))
+			{
+				if ($aOldStat = stat($sOldArchiveFile))
+				{
+					echo "Comparing its size with older file: $sOldArchiveFile\n";
+					$iOldSize = (int) $aOldStat['size'];
+					$fVariationPercent = 100 * ($iSize - $iOldSize) / $iOldSize;
+					$sVariation = round($fVariationPercent, 2)." percent(s)";
+
+					$iREDUCTIONMAX = utils::ReadParam('check_size_reduction_max');
+					if ($fVariationPercent < -$iREDUCTIONMAX)
+					{
+						RaiseAlarm("Backup file '$sZipArchiveFile' changed by $sVariation, expecting a reduction limited to $iREDUCTIONMAX percents of the original size");
+					}
+					elseif ($fVariationPercent < 0)
+					{
+						echo "Size variation: $sVariation (the maximum allowed reduction is $iREDUCTIONMAX) \n";
+					}
+					else
+					{
+						echo "The archive grew by: $sVariation\n";
+					}
+				}
+			}
+		}
+		else
+		{
+			RaiseAlarm("Backup file '$sZipArchiveFile' too small (Found: $iSize, while expecting $iMIN bytes)");
+		}
+	}
+	else
+	{
+		RaiseAlarm("Failed to stat backup file '$sZipArchiveFile'");
+	}
+}
+else
+{
+	RaiseAlarm("Missing backup file '$sZipArchiveFile'");
+}
+
+?>

+ 129 - 0
datamodels/2.x/itop-backup/dbrestore.class.inc.php

@@ -0,0 +1,129 @@
+<?php
+// Copyright (C) 2014 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+
+class DBRestore extends DBBackup
+{
+	protected function LogInfo($sMsg)
+	{
+		//IssueLog::Info('non juste info: '.$sMsg);
+	}
+	protected function LogError($sMsg)
+	{
+		IssueLog::Error($sMsg);
+	}
+
+	protected function LoadDatabase($sDataFile)
+	{
+		$this->LogInfo("Loading data onto $this->sDBHost/$this->sDBName(suffix:'$this->sDBSubName')");
+
+		// Just to check the connection to the DB (more accurate than getting the retcode of mysql)
+		$oMysqli = $this->DBConnect();
+
+		$sHost = self::EscapeShellArg($this->sDBHost);
+		$sUser = self::EscapeShellArg($this->sDBUser);
+		$sPwd = self::EscapeShellArg($this->sDBPwd);
+		$sDBName = self::EscapeShellArg($this->sDBName);
+		if (empty($this->sMySQLBinDir))
+		{
+			$sMySQLExe = 'mysql';
+		}
+		else
+		{
+			$sMySQLExe = '"'.$this->sMySQLBinDir.'/mysql"';
+		}
+		if (is_null($this->iDBPort))
+		{
+			$sPortOption = '';
+		}
+		else
+		{
+			$sPortOption = '--port='.$this->iDBPort.' ';
+		}
+
+		$sDataFileEscaped = self::EscapeShellArg($sDataFile);
+		$sCommand = "$sMySQLExe --default-character-set=utf8 --host=$sHost $sPortOption --user=$sUser --password=$sPwd $sDBName <$sDataFileEscaped 2>&1";
+		$sCommandDisplay = "$sMySQLExe --default-character-set=utf8 --host=$sHost $sPortOption --user=xxxx --password=xxxx $sDBName <$sDataFileEscaped 2>&1";
+
+		// Now run the command for real
+		$this->LogInfo("Executing command: $sCommandDisplay");
+		$aOutput = array();
+		$iRetCode = 0;
+		exec($sCommand, $aOutput, $iRetCode);
+		foreach($aOutput as $sLine)
+		{
+			$this->LogInfo("mysql said: $sLine");
+		}
+		if ($iRetCode != 0)
+		{
+			$this->LogError("Failed to execute: $sCommandDisplay. The command returned:$iRetCode");
+			foreach($aOutput as $sLine)
+			{
+				$this->LogError("mysql said: $sLine");
+			}
+			if (count($aOutput) == 1) 
+			{
+				$sMoreInfo = trim($aOutput[0]); 
+			}
+			else
+			{
+				$sMoreInfo = "Check the log file '".realpath(APPROOT.'/log/error.log')."' for more information.";
+			}
+			throw new BackupException("Failed to execute mysql: ".$sMoreInfo);
+		}
+	}
+
+	public function RestoreFromZip($sZipFile, $sEnvironment = 'production')
+	{
+		$this->LogInfo("Starting restore of ".basename($sZipFile));
+
+		$oZip = new ZipArchive();
+		$res = $oZip->open($sZipFile);
+
+		// Load the database
+		//
+		$sDataDir = tempnam(SetupUtils::GetTmpDir(), 'itop-');
+		unlink($sDataDir); // I need a directory, not a file...
+		SetupUtils::builddir($sDataDir); // Here is the directory
+		$oZip->extractTo($sDataDir, 'itop-dump.sql');
+		$sDataFile = $sDataDir.'/itop-dump.sql';
+		$this->LoadDatabase($sDataFile);
+		unlink($sDataFile);
+
+		// Update the code
+		//
+		$sDeltaFile = APPROOT.'data/'.$sEnvironment.'.delta.xml';
+		if ($oZip->locateName('delta.xml') !== false)
+		{
+			// Extract and rename delta.xml => <env>.delta.xml;
+			file_put_contents($sDeltaFile, $oZip->getFromName('delta.xml'));
+		}
+		else
+		{
+			@unlink($sDeltaFile);
+		}
+		$sConfigFile = APPROOT.'conf/'.$sEnvironment.'/config-itop.php';
+		@chmod($sConfigFile, 0770); // Allow overwriting the file
+		$oZip->extractTo(APPROOT.'conf/'.$sEnvironment, 'config-itop.php');
+		@chmod($sConfigFile, 0444); // Read-only
+
+		$oEnvironment = new RunTimeEnvironment($sEnvironment);
+		$oEnvironment->CompileFrom($sEnvironment);
+	}
+}
+

+ 48 - 0
datamodels/2.x/itop-backup/de.dict.itop-backup.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Localized data
+ *
+ * @copyright   Copyright (C) 2013 Combodo
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ 
+  * @author      Robert Jaehne <robert.jaehne@itomig.de>
+ 
+ */
+
+Dict::Add('DE DE', 'German', 'Deutsch', array(
+
+	'bkp-backup-running' => 'Backup wird durchgeführt. Bitte warten ...',
+	'bkp-restore-running' => 'Wiederherstellung läuft. Bitte warten ...',
+
+	'Menu:BackupStatus' => 'Geplante Backups',
+	'bkp-status-title' => 'Geplante Backups',
+	'bkp-status-checks' => 'Einstellungen und Prüfungen',
+	'bkp-mysqldump-ok' => 'mysqldump ist vorhanden: %1$s',
+	'bkp-mysqldump-notfound' => 'mysqldump wurde nicht gefunden: %1$s - Stellen sie sicher, das er eingespielt und im Pfad verfügbar ist oder editieren sie die Konfigurationsdatei um das MySQL bindir anzupassen.',
+	'bkp-mysqldump-issue' => 'mysqldump konnte nicht eingespielt werden (retcode=%1$d): Stellen sie sicher, das er eingespielt und im Pfad verfügbar ist oder editieren sie die Konfigurationsdatei um das MySQL bindir anzupassen.',
+	'bkp-missing-dir' => 'Zielverzeichniss %1$s nicht gefunden',
+	'bkp-free-disk-space' => '<b>%1$s frei</b> in %2$s',
+	'bkp-dir-not-writeable' => '%1$s ist nicht schreibbar',
+	'bkp-wrong-format-spec' => 'Die verwendete Definition zur Formatierung von Dateinamen ist nicht korrekt (%1$s). Die Standard-Definition %2$s wird verwendet',
+	'bkp-name-sample' => 'Backup-Dateien werden abhängig von Datum, Zeit und Datenbank-Identifier erstellt. Beispiel: %1$s',
+	'bkp-week-days' => 'Backups werden <b>jeden %1$s um %2$s durchgeführt</b>',
+	'bkp-retention' => 'Mindestens <b>%1$d Backups werden im Zielverzeichniss vorgehalten</b>',
+	'bkp-next-to-delete' => 'Wird gelöscht, wenn das nächste Backup angelegt wird (unter Einstellungen "Menge vorhalten")',
+	'bkp-table-file' => 'Datei', 
+	'bkp-table-file+' => 'Nur Dateien mit der Endung .zip werden als Backup-Dateien berücksichtigt.',
+	'bkp-table-size' => 'Grösse',
+	'bkp-table-size+' => '',
+	'bkp-table-actions' => 'Aktionen',
+	'bkp-table-actions+' => '',
+	'bkp-status-backups-auto' => 'Geplante Backups',
+	'bkp-status-backups-manual' => 'Manuelle Backups',
+	'bkp-status-backups-none' => 'Kein Backup vorhanden',
+	'bkp-next-backup' => 'Das nächste Backup wird am <b>%1$s</b> (%2$s) um %3$s durchgeführt',
+	'bkp-button-backup-now' => 'Backup läuft!',
+	'bkp-button-restore-now' => 'Wiederherstellen!',
+	'bkp-confirm-backup' => 'Bitte bestätigen sie, dass sie jetzt ein Backup erstellen wollen now.',
+	'bkp-confirm-restore' => 'Bitte bestätigen sie, dass sie mit Backup %1$s eine Wiederherstellung durchführen wollen.',
+	'bkp-wait-backup' => 'Bitte warten, bis das Backup abgeschlossen ist ...',
+	'bkp-wait-restore' => 'Bitte warten, bis die Wiederherstellung abgeschlossen ist ...',
+	'bkp-success-restore' => 'Wiederherstellung erfolgreich.',
+));

+ 46 - 0
datamodels/2.x/itop-backup/en.dict.itop-backup.php

@@ -0,0 +1,46 @@
+<?php
+/**
+ * Localized data
+ *
+ * @copyright   Copyright (C) 2013 Combodo
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+Dict::Add('EN US', 'English', 'English', array(
+
+	'bkp-backup-running' => 'A backup is running. Please wait...',
+	'bkp-restore-running' => 'A restore is running. Please wait...',
+
+	'Menu:BackupStatus' => 'Scheduled Backups',
+	'bkp-status-title' => 'Scheduled Backups',
+	'bkp-status-checks' => 'Settings and checks',
+	'bkp-mysqldump-ok' => 'mysqldump is present: %1$s',
+	'bkp-mysqldump-notfound' => 'mysqldump could not be found: %1$s - Please make sure it is installed and in the path, or edit the configuration file to tune mysql_bindir.',
+	'bkp-mysqldump-issue' => 'mysqldump could not be executed (retcode=%1$d): Please make sure it is installed and in the path, or edit the configuration file to tune mysql_bindir',
+	'bkp-missing-dir' => 'The target directory %1$s count not be found',
+	'bkp-free-disk-space' => '<b>%1$s free</b> in %2$s',
+	'bkp-dir-not-writeable' => '%1$s is not writeable',
+	'bkp-wrong-format-spec' => 'The current specification to format the file names is wrong (%1$s). A default specification will apply: %2$s',
+	'bkp-name-sample' => 'Backup files are named depending on DB identifiers, date and time. Example: %1$s',
+	'bkp-week-days' => 'Backups will occur <b>every %1$s at %2$s</b>',
+	'bkp-retention' => 'At most <b>%1$d backup files will be kept</b> in the target directory.',
+	'bkp-next-to-delete' => 'Will be deleted when the next backup occurs (see the setting "retention_count")',
+	'bkp-table-file' => 'File', 
+	'bkp-table-file+' => 'Only files having the extension .zip are considered as being backup files',
+	'bkp-table-size' => 'Size',
+	'bkp-table-size+' => '',
+	'bkp-table-actions' => 'Actions',
+	'bkp-table-actions+' => '',
+	'bkp-status-backups-auto' => 'Scheduled backups',
+	'bkp-status-backups-manual' => 'Manual backups',
+	'bkp-status-backups-none' => 'No backup yet',
+	'bkp-next-backup' => 'The next backup will occur on <b>%1$s</b> (%2$s) at %3$s',
+	'bkp-button-backup-now' => 'Backup now!',
+	'bkp-button-restore-now' => 'Restore!',
+	'bkp-confirm-backup' => 'Please confirm that you do request the backup to occur right now.',
+	'bkp-confirm-restore' => 'Please confirm that you do want to restore the backup %1$s.',
+	'bkp-wait-backup' => 'Please wait for the backup to complete...',
+	'bkp-wait-restore' => 'Please wait for the restore to complete...',
+	'bkp-success-restore' => 'Restore successfully completed.',
+));
+?>

+ 46 - 0
datamodels/2.x/itop-backup/fr.dict.itop-backup.php

@@ -0,0 +1,46 @@
+<?php
+/**
+ * Localized data
+ *
+ * @copyright   Copyright (C) 2013 Combodo
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+Dict::Add('FR FR', 'French', 'Français', array(
+
+	'bkp-backup-running' => 'Une sauvegarde est en cours. Veuillez patienter...',
+	'bkp-restore-running' => 'Une restauration des données est en cours. Veuillez patienter...',
+
+	'Menu:BackupStatus' => 'Sauvegarde automatique',
+	'bkp-status-title' => 'Sauvegarde automatique',
+	'bkp-status-checks' => 'Réglages et vérifications',
+	'bkp-mysqldump-ok' => 'mysqldump est installé: %1$s',
+	'bkp-mysqldump-notfound' => 'mysqldump n\'a pas été trouvé: %1$s - Veuillez vous assurer que les outils mysql sont installés et qu\'ils sont accessibles en ligne de commande, ou bien éditez le fichier de configuration pour en donner le chemin via mysql_bindir.',
+	'bkp-mysqldump-issue' => 'mysqldump n\'a pas pu être exécuté (code de retour: %1$d). Veuillez vérifier que les outils mysql sont installés et qu\'ils sont accessibles en ligne de commande, ou bien éditez le fichier de configuration pour en donner le chemin via mysql_bindir.',
+	'bkp-missing-dir' => 'Le répertoire cible \'%1$s\' n\'existe pas ou ne peut pas être lu.',
+	'bkp-free-disk-space' => 'Vous disposez de <b>%1$s d\'espace disque</b> sur %2$s',
+	'bkp-dir-not-writeable' => 'Le répertoire cible \'%1$s\' n\'est pas accessible en écriture.',
+	'bkp-wrong-format-spec' => 'La spécification de format pour le nom des sauvegarde est incorrecte (%1$s). La spécification par défaut sera appliquée: %2$s',
+	'bkp-name-sample' => 'Les fichiers de sauvegardes seront nommés en fonction de la base, la date et l\'heure. Par exemple: %1$s',
+	'bkp-week-days' => 'Les sauvegardes seront effectuées <b>tous les %1$s à %2$s</b>',
+	'bkp-retention' => 'Au plus <b>%1$d fichiers de sauvegardes seront conservés</b> dans le répertoire cible.',
+	'bkp-next-to-delete' => 'Sera effacé lors de la prochaine sauvegarde (Cf. réglage "retention_count")',
+	'bkp-table-file' => 'Fichier', 
+	'bkp-table-file+' => 'Seuls les fichiers ayant l\'extension .zip sont considérés comme étant des fichiers de sauvegarde',
+	'bkp-table-size' => 'Taille',
+	'bkp-table-size+' => '',
+	'bkp-table-actions' => 'Actions',
+	'bkp-table-actions+' => '',
+	'bkp-status-backups-auto' => 'Sauvegardes automatiques',
+	'bkp-status-backups-manual' => 'Sauvegardes manuelles',
+	'bkp-status-backups-none' => 'Aucune sauvegarde n\'a été faite jusqu\' à présent.',
+	'bkp-next-backup' => 'La prochaine sauvegarde aura lieu <b>%1$s</b> (%2$s) à %3$s',
+	'bkp-button-backup-now' => 'Sauvegarder maintenant !',
+	'bkp-button-restore-now' => 'Restaurer !',
+	'bkp-confirm-backup' => 'Veuillez confirmer que vous souhaiter effectuer une sauvegarde maintenant.',
+	'bkp-confirm-restore' => 'Veuillez confirmer que vous souhaiter effectuer la restauration de \'%1$s\' maintenant.',
+	'bkp-wait-backup' => 'Sauvegarde en cours...',
+	'bkp-wait-restore' => 'Restauration des données en cours...',
+	'bkp-success-restore' => 'Restauration des données terminée.',
+));
+?>

+ 302 - 0
datamodels/2.x/itop-backup/main.itop-backup.php

@@ -0,0 +1,302 @@
+<?php
+// Copyright (C) 2014 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+require_once(APPROOT.'setup/setuputils.class.inc.php');
+require_once(APPROOT.'setup/backup.class.inc.php');
+require_once(APPROOT.'core/mutex.class.inc.php');
+
+
+define('BACKUP_DEFAULT_FORMAT', '__DB__-%Y-%m-%d_%H_%M');
+
+class BackupHandler extends ModuleHandlerAPI
+{
+	public static function OnMetaModelStarted()
+	{
+
+		try
+		{
+			$oBackupMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
+			if ($oBackupMutex->TryLock())
+			{
+				$oBackupMutex->Unlock();
+			}
+			else
+			{
+				// Not needed: the DB dump is done in a single transaction
+				//MetaModel::GetConfig()->Set('access_mode', ACCESS_READONLY, 'itop-backup');
+				//MetaModel::GetConfig()->Set('access_message', ' - '.dict::S('bkp-backup-running'), 'itop-backup');
+			}
+	
+			$oRestoreMutex = new iTopMutex('restore.'.utils::GetCurrentEnvironment());
+			if ($oRestoreMutex->TryLock())
+			{
+				$oRestoreMutex->Unlock();
+			}
+			else
+			{
+				MetaModel::GetConfig()->Set('access_mode', ACCESS_READONLY, 'itop-backup');
+				MetaModel::GetConfig()->Set('access_message', ' - '.dict::S('bkp-restore-running'), 'itop-backup');
+			}
+		}
+		catch(Exception $e)
+		{
+			
+		}
+	}
+}
+
+class DBBackupScheduled extends DBBackup
+{
+	protected function LogInfo($sMsg)
+	{
+		static $bDebug = null;
+		if ($bDebug == null)
+		{
+			$bDebug = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'debug', false);
+		}
+
+		if ($bDebug)
+		{
+			echo $sMsg."\n";
+		}
+	}
+
+	protected function LogError($sMsg)
+	{
+		static $bDebug = null;
+		if ($bDebug == null)
+		{
+			$bDebug = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'debug', false);
+		}
+
+		IssueLog::Error($sMsg);
+		if ($bDebug)
+		{
+			echo 'Error: '.$sMsg."\n";
+		}
+	}
+
+	/**
+	 * List and order by date the backups in the given directory 	
+	 * Note: the algorithm is currently based on the file modification date... because there is no "creation date" in general
+	 */	
+	public function ListFiles($sBackupDir)
+	{
+		$aFiles = array();
+		$aTimes = array();
+		foreach(glob($sBackupDir.'*.zip') as $sFilePath)
+		{
+			$aFiles[] = $sFilePath;
+			$aTimes[] = filemtime($sFilePath); // unix time
+		}
+		array_multisort($aTimes, $aFiles);
+	
+		return $aFiles;
+	}
+}
+
+class BackupExec implements iScheduledProcess
+{
+	protected $sBackupDir;
+	protected $iRetentionCount;
+
+	/**
+	 * Constructor
+	 * @param sBackupDir string Target directory, defaults to APPROOT/data/backups/auto
+	 * @param iRetentionCount int Rotation (default to the value given in the configuration file 'retentation_count') set to 0 to disable this feature	 	 
+	 */	 	
+	public function __construct($sBackupDir = null, $iRetentionCount = null)
+	{
+		if (is_null($sBackupDir))
+		{
+			$this->sBackupDir = APPROOT.'data/backups/auto/';
+		}
+		else
+		{
+			$this->sBackupDir = $sBackupDir;
+		}
+		if (is_null($iRetentionCount))
+		{
+			$this->iRetentionCount = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'retention_count', 5);
+		}
+		else
+		{
+			$this->iRetentionCount = $iRetentionCount;
+		}
+	}
+
+	public function Process($iUnixTimeLimit)
+	{
+		$oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
+		$oMutex->Lock();
+
+		try
+		{
+			// Make sure the target directory exists
+			SetupUtils::builddir($this->sBackupDir);
+	
+			$oBackup = new DBBackupScheduled();
+
+			// Eliminate files exceeding the retention setting
+			//
+			if ($this->iRetentionCount > 0)
+			{
+				$aFiles = $oBackup->ListFiles($this->sBackupDir);
+				while (count($aFiles) >= $this->iRetentionCount)
+				{
+					$sFileToDelete = array_shift($aFiles);
+					unlink($sFileToDelete);
+					if (file_exists($sFileToDelete))
+					{
+						// Ok, do not loop indefinitely on this
+						break;
+					}
+				}
+			}
+	
+			// Do execute the backup
+			//
+			$oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
+	
+			$sBackupFile = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'file_name_format', '__DB__-%Y-%m-%d_%H_%M');
+			$sName = $oBackup->MakeName($sBackupFile);
+			if ($sName == '')
+			{
+				$sName = $oBackup->MakeName(BACKUP_DEFAULT_FORMAT);
+			}
+			$sZipFile = $this->sBackupDir.$sName.'.zip';
+			$sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
+			$oBackup->CreateZip($sZipFile, $sSourceConfigFile);
+		}
+		catch (Exception $e)
+		{
+			$oMutex->Unlock();
+			throw $e;
+		}
+		$oMutex->Unlock();
+		return "Created the backup: $sZipFile";
+	}
+
+	/*
+		Interpret current setting for the week days
+		@returns array of int (monday = 1)
+	*/
+	public function InterpretWeekDays()
+	{
+		static $aWEEKDAYTON = array('monday' => 1, 'tuesday' => 2, 'wednesday' => 3, 'thursday' => 4, 'friday' => 5, 'saturday' => 6, 'sunday' => 7);
+		$aDays = array();
+		$sWeekDays = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'week_days', 'monday, tuesday, wednesday, thursday, friday');
+		if ($sWeekDays != '')
+		{
+			$aWeekDaysRaw = explode(',', $sWeekDays);
+			foreach ($aWeekDaysRaw as $sWeekDay)
+			{
+				$sWeekDay = strtolower(trim($sWeekDay));
+				if (array_key_exists($sWeekDay, $aWEEKDAYTON))
+				{
+					$aDays[] = $aWEEKDAYTON[$sWeekDay];
+				}
+				else
+				{
+					throw new Exception("'itop-backup: wrong format for setting 'week_days' (found '$sWeekDay')");
+				}
+			}
+		}
+		if (count($aDays) == 0)
+		{
+			throw new Exception("'itop-backup: missing setting 'week_days'");
+		}
+		$aDays = array_unique($aDays);   
+		sort($aDays);
+		return $aDays;
+	}
+
+	/*
+		Gives the exact time at which the process must be run next time
+		@returns DateTime
+	*/
+	public function GetNextOccurrence()
+	{
+		$bEnabled = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'enabled', true);
+		if (!$bEnabled)
+		{
+			$oRet = new DateTime('3000-01-01');
+		}
+		else
+		{
+			// 1st - Interpret the list of days as ordered numbers (monday = 1)
+			// 
+			$aDays = $this->InterpretWeekDays();	
+	
+			// 2nd - Find the next active week day
+			//
+			$sBackupTime = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'time', '23:30');
+			if (!preg_match('/[0-2][0-9]:[0-5][0-9]/', $sBackupTime))
+			{
+				throw new Exception("'itop-backup: wrong format for setting 'time' (found '$sBackupTime')");
+			}
+			$oNow = new DateTime();
+			$iNextPos = false;
+			for ($iDay = $oNow->format('N') ; $iDay <= 7 ; $iDay++)
+			{
+				$iNextPos = array_search($iDay, $aDays);
+				if ($iNextPos !== false)
+				{
+					if (($iDay > $oNow->format('N')) || ($oNow->format('H:i') < $sBackupTime))
+					{
+						break;
+					}
+				}
+			}
+	
+			// 3rd - Compute the result
+			//
+			if ($iNextPos === false)
+			{
+				// Jump to the first day within the next week
+				$iFirstDayOfWeek = $aDays[0];
+				$iDayMove = $oNow->format('N') - $iFirstDayOfWeek;
+				$oRet = clone $oNow;
+				$oRet->modify('-'.$iDayMove.' days');
+				$oRet->modify('+1 weeks');
+			}
+			else
+			{
+				$iNextDayOfWeek = $aDays[$iNextPos];
+				$iMove = $iNextDayOfWeek - $oNow->format('N');
+				$oRet = clone $oNow;
+				$oRet->modify('+'.$iMove.' days');
+			}
+			list($sHours, $sMinutes) = explode(':', $sBackupTime);
+			$oRet->setTime((int)$sHours, (int) $sMinutes);
+		}
+		return $oRet;
+	}
+}
+
+class ItopBackup extends ModuleHandlerAPI
+{
+	public static function OnMenuCreation()
+	{
+		if (UserRights::IsAdministrator())
+		{
+			$oAdminMenu = new MenuGroup('AdminTools', 80 /* fRank */);
+			new WebPageMenuNode('BackupStatus', utils::GetAbsoluteUrlModulePage('itop-backup', 'status.php'), $oAdminMenu->GetIndex(), 15 /* fRank */);
+		}
+	}
+}
+

+ 60 - 0
datamodels/2.x/itop-backup/module.itop-backup.php

@@ -0,0 +1,60 @@
+<?php
+
+
+SetupWebPage::AddModule(
+	__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
+	'itop-backup/2.1.0',
+	array(
+		// Identification
+		//
+		'label' => 'Backup utilities',
+		'category' => 'Application management',
+
+		// Setup
+		//
+		'dependencies' => array(
+		),
+		'mandatory' => true,
+		'visible' => false,
+
+		// Components
+		//
+		'datamodel' => array(
+			'main.itop-backup.php',
+			//'model.itop-backup.php',
+		),
+		'webservice' => array(
+			//'webservices.itop-backup.php',
+		),
+		'dictionary' => array(
+			'en.dict.itop-backup.php',
+			'fr.dict.itop-backup.php',
+			//'de.dict.itop-backup.php',
+		),
+		'data.struct' => array(
+			//'data.struct.itop-backup.xml',
+		),
+		'data.sample' => array(
+			//'data.sample.itop-backup.xml',
+		),
+		
+		// Documentation
+		//
+		'doc.manual_setup' => '',
+		'doc.more_information' => '',
+
+		// Default settings
+		//
+		'settings' => array(
+			'mysql_bindir' => '',
+			'week_days' => 'monday, tuesday, wednesday, thursday, friday',
+			'time' => '23:30',
+			//'file_name_format' => '__DB__-%Y-%m-%d_%H_%M',
+			'retention_count' => 5, 
+			'enabled' => true,
+			'debug' => false
+		),
+	)
+);
+
+?>

+ 386 - 0
datamodels/2.x/itop-backup/status.php

@@ -0,0 +1,386 @@
+<?php
+// Copyright (C) 2014 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+
+/**
+ * Monitor the backup
+ *
+ * @copyright   Copyright (C) 2013 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__));
+require_once(__DIR__.'/../../approot.inc.php');
+require_once(APPROOT.'application/application.inc.php');
+require_once(APPROOT.'application/itopwebpage.class.inc.php');
+
+require_once(APPROOT.'application/startup.inc.php');
+
+require_once(APPROOT.'application/loginwebpage.class.inc.php');
+
+
+/////////////////////////////////////////////////////////////////////
+// Main program
+//
+LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
+
+//$sOperation = utils::ReadParam('operation', 'menu');
+//$oAppContext = new ApplicationContext();
+
+$oP = new iTopWebPage(Dict::S('bkp-status-title'));
+$oP->set_base(utils::GetAbsoluteUrlAppRoot().'pages/');
+
+
+try
+{
+	$oP->add("<h1>".Dict::S('bkp-status-title')."</h1>");
+
+	$sImgOk = '<img src="../images/validation_ok.png"> ';
+	$sImgError = '<img src="../images/validation_error.png"> ';
+
+	$oP->add("<fieldset>");
+	$oP->add("<legend>".Dict::S('bkp-status-checks')."</legend>");
+
+	// Availability of mysqldump
+	//
+	$sMySQLBinDir = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '');
+	$sMySQLBinDir = utils::ReadParam('mysql_bindir', $sMySQLBinDir, true);
+	if (empty($sMySQLBinDir))
+	{
+		$sMySQLDump = 'mysqldump';
+	}
+	else
+	{
+		//echo 'Info - Found mysql_bindir: '.$sMySQLBinDir;
+		$sMySQLDump = '"'.$sMySQLBinDir.'/mysqldump"';
+	}
+	$sCommand = "$sMySQLDump -V 2>&1";
+
+	$aOutput = array();
+	$iRetCode = 0;
+	exec($sCommand, $aOutput, $iRetCode);
+	if ($iRetCode == 0)
+	{
+		$sMySqlDump = $sImgOk.Dict::Format("bkp-mysqldump-ok", $aOutput[0]);
+	}
+	elseif ($iRetCode == 1)
+	{
+		$sMySqlDump = $sImgError.Dict::Format("bkp-mysqldump-notfound", implode(' ', $aOutput));
+	}
+	else
+	{
+		$sMySqlDump = $sImgError.Dict::Format("bkp-mysqldump-issue", $iRetCode);
+	}
+	foreach($aOutput as $sLine)
+	{
+		//echo 'Info - mysqldump -V said: '.$sLine;
+	}
+	$oP->p($sMySqlDump);
+
+	// Destination directory
+	//
+	// Make sure the target directory exists and is writeable
+	$sBackupDir = APPROOT.'data/backups/';
+	SetupUtils::builddir($sBackupDir);
+	if (!is_dir($sBackupDir))
+	{
+		$oP->p($sImgError.Dict::Format('bkp-missing-dir', $sBackupDir));
+	}
+	else
+	{
+		$oP->p(Dict::Format('bkp-free-disk-space', SetupUtils::HumanReadableSize(SetupUtils::CheckDiskSpace($sBackupDir)), $sBackupDir));
+		if (!is_writable($sBackupDir))
+		{
+			$oP->p($sImgError.Dict::Format('bkp-dir-not-writeable', $sBackupDir));
+		}
+	}
+	$sBackupDirAuto = $sBackupDir.'auto/';
+	SetupUtils::builddir($sBackupDirAuto);
+	$sBackupDirManual = $sBackupDir.'manual/';
+	SetupUtils::builddir($sBackupDirManual);
+
+	// Wrong format
+	//
+	$sBackupFile = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'file_name_format', BACKUP_DEFAULT_FORMAT);
+	$oBackup = new DBBackupScheduled();
+	$sZipName = $oBackup->MakeName($sBackupFile);
+	if ($sZipName == '')
+	{
+		$oP->p($sImgError.Dict::Format('bkp-wrong-format-spec', $sBackupFile, BACKUP_DEFAULT_FORMAT));
+	}
+	else
+	{
+		$oP->p(Dict::Format('bkp-name-sample', $sZipName));
+	}
+
+	// Week Days
+	//
+	$aWeekDayToString = array(
+		1 => Dict::S('DayOfWeek-Monday'),
+		2 => Dict::S('DayOfWeek-Tuesday'),
+		3 => Dict::S('DayOfWeek-Wednesday'),
+		4 => Dict::S('DayOfWeek-Thursday'),
+		5 => Dict::S('DayOfWeek-Friday'),
+		6 => Dict::S('DayOfWeek-Saturday'),
+		7 => Dict::S('DayOfWeek-Sunday')
+	);
+	$aDayLabels = array();
+	$oBackupExec = new BackupExec();
+	foreach ($oBackupExec->InterpretWeekDays() as $iDay)
+	{
+		$aDayLabels[] = $aWeekDayToString[$iDay];
+	}
+	$sDays = implode(', ', $aDayLabels);
+	$sBackupTime = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'time', '23:30');
+	$oP->p(Dict::Format('bkp-week-days', $sDays, $sBackupTime));
+
+	$iRetention = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'retention_count', 5);
+	$oP->p(Dict::Format('bkp-retention', $iRetention));
+
+	$oP->add("</fieldset>");
+
+	// List of backups
+	//
+	$aFiles = $oBackup->ListFiles($sBackupDirAuto);
+	$aFilesToDelete = array();
+	while (count($aFiles) > $iRetention - 1)
+	{
+		$aFilesToDelete[] = array_shift($aFiles);
+	}
+
+	$oRestoreMutex = new iTopMutex('restore.'.utils::GetCurrentEnvironment());
+	if ($oRestoreMutex->TryLock())
+	{
+		$oRestoreMutex->Unlock();
+		$sDisableRestore = '';
+	}
+	else
+	{
+		$sDisableRestore = 'disabled="disabled"';
+	}
+	
+	// 1st table: list the backups made in the background
+	//
+	$aDetails = array();
+	foreach ($oBackup->ListFiles($sBackupDirAuto) as $sBackupFile)
+	{
+		$sFileName = basename($sBackupFile);
+		$sFilePath = 'auto/'.$sFileName;
+		$sAjax = utils::GetAbsoluteUrlModulePage('itop-backup', 'ajax.backup.php', array('operation' => 'download', 'file' => $sFilePath));
+		$sName = "<a href=\"$sAjax\">".$sFileName.'</a>';
+		$sSize = SetupUtils::HumanReadableSize(filesize($sBackupFile));
+		$sConfirmRestore = addslashes(Dict::Format('bkp-confirm-restore', $sFileName));
+		$sFileEscaped = addslashes($sFilePath);
+		$sRestoreBtn = '<button class="restore" onclick="LaunchRestoreNow(\''.$sFileEscaped.'\', \''.$sConfirmRestore.'\');" '.$sDisableRestore.'>'.Dict::S('bkp-button-restore-now').'</button>';
+		if (in_array($sBackupFile, $aFilesToDelete))
+		{
+			$aDetails[] = array('file' => $sName.' <span class="next_to_delete" title="'.Dict::S('bkp-next-to-delete').'">*</span>', 'size' => $sSize, 'actions' => $sRestoreBtn);
+		}
+		else
+		{
+			$aDetails[] = array('file' => $sName, 'size' => $sSize, 'actions' => $sRestoreBtn);
+		}
+	}
+	$aConfig = array(
+		'file' => array('label' => Dict::S('bkp-table-file'), 'description' => Dict::S('bkp-table-file+')),
+		'size' => array('label' => Dict::S('bkp-table-size'), 'description' => Dict::S('bkp-table-size+')),
+		'actions' => array('label' => Dict::S('bkp-table-actions'), 'description' => Dict::S('bkp-table-actions+')),
+	);
+	$oP->add("<fieldset>");
+	$oP->add("<legend>".Dict::S('bkp-status-backups-auto')."</legend>");
+	if (count($aDetails) > 0)
+	{
+		$oP->add('<div style="max-height:400px; overflow: auto;">');
+		$oP->table($aConfig, array_reverse($aDetails));
+		$oP->add('</div>');
+	}
+	else
+	{
+		$oP->p(Dict::S('bkp-status-backups-none'));
+	}
+	$oP->add("</fieldset>");
+
+	// 2nd table: list the backups made manually
+	//
+	$aDetails = array();
+	foreach ($oBackup->ListFiles($sBackupDirManual) as $sBackupFile)
+	{
+		$sFileName = basename($sBackupFile);
+		$sFilePath = 'manual/'.$sFileName;
+		$sAjax = utils::GetAbsoluteUrlModulePage('itop-backup', 'ajax.backup.php', array('operation' => 'download', 'file' => $sFilePath));
+		$sName = "<a href=\"$sAjax\">".$sFileName.'</a>';
+		$sSize = SetupUtils::HumanReadableSize(filesize($sBackupFile));
+		$sConfirmRestore = addslashes(Dict::Format('bkp-confirm-restore', $sFileName));
+		$sFileEscaped = addslashes($sFilePath);
+		$sRestoreBtn = '<button class="restore" onclick="LaunchRestoreNow(\''.$sFileEscaped.'\', \''.$sConfirmRestore.'\');" '.$sDisableRestore.'>'.Dict::S('bkp-button-restore-now').'</button>';
+		$aDetails[] = array('file' => $sName, 'size' => $sSize, 'actions' => $sRestoreBtn);
+	}
+	$aConfig = array(
+		'file' => array('label' => Dict::S('bkp-table-file'), 'description' => Dict::S('bkp-table-file+')),
+		'size' => array('label' => Dict::S('bkp-table-size'), 'description' => Dict::S('bkp-table-size+')),
+		'actions' => array('label' => Dict::S('bkp-table-actions'), 'description' => Dict::S('bkp-table-actions+')),
+	);
+	$oP->add("<fieldset>");
+	$oP->add("<legend>".Dict::S('bkp-status-backups-manual')."</legend>");
+	if (count($aDetails) > 0)
+	{
+		$oP->add('<div style="max-height:400px; overflow: auto;">');
+		$oP->table($aConfig, array_reverse($aDetails));
+		$oP->add('</div>');
+	}
+	else
+	{
+		$oP->p(Dict::S('bkp-status-backups-none'));
+	}
+	$oP->add("</fieldset>");
+
+	// Ongoing operation ?
+	//
+	$oBackupMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
+	if ($oBackupMutex->TryLock())
+	{
+		$oBackupMutex->Unlock();
+	}
+	else
+	{
+		$oP->p(Dict::S('bkp-backup-running'));
+	}
+	$oRestoreMutex = new iTopMutex('restore.'.utils::GetCurrentEnvironment());
+	if ($oRestoreMutex->TryLock())
+	{
+		$oRestoreMutex->Unlock();
+	}
+	else
+	{
+		$oP->p(Dict::S('bkp-restore-running'));
+	}
+
+	// Do backup now
+	//
+	$oBackupExec = new BackupExec();
+	$oNext = $oBackupExec->GetNextOccurrence();
+	$oP->p(Dict::Format('bkp-next-backup', $aWeekDayToString[$oNext->Format('N')], $oNext->Format('Y-m-d'), $oNext->Format('H:i')));
+	$oP->p('<button onclick="LaunchBackupNow();">'.Dict::S('bkp-button-backup-now').'</button>');
+	$oP->add('<div id="backup_success" class="header_message message_ok" style="display: none;"></div>');
+	$oP->add('<div id="backup_errors" class="header_message message_error" style="display: none;"></div>');
+	$oP->add('<input type="hidden" name="restore_token" id="restore_token"/>');
+	
+	$sConfirmBackup = addslashes(Dict::S('bkp-confirm-backup'));
+	$sPleaseWaitBackup = addslashes(Dict::S('bkp-wait-backup'));
+	$sPleaseWaitRestore = addslashes(Dict::S('bkp-wait-restore'));
+	$sRestoreDone = addslashes(Dict::S('bkp-success-restore'));
+
+	$sMySQLBinDir = addslashes(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
+	$sDBHost = addslashes(MetaModel::GetConfig()->GetDBHost());
+	$sDBUser = addslashes(MetaModel::GetConfig()->GetDBUser());
+	$sDBPwd = addslashes(MetaModel::GetConfig()->GetDBPwd());
+	$sDBName = addslashes(MetaModel::GetConfig()->GetDBName());
+	$sDBSubName = addslashes(MetaModel::GetConfig()->GetDBSubName());
+
+	$sEnvironment = addslashes(utils::GetCurrentEnvironment());
+	
+	$oP->add_script(
+<<<EOF
+function LaunchBackupNow()
+{
+	$('#backup_success').hide();
+	$('#backup_errors').hide();
+
+	if (confirm('$sConfirmBackup'))
+	{
+		$.blockUI({ message: '<h1><img src="../images/indicator.gif" /> $sPleaseWaitBackup</h1>' });
+
+		var oParams = {};
+		oParams.operation = 'backup';
+		$.post(GetAbsoluteUrlModulePage('itop-backup', 'ajax.backup.php'), oParams, function(data){
+			if (data.search(/error|exceptio|notice|warning/i) != -1)
+			{
+				$('#backup_errors').html(data);
+				$('#backup_errors').show();
+			}
+			else
+			{
+				window.location.reload();
+			}
+			$.unblockUI();
+		});
+	}
+}
+function LaunchRestoreNow(sBackupFile, sConfirmationMessage)
+{
+	if (confirm(sConfirmationMessage))
+	{
+		$.blockUI({ message: '<h1><img src="../images/indicator.gif" /> $sPleaseWaitRestore</h1>' });
+
+		$('#backup_success').hide();
+		$('#backup_errors').hide();
+
+		var oParams = {};
+		oParams.operation = 'restore_get_token';
+		oParams.file = sBackupFile;
+		$.post(GetAbsoluteUrlModulePage('itop-backup', 'ajax.backup.php'), oParams, function(data){
+
+			// Get the value of restore_token
+			$('#backup_errors').append(data);
+
+			var oParams = {};
+			oParams.operation = 'restore_exec';
+			oParams.token = $("#restore_token").val();
+			oParams.mysql_bindir = '$sMySQLBinDir';
+			oParams.db_host = '$sDBHost';
+			oParams.db_user = '$sDBUser';
+			oParams.db_pwd = '$sDBPwd';
+			oParams.db_name = '$sDBName';
+			oParams.db_subname = '$sDBSubName';
+			oParams.environment = '$sEnvironment';
+			if (oParams.token.length > 0)
+			{
+				$.post(GetAbsoluteUrlModulePage('itop-backup', 'ajax.backup.php'), oParams, function(data){
+					if (data.search(/error|exceptio|notice|warning/i) != -1)
+					{
+						$('#backup_success').hide();
+						$('#backup_errors').html(data);
+						$('#backup_errors').show();
+					}
+					else
+					{
+						$('#backup_errors').hide();
+						$('#backup_success').html('$sRestoreDone');
+						$('#backup_success').show();
+					}
+					$.unblockUI();
+				});
+			}
+			else
+			{
+				$('button.restore').attr('disabled', 'disabled');
+				$.unblockUI();
+			}
+		});
+	}
+}
+EOF
+	);
+}
+catch(Exception $e)
+{
+	$oP->p('<b>'.$e->getMessage().'</b>');
+}
+
+$oP->output();
+?>