Zoidberg's son: Update system (#4534)

Zoidberg's son: Update system
pull/4571/head
Steve Clement 2019-05-01 18:24:41 +09:00 committed by GitHub
commit fc8f7982df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 917 additions and 47 deletions

View File

@ -308,11 +308,48 @@ class AdminShell extends AppShell
}
public function updateDatabase() {
echo 'Executing all updates to bring the database up to date with the current version.' . PHP_EOL;
$this->Server->runUpdates(true);
echo 'All updates completed.' . PHP_EOL;
$whoami = exec('whoami');
if ($whoami === 'httpd' || $whoami === 'www-data') {
echo 'Executing all updates to bring the database up to date with the current version.' . PHP_EOL;
$this->Server->runUpdates(true);
echo 'All updates completed.' . PHP_EOL;
} else {
die('This OS user is not allowed to run this command.'. PHP_EOL. 'Run it under `www-data` or `httpd`.' . PHP_EOL);
}
}
public function updateApp() {
$whoami = exec('whoami');
if ($whoami === 'httpd' || $whoami === 'www-data') {
$command = $this->args[0];
if (!empty($this->args[1])) {
$processId = $this->args[1];
$job = $this->Job->read(null, $processId);
} else { // create worker
$this->Job->create();
$job_data = array(
'worker' => 'prio',
'job_type' => 'update_app',
'job_input' => 'command: ' . $command,
'status' => 0,
'retries' => 0,
'org_id' => '',
'org' => '',
'message' => 'Updating.',
);
$this->Job->save($job_data);
$job = $this->Job->read(null, $this->Job->id);
}
$result = $this->Server->updateDatabase($command, false);
$job['Job']['progress'] = 100;
$job['Job']['message'] = 'Update done';
$this->Job->save($job);
} else {
die('This OS user is not allowed to run this command.'. PHP_EOL. 'Run it under `www-data` or `httpd`.' . PHP_EOL);
}
}
public function getAuthkey() {
if (empty($this->args[0])) {
echo 'Invalid parameters. Usage: ' . APP . 'Console/cake Admin getAuthkey [user_email]' . PHP_EOL;
@ -404,7 +441,7 @@ class AdminShell extends AppShell
}
echo 'Updated, new key:' . PHP_EOL . $authKey . PHP_EOL;
}
public function getOptionParser() {
$parser = parent::getOptionParser();
$parser->addSubcommand('updateJSON', array(

View File

@ -446,4 +446,5 @@ class ServerShell extends AppShell
$this->Task->id = $task['Task']['id'];
$this->Task->saveField('message', count($servers) . ' job(s) completed at ' . date('d/m/Y - H:i:s') . '.');
}
}

View File

@ -372,7 +372,7 @@ class AppController extends Controller
$this->Auth->logout();
throw new MethodNotAllowedException($message);//todo this should pb be removed?
} else {
$this->Flash->error('Warning: MISP is currently disabled for all users. Enable it in Server Settings (Administration -> Server Settings -> MISP tab -> live)', array('clear' => 1));
$this->Flash->error('Warning: MISP is currently disabled for all users. Enable it in Server Settings (Administration -> Server Settings -> MISP tab -> live). An update might also be in progress, you can see the progress in ' , array('params' => array('url' => $baseurl . '/servers/advancedUpdate/', 'urlName' => 'Advanced Update'), 'clear' => 1));
}
}
@ -835,7 +835,11 @@ class AppController extends Controller
}
$this->Server->updateDatabase($command);
$this->Flash->success('Done.');
$this->redirect(array('controller' => 'pages', 'action' => 'display', 'administration'));
if ($liveOff) {
$this->redirect(array('controller' => 'servers', 'action' => 'updateProgress'));
} else {
$this->redirect(array('controller' => 'pages', 'action' => 'display', 'administration'));
}
}
public function upgrade2324()

View File

@ -340,6 +340,7 @@ class ACLComponent extends Component
'getSubmoduleQuickUpdateForm' => array('perm_site_admin'),
'getVersion' => array('*'),
'index' => array('OR' => array('perm_sync', 'perm_admin')),
'ondemandAction' => array(),
'postTest' => array('perm_sync'),
'previewEvent' => array(),
'previewIndex' => array(),
@ -359,6 +360,7 @@ class ACLComponent extends Component
'testConnection' => array('perm_sync'),
'update' => array(),
'updateJSON' => array(),
'updateProgress' => array(),
'updateSubmodule' => array(),
'uploadFile' => array(),
'clearWorkerQueue' => array()

View File

@ -1545,6 +1545,69 @@ class ServersController extends AppController
}
}
public function ondemandAction()
{
if (!$this->_isSiteAdmin()) {
throw new MethodNotAllowedException('You are not authorised to do that.');
}
$this->AdminSetting = ClassRegistry::init('AdminSetting');
$actions = $this->Server->actions_description;
$default_fields = array(
'title' => '',
'description' => '',
'liveOff' => false,
'recommendBackup' => false,
'exitOnError' => false,
'requirements' => '',
'url' => '/'
);
foreach($actions as $id => $action) {
foreach($default_fields as $field => $value) {
if (!isset($action[$field])) {
$actions[$id][$field] = $value;
}
}
$done = $this->AdminSetting->getSetting($id);
$actions[$id]['done'] = ($done == '1');
}
$this->set('actions', $actions);
$this->set('updateLocked', $this->Server->isUpdateLocked());
}
public function updateProgress()
{
if (!$this->_isSiteAdmin()) {
throw new MethodNotAllowedException('You are not authorised to do that.');
}
$update_progress = $this->Server->getUpdateProgress();
$current_index = $update_progress['current'];
$current_command = !isset($update_progress['commands'][$current_index]) ? '' : $update_progress['commands'][$current_index];
$lookup_string = preg_replace('/\s{2,}/', '', substr($current_command, 0, -1));
$sql_info = $this->Server->query("SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST;");
if (empty($sql_info)) {
$update_progress['process_list'] = array();
} else {
// retreive current update process
foreach($sql_info as $row) {
if (preg_replace('/\s{2,}/', '', $row['PROCESSLIST']['INFO']) == $lookup_string) {
$sql_info = $row['PROCESSLIST'];
break;
}
}
$update_progress['process_list'] = array();
$update_progress['process_list']['STATE'] = isset($sql_info['STATE']) ? $sql_info['STATE'] : '';
$update_progress['process_list']['PROGRESS'] = isset($sql_info['PROGRESS']) ? $sql_info['PROGRESS'] : 0;
$update_progress['process_list']['STAGE'] = isset($sql_info['STAGE']) ? $sql_info['STAGE'] : 0;
$update_progress['process_list']['MAX_STAGE'] = isset($sql_info['MAX_STAGE']) ? $sql_info['MAX_STAGE'] : 0;
}
if ($this->request->is('ajax')) {
return $this->RestResponse->viewData(h($update_progress), $this->response->type());
} else {
$this->set('updateProgress', $update_progress);
}
}
public function getSubmoduleQuickUpdateForm($submodule_path=false) {
$this->set('submodule', base64_decode($submodule_path));
$this->render('ajax/submodule_quick_update_form');

View File

@ -78,6 +78,21 @@ class AppModel extends Model
33 => false, 34 => false
);
public $advanced_updates_description = array(
);
public $actions_description = array(
'verifyGnuPGkeys' => array(
'title' => 'Verify GnuPG keys',
'description' => "Run a full validation of all GnuPG keys within this instance's userbase. The script will try to identify possible issues with each key and report back on the results.",
'url' => '/users/verifyGPG/'
),
'databaseCleanupScripts' => array(
'title' => 'Database Cleanup Scripts',
'description' => 'If you run into an issue with an infinite upgrade loop (when upgrading from version ~2.4.50) that ends up filling your database with upgrade script log messages, run the following script.',
'url' => '/logs/pruneUpdateLogs/'
)
);
public function afterSave($created, $options = array())
{
if ($created) {
@ -209,8 +224,46 @@ class AppModel extends Model
}
// SQL scripts for updates
public function updateDatabase($command)
public function updateDatabase($command, $useWorker=true)
{
// Exit if updates are locked
if ($this->isUpdateLocked()) {
return false;
}
$this->__resetUpdateProgress();
// restart this function by a worker
if ($useWorker && Configure::read('MISP.background_jobs')) {
$job = ClassRegistry::init('Job');
$job->create();
$data = array(
'worker' => 'prio',
'job_type' => 'update_app',
'job_input' => 'command: ' . $command,
'status' => 0,
'retries' => 0,
'org_id' => '',
'org' => '',
'message' => 'Updating.',
);
$job->save($data);
$jobId = $job->id;
$process_id = CakeResque::enqueue(
'prio',
'AdminShell',
array('updateApp', $command, $jobId),
true
);
$job->saveField('process_id', $process_id);
return true;
}
$liveOff = false;
$exitOnError = false;
if (isset($advanced_updates_description[$command])) {
$liveOff = isset($advanced_updates_description[$command]['liveOff']) ? $advanced_updates_description[$command]['liveOff'] : $liveOff;
$exitOnError = isset($advanced_updates_description[$command]['exitOnError']) ? $advanced_updates_description[$command]['exitOnError'] : $exitOnError;
}
$dataSourceConfig = ConnectionManager::getDataSource('default')->config;
$dataSource = $dataSourceConfig['datasource'];
$sqlArray = array();
@ -1150,52 +1203,123 @@ class AppModel extends Model
return false;
break;
}
foreach ($sqlArray as $sql) {
$now = new DateTime();
$this->__changeLockState(time());
// switch MISP instance live to false
if ($liveOff) {
$this->Server = Classregistry::init('Server');
$liveSetting = 'MISP.live';
$this->Server->serverSettingsSaveValue($liveSetting, false);
}
$sql_update_count = count($sqlArray);
$index_update_count = count($indexArray);
$total_update_count = $sql_update_count + $index_update_count;
$this->__setUpdateProgress(0, $total_update_count);
$str_index_array = array();
foreach($indexArray as $toIndex) {
$str_index_array[] = __('Indexing ') . implode($toIndex, '->');
}
$this->__setUpdateCmdMessages(array_merge($sqlArray, $str_index_array));
$flag_stop = false;
$error_count = 0;
// execute test before update. Exit if it fails
if (isset($this->advanced_updates_description[$command]['preUpdate'])) {
$function_name = $this->advanced_updates_description[$command]['preUpdate'];
try {
$this->query($sql);
$this->Log->create();
$this->Log->save(array(
'org' => 'SYSTEM',
'model' => 'Server',
'model_id' => 0,
'email' => 'SYSTEM',
'action' => 'update_database',
'user_id' => 0,
'title' => 'Successfuly executed the SQL query for ' . $command,
'change' => 'The executed SQL query was: ' . $sql
));
$this->{$function_name}();
} catch (Exception $e) {
$this->Log->create();
$this->Log->save(array(
'org' => 'SYSTEM',
'model' => 'Server',
'model_id' => 0,
'email' => 'SYSTEM',
'action' => 'update_database',
'user_id' => 0,
'title' => 'Issues executing the SQL query for ' . $command,
'change' => 'The executed SQL query was: ' . $sql . PHP_EOL . ' The returned error is: ' . $e->getMessage()
));
$this->__setPreUpdateTestState(false);
$this->__setUpdateProgress(0, false);
$this->__setUpdateResMessages(0, __('Issues executing the pre-update test `') . $function_name . __('`. The returned error is: ') . PHP_EOL . $e->getMessage());
$this->__setUpdateError(0);
$error_count++;
$exitOnError = true;
$flag_stop = true;
}
}
if (!empty($indexArray)) {
if ($clean) {
$this->cleanCacheFiles();
}
foreach ($indexArray as $iA) {
if (isset($iA[2])) {
$this->__addIndex($iA[0], $iA[1], $iA[2]);
} else {
$this->__addIndex($iA[0], $iA[1]);
if (!$flag_stop) {
$this->__setPreUpdateTestState(true);
foreach ($sqlArray as $i => $sql) {
try {
$this->__setUpdateProgress($i, false);
$this->query($sql);
$this->Log->create();
$this->Log->save(array(
'org' => 'SYSTEM',
'model' => 'Server',
'model_id' => 0,
'email' => 'SYSTEM',
'action' => 'update_database',
'user_id' => 0,
'title' => __('Successfuly executed the SQL query for ') . $command,
'change' => __('The executed SQL query was: ') . $sql
));
$this->__setUpdateResMessages($i, __('Successfuly executed the SQL query for ') . $command);
} catch (Exception $e) {
$this->Log->create();
$this->Log->save(array(
'org' => 'SYSTEM',
'model' => 'Server',
'model_id' => 0,
'email' => 'SYSTEM',
'action' => 'update_database',
'user_id' => 0,
'title' => __('Issues executing the SQL query for ') . $command,
'change' => __('The executed SQL query was: ') . $sql . PHP_EOL . __(' The returned error is: ') . $e->getMessage()
));
$this->__setUpdateResMessages($i, __('Issues executing the SQL query for ') . $command . __('. The returned error is: ') . PHP_EOL . $e->getMessage());
$this->__setUpdateError($i);
$error_count++;
if ($exitOnError) {
$flag_stop = true;
break;
}
}
}
}
if (!$flag_stop) {
if (!empty($indexArray)) {
if ($clean) {
$this->cleanCacheFiles();
}
foreach ($indexArray as $i => $iA) {
$this->__setUpdateProgress(count($sqlArray)+$i, false);
if (isset($iA[2])) {
$this->__addIndex($iA[0], $iA[1], $iA[2]);
} else {
$this->__addIndex($iA[0], $iA[1]);
}
$this->__setUpdateResMessages(count($sqlArray)+$i, __('Successfuly indexed ') . implode($iA, '->'));
}
}
$this->__setUpdateProgress(count($sqlArray)+count($indexArray), false);
}
if ($clean) {
$this->cleanCacheFiles();
}
if ($liveOff) {
$liveSetting = 'MISP.live';
$this->Server->serverSettingsSaveValue($liveSetting, true);
}
if (!$flag_stop && $error_count == 0) {
$this->__postUpdate($command);
}
$this->__changeLockState(false);
return true;
}
// check whether the adminSetting should be updated after the update
private function __postUpdate($command) {
if (isset($this->advanced_updates_description[$command]['record'])) {
if($this->advanced_updates_description[$command]['record']) {
$this->AdminSetting->changeSetting($command, 1);
}
}
}
private function __dropIndex($table, $field)
{
$dataSourceConfig = ConnectionManager::getDataSource('default')->config;
@ -1390,6 +1514,126 @@ class AppModel extends Model
if ($requiresLogout) {
$this->updateDatabase('destroyAllSessions');
}
return true;
}
private function __setUpdateProgress($current, $total=false)
{
$updateProgress = $this->getUpdateProgress();
$updateProgress['current'] = $current;
if ($total !== false) {
$updateProgress['total'] = $total;
} else {
$now = new DateTime();
$updateProgress['time']['started'][$current] = $now->format('Y-m-d H:i:s');
}
$this->__saveUpdateProgress($updateProgress);
}
private function __setPreUpdateTestState($state)
{
$updateProgress = $this->getUpdateProgress();
$updateProgress['preTestSuccess'] = $state;
$this->__saveUpdateProgress($updateProgress);
}
private function __setUpdateError($index)
{
$updateProgress = $this->getUpdateProgress();
$updateProgress['failed_num'][] = $index;
$this->__saveUpdateProgress($updateProgress);
}
private function __resetUpdateProgress()
{
$updateProgress = array(
'commands' => array(),
'results' => array(),
'time' => array('started' => array(), 'elapsed' => array()),
'current' => '',
'total' => '',
'failed_num' => array()
);
$this->__saveUpdateProgress($updateProgress);
}
private function __setUpdateCmdMessages($messages)
{
$updateProgress = $this->getUpdateProgress();
$updateProgress['commands'] = $messages;
$this->__saveUpdateProgress($updateProgress);
}
private function __setUpdateResMessages($index, $message)
{
$updateProgress = $this->getUpdateProgress();
$updateProgress['results'][$index] = $message;
$temp = new DateTime();
$diff = $temp->diff(new DateTime($updateProgress['time']['started'][$index]));
$updateProgress['time']['elapsed'][$index] = $diff->format('%H:%I:%S');
$this->__saveUpdateProgress($updateProgress);
}
public function getUpdateProgress()
{
if (!isset($this->AdminSetting)) {
$this->AdminSetting = ClassRegistry::init('AdminSetting');
}
$updateProgress = $this->AdminSetting->getSetting('update_progress');
if ($updateProgress !== false) {
$updateProgress = json_decode($updateProgress, true);
} else {
$this->__resetUpdateProgress();
$updateProgress = $this->AdminSetting->getSetting('update_progress');
$updateProgress = json_decode($updateProgress, true);
}
foreach($updateProgress as $setting => $value) {
if (!is_array($value)) {
$value = $value !== false && $value !== '' ? intval($value) : 0;
}
$updateProgress[$setting] = $value;
}
return $updateProgress;
}
private function __saveUpdateProgress($updateProgress)
{
$data = json_encode($updateProgress);
$this->AdminSetting->changeSetting('update_progress', $data);
}
private function __changeLockState($locked)
{
$this->AdminSetting->changeSetting('update_locked', $locked);
}
private function getUpdateLockState()
{
if (!isset($this->AdminSetting)) {
$this->AdminSetting = ClassRegistry::init('AdminSetting');
}
$locked = $this->AdminSetting->getSetting('update_locked');
return is_null($locked) ? false : $locked;
}
public function isUpdateLocked()
{
$lockState = $this->getUpdateLockState();
if ($lockState !== false && $lockState !== '') {
// if lock is old, still allows the update
// This can be useful if the update process crashes
$diffSec = time() - intval($lockState);
if (Configure::read('MISP.updateTimeThreshold')) {
$updateWaitThreshold = intval(Configure::read('MISP.updateTimeThreshold'));
} else {
$this->Server = ClassRegistry::init('Server');
$updateWaitThreshold = intval($this->Server->serverSettings['MISP']['updateTimeThreshold']['value']);
}
if ($diffSec < $updateWaitThreshold) {
return true;
}
}
return false;
}
private function __queueCleanDB()

View File

@ -915,7 +915,15 @@ class Server extends AppModel
'test' => 'testBool',
'type' => 'boolean',
'null' => true
)
),
'updateTimeThreshold' => array(
'level' => 1,
'description' => __('Sets the minimum time before being able to re-trigger an update if the previous one failed. (safe guard to avoid starting the same update multiple time)'),
'value' => '7200',
'test' => 'testForNumeric',
'type' => 'numeric',
'null' => true
)
),
'GnuPG' => array(
'branch' => 1,

View File

@ -6,6 +6,13 @@
$message = str_replace('$flashErrorMessage', '<span class="useCursorPointer underline bold" onClick="flashErrorPopover();">here</span>', $message);
}
echo $message;
if (isset($params['url'])) {
if (isset($params['urlName'])) {
echo '<a href="' . h($params['url']) . '">' . h($params['urlName']) . '</a>';
} else {
echo '<a href="' . h($params['url']) . '">' . h($params['url']) . '</a>';
}
}
if ($this->Session->read('flashErrorMessage')) {
echo sprintf('<div class="hidden" id="flashErrorMessage">%s</div>', $this->element('flashErrorMessage', array('message' => $this->Session->read('flashErrorMessage'))));
}

View File

@ -60,6 +60,7 @@
</span><br />
<pre class="hidden green bold" id="gitResult"></pre>
<button title="<?php echo __('Pull the latest MISP version from github');?>" class="btn btn-inverse" style="padding-top:1px;padding-bottom:1px;" onClick = "updateMISP();"><?php echo __('Update MISP');?></button>
<a title="<?php echo __('Click the following button to go to the update progress page. This page lists all updates that are currently queued and executed.'); ?>" class="btn btn-inverse" style="padding-top:1px;padding-bottom:1px;" href="<?php echo $baseurl; ?>/servers/updateProgress/"><?php echo __('Update Progress');?></a>
</div>
<h3><?php echo __('Submodules version');?>
<it id="refreshSubmoduleStatus" class="fas fa-sync useCursorPointer" style="font-size: small; margin-left: 5px;" title="<?php echo __('Refresh submodules version.'); ?>"></it>
@ -361,12 +362,9 @@
</div><br />
<span class="btn btn-inverse" role="button" tabindex="0" aria-label="<?php echo __('Check for orphaned attribute');?>" title="<?php echo __('Check for orphaned attributes');?>" style="padding-top:1px;padding-bottom:1px;" onClick="checkOrphanedAttributes();"><?php echo __('Check for orphaned attributes');?></span><br /><br />
<?php echo $this->Form->postButton(__('Remove orphaned attributes'), $baseurl . '/attributes/pruneOrphanedAttributes', $options = array('class' => 'btn btn-primary', 'style' => 'padding-top:1px;padding-bottom:1px;')); ?>
<h3><?php echo __('Verify GnuPG keys');?></h3>
<p><?php echo __('Run a full validation of all GnuPG keys within this instance\'s userbase. The script will try to identify possible issues with each key and report back on the results.');?></p>
<span class="btn btn-inverse" onClick="location.href='<?php echo $baseurl;?>/users/verifyGPG';"><?php echo __('Verify GnuPG keys');?></span> (<?php echo __('Check whether every user\'s GnuPG key is usable');?>)</li>
<h3><?php echo __('Database cleanup scripts');?></h3>
<p><?php echo __('If you run into an issue with an infinite upgrade loop (when upgrading from version ~2.4.50) that ends up filling your database with upgrade script log messages, run the following script.');?></p>
<?php echo $this->Form->postButton(__('Prune upgrade logs'), $baseurl . '/logs/pruneUpdateLogs', $options = array('class' => 'btn btn-primary', 'style' => 'padding-top:1px;padding-bottom:1px;')); ?>
<h3><?php echo __('Administrator On-demand Action');?></h3>
<p><?php echo __('Click the following button to go to the Administrator On-demand Action page.');?></p>
<span class="btn btn-inverse" style="padding-top:1px;padding-bottom:1px;" onClick="location.href = '<?php echo $baseurl; ?>/servers/ondemandAction/';"><?php echo __('Administrator On-demand Action');?></span>
<h3><?php echo __('Legacy Administrative Tools');?></h3>
<p><?php echo __('Click the following button to go to the legacy administrative tools page. There should in general be no need to do this unless you are upgrading a very old MISP instance (<2.4), all updates are done automatically with more current versions.');?></p>
<span class="btn btn-inverse" style="padding-top:1px;padding-bottom:1px;" onClick="location.href = '<?php echo $baseurl; ?>/pages/display/administration';"><?php echo __('Legacy Administrative Tools');?></span>

View File

@ -0,0 +1,87 @@
<?php
if (!$isSiteAdmin) exit();
$disabledBtnText = $updateLocked ? 'title="' . __('An action is already in progress...') . '" disabled' : 'title=' . __('Action');
?>
<div class="index">
<h2><?php echo __('Administrator On-demand Action'); ?></h2>
<?php if ($updateLocked): ?>
<div class='alert alert-danger'>
<?php echo __('An action is already in progress. Starting new actions is not possible until completion of the current action process.'); ?>
</div>
<?php endif; ?>
<div style="margin-bottom: 10px;">
<a id="btnShowProgress" class="btn btn-inverse" href="<?php echo $baseurl; ?>/servers/updateProgress/"><?php echo __('Show Update Progress Page'); ?></a>
</div>
<?php $i = 0; ?>
<?php foreach($actions as $id => $action): ?>
<div class="headerUpdateBlock">
<h4><?php echo ($i+1) . '. ' . h($action['title']); ?></h4>
</div>
<div class="bodyUpdateBlock">
<h5><?php echo h($action['description']); ?></h5>
<?php if (!$action['done']): ?>
<?php if ($action['requirements'] !== ''): ?>
<div class="alert alert-warning">
<i class="icon-warning-sign"></i> <?php echo h($action['requirements']); ?>
</div>
<?php endif; ?>
<?php if ($action['recommendBackup']): ?>
<div class="alert alert-block">
<i class="icon-warning-sign"></i> <?php echo __('Running this script may take a very long time depending of the size of your database. It is adviced that you <b>back your database up</b> before running it.'); ?>
</div>
<?php endif; ?>
<?php if ($action['liveOff']): ?>
<div class="alert alert-info">
<i class="icon-question-sign"></i> <?php echo __('Running this script will make this instance unusable for all users (not site-admin) during the time of upgrade.'); ?>
</div>
<?php endif; ?>
<?php
$url_param = $action['liveOff'] ? '1' : '';
$url_param .= $action['exitOnError'] ? '/1' : '';
echo $this->Form->create(false, array( 'url' => $baseurl . $action['url'] . $url_param ));
?>
<button class="btn btn-warning <?php echo isset($action['redirectToUpdateProgress']) && $action['redirectToUpdateProgress'] ? 'submitButton' : 'submitButtonToUpdateProgress'; ?>" <?php echo $disabledBtnText; ?> role="button" tabindex="0" aria-label="<?php echo __('Submit'); ?>"><?php echo __('Action: ') . h($action['title']); ?></button>
<?php
echo $this->Form->end();
?>
<?php else: ?>
<div class="alert alert-success">
<i class="fa fa-check-square"></i> <?php echo __('This action has been done and cannot be run again.'); ?>
</div>
<?php endif; ?>
</div>
<?php $i++; ?>
<?php endforeach; ?>
</div>
<?php
echo $this->element('side_menu', array('menuList' => 'admin', 'menuItem' => 'adminTools'));
?>
<script type="text/javascript">
$(document).ready(function(){
$('.submitButtonToUpdateProgress').click(function() {
var form = $(this).closest("form");
$.ajax({
data: form.serialize(),
cache: false,
timeout: 100,
complete: function (data, textStatus) {
window.location.href = $('#btnShowProgress').prop('href');
},
type:"post",
url: form.prop('action')
});
});
});
</script>

View File

@ -0,0 +1,161 @@
<?php
if (!$isSiteAdmin) exit();
if ($updateProgress['total'] !== 0 ) {
$percentageFail = floor(count($updateProgress['failed_num']) / $updateProgress['total']*100);
$percentage = floor(($updateProgress['current']) / $updateProgress['total']*100);
} else {
$percentage = 100;
$percentageFail = 0;
}
if (isset($updateProgress['preTestSuccess']) && $updateProgress['preTestSuccess'] === false) {
$percentage = 0;
$percentageFail = 100;
}
?>
<div style="width: 50%;margin: 0 auto;">
<?php if (count($updateProgress['commands']) > 0): ?>
<h2><?php echo(__('Database Update progress'));?></h2>
<div class="" style="max-width: 1000px;">
<div>
<h5 style='display: inline-block'>Pre update test status:</h5>
<?php
$icon = isset($updateProgress['preTestSuccess']) ? ($updateProgress['preTestSuccess'] ? 'fa-check' : 'fa-times') : 'fa-question-circle ';
?>
<i class='fa <?php echo($icon); ?>' style="font-size: x-large"></i>
</div>
<div class="progress progress-striped" style="max-width: 1000px;">
<div id="pb-progress" class="bar" style="font-weight: bold; width: <?php echo h($percentage);?>%;"><?php echo h($percentage);?>%</div>
<div id="pb-fail" class="bar" style="width: <?php echo h($percentageFail);?>%; background-color: #ee5f5b;"></div>
</div>
<table class="table table-bordered table-stripped updateProgressTable">
<thead>
<tr>
<th></th>
<th>Update command</th>
</tr>
</thead>
<tbody>
<?php foreach($updateProgress['commands'] as $i => $cmd):
if (isset($updateProgress['results'][$i])) {
$res = $updateProgress['results'][$i];
} else {
$res = false;
}
$rowDone = $i < $updateProgress['current'];
$rowCurrent = $i === $updateProgress['current'];
$rowFail = in_array($i, $updateProgress['failed_num']);
$rowClass = '';
$rowIcon = '<i id="icon-' . $i . '" class="fa"></i>';
if ($rowDone) {
$rowClass = 'class="alert alert-success"';
$rowIcon = '<i id="icon-' . $i . '" class="fa fa-check-circle-o"></i>';
}
if ($rowCurrent && !$rowFail) {
$rowClass = 'class="alert alert-info"';
$rowIcon = '<i id="icon-' . $i . '" class="fa fa-cogs"></i>';
} else if ($rowFail) {
$rowClass = 'class="alert alert-danger"';
$rowIcon = '<i id="icon-' . $i . '" class="fa fa-times-circle-o"></i>';
}
if (isset($updateProgress['time']['started'][$i])) {
$datetimeStart = $updateProgress['time']['started'][$i];
if (isset($updateProgress['time']['elapsed'][$i])) {
$updateDuration = $updateProgress['time']['elapsed'][$i];
} else { // compute elapsed based on started
$temp = new DateTime();
$diff = $temp->diff(new DateTime($datetimeStart));
$updateDuration = $diff->format('%H:%I:%S');
}
} else {
$datetimeStart = '';
$updateDuration = '';
}
?>
<tr id="row-<?php echo $i; ?>" <?php echo $rowClass; ?> >
<td><?php echo $rowIcon; ?></td>
<td>
<div>
<a style="cursor: pointer; maring-bottom: 2px;" onclick="toggleVisiblity(<?php echo $i;?>)">
<span class="foldable fa fa-terminal"></span>
<?php echo __('Update ') . ($i+1); ?>
<span class="inline-term"><?php echo h(substr($cmd, 0, 60)) . (strlen($cmd) > 60 ? '[...]' : '' );?></span>
<span class="label">
<?php echo __('Started @ '); ?>
<span id="startedTime-<?php echo $i; ?>"><?php echo h($datetimeStart); ?></span>
</span>
<span class="label">
<?php echo __('Elapsed Time @ '); ?>
<span id="elapsedTime-<?php echo $i; ?>"><?php echo h($updateDuration); ?></span>
</span>
</a>
<div data-terminalid="<?php echo $i;?>" style="display: none; margin-top: 5px;">
<div id="termcmd-<?php echo $i;?>" class="div-terminal">
<?php
$temp = preg_replace('/^\n*\s+/', '', $cmd);
$temp = preg_split('/\s{4,}/m', $temp);
foreach ($temp as $j => $line) {
$pad = $j > 0 ? '30' : '0';
if ($line !== '') {
echo '<span style="margin-left: ' . $pad . 'px;">' . h($line) . '</span>';
}
}
?>
</div>
<div>
<span class="fa fa-level-up terminal-res-icon"></span>
<div id="termres-<?php echo $i;?>" class="div-terminal terminal-res">
<?php
if ($res !== false) {
$temp = preg_replace('/^\n*\s+/', '', $res);
$temp = preg_split('/\s{2,}/m', $temp);
foreach ($temp as $j => $line) {
$pad = $j > 0 ? '30' : '0';
if ($line !== '') {
echo '<span style="margin-left: ' . $pad . 'px;">' . h($line) . '</span>';
}
}
}
?>
</div>
</div>
</div>
</div>
<div id="single-update-progress-<?php echo $i;?>" class="single-update-progress hidden">
<div class="small-pb-in-td">
<div id="single-update-pb-<?php echo $i;?>" style="height: 100%; background: #149bdf; transition: width 0.6s ease;"></div>
</div>
<div id="small-state-text-<?php echo $i;?>" class="small-state-text-in-td badge" class="badge">Filling schema table</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<h2><?php echo __('No update in progress'); ?></h2>
<script>
$(document).ready(function() {
setInterval(function() { location.reload(); }, 1000);
});
</script>
<?php endif; ?>
<script>
var updateProgress = <?php echo json_encode($updateProgress); ?>;
var urlGetProgress = "<?php echo $baseurl; ?>/servers/updateProgress";
</script>
<?php
echo $this->element('genericElements/assetLoader', array(
'css' => array('update_progress'),
'js' => array('update_progress')
));
?>
</div>

View File

@ -0,0 +1,76 @@
.headerUpdateBlock {
border: #ccc solid 1px;
border-bottom: 1px solid transparent;
background: #e6e6e6;
padding: 0px 6px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.bodyUpdateBlock {
border: #ccc solid 1px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
background: #fbfbfb;
padding: 6px;
margin-bottom: 10px;
}
.div-terminal {
background-color: #303030;
color: white;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
padding: 10px;
border-radius: 5px;
}
.div-terminal > span {
display: block;
}
.terminal-res {
margin-top: 5px;
margin-left: 25px;
max-height: 500px;
overflow-y: auto;
}
.terminal-res-icon {
position: absolute;
transform: rotate(90deg);
font-size: large;
margin-right: 5px;
margin-left: 5px;
}
table.updateProgressTable > tbody > tr > td:first-child {
width: 40px;
text-align: center;
}
table.updateProgressTable .small-pb-in-td {
width: calc(100% + 16px);
height: 4px;
position: relative;
left: -8px;
top: 26px;
background-color: #f3f3f3;
}
table.updateProgressTable .small-state-text-in-td {
position: relative;
display: inline-block;
float: right;
}
.back-and-forth-animation {
position: relative;
animation: backandforthAnim 2.0s infinite;
}
@keyframes backandforthAnim {
0% {left: 0;}
50% {left: calc(95%);}
100% {left: 0;}
}

View File

@ -0,0 +1,182 @@
function toggleVisiblity(termId, auto, show) {
var term = $('div[data-terminalid='+termId+']')
if (auto === true) {
if (term.data('manual') !== true) { // show if manual is not set
if (show === true) {
term.show();
} else if (show === false) {
term.hide();
} else {
term.toggle();
}
}
} else {
term.data('manual', true);
if (show === true) {
term.show();
} else if (show === false) {
term.hide();
} else {
term.toggle();
}
}
}
var pooler;
var poolerInterval = 3000;
$(document).ready(function() {
pooler = setInterval(function() { update_state(); }, poolerInterval);
});
function update_state() {
$.getJSON(urlGetProgress, function(data) {
var total = parseInt(data['total']);
var current = parseInt(data['current']);
var failArray = data['failed_num'];
for (var i=0; i<total; i++) {
var term = $('div[data-terminalid='+i+']')
toggleVisiblity(i, true, false);
if (i < current) {
if (failArray.indexOf(String(i)) != -1) {
update_row_state(i, 2);
} else {
update_row_state(i, 0);
}
} else if (i == current) {
if (failArray.indexOf(String(i)) != -1) {
update_row_state(i, 2);
toggleVisiblity(i, true, true);
} else {
update_row_state(i, 1);
toggleVisiblity(i-1, true, true);
}
update_single_update_progress(i, data);
} else {
update_row_state(i, 3);
}
}
update_messages(data);
if (total > 0) {
var percFail = Math.round(failArray.length/total*100);
var perc = Math.round(current/total*100);
update_pb(perc, percFail);
}
if ((current+1) >= total || failArray.indexOf(current) != -1) {
clearInterval(pooler);
$('.single-update-progress').hide();
}
});
}
function update_messages(messages) {
if (messages.commands === undefined) {
return;
}
messages.commands.forEach(function(msg, i) {
var div = $('#termcmd-'+i);
create_spans_from_message(div, msg);
});
messages.results.forEach(function(msg, i) {
var div = $('#termres-'+i);
div.css('display', '');
create_spans_from_message(div, msg);
});
messages.time.started.forEach(function(startedText, i) {
var elapsedText = messages.time.elapsed[i];
if (elapsedText === undefined) {
var diff = new Date((new Date()).getTime() - (new Date(startedText)).getTime());
elapsedText = pad(diff.getUTCHours(), 2)
+ ':' + pad(diff.getUTCMinutes(), 2)
+ ':' + pad(diff.getUTCSeconds(), 2);
}
update_times(i, startedText, elapsedText)
});
}
function create_spans_from_message(toAppendto, msg) {
toAppendto.empty();
// create span for each line of text
msg = msg.replace(/^\n*\s+/, '');
var lines = msg.split(/\s{2,}/m)
lines.forEach(function(line, j) {
var pad = j > 0 ? '30' : '0';
if (line !== '') {
var span = $('<span style="margin-left: ' + pad + 'px;">' + line + '</span>');
toAppendto.append(span);
}
});
}
function update_row_state(i, state) {
var icon = $('#icon-'+i);
var row = $('#row-'+i);
switch(state) {
case 0: // success
row.removeClass('alert-danger alert-info');
row.addClass('alert-success');
icon.removeClass('fa-times-circle-o fa-cogs');
icon.addClass('fa-check-circle-o');
break;
case 1: // current
row.removeClass('alert-success alert-danger');
row.addClass('alert-info');
icon.removeClass('fa-check-circle-o', 'fa-times-circle-o');
icon.addClass('fa-cogs');
break;
case 2: //fail
row.removeClass('alert-success alert-info');
row.addClass('alert-danger');
icon.removeClass('fa-check-circle-o fa-cogs');
icon.addClass('fa-times-circle-o');
break;
case 3: //no state
default:
row.removeClass('alert-success alert-info alert-danger');
icon.removeClass('fa-check-circle-o fa-times-circle-o fa-cogs');
break;
}
}
function update_pb(perc, percFail) {
var pb = $('#pb-progress');
pb.css('width', perc+'%');
pb.text(perc+'%');
var pbF = $('#pb-fail');
pbF.css('width', percFail+'%');
}
function update_times(i, startedText, elapsedText) {
var started = $('#startedTime-'+i);
var elapsed = $('#elapsedTime-'+i);
started.text(startedText);
elapsed.text(elapsedText);
}
function update_single_update_progress(i, data) {
$('.single-update-progress').hide();
var div = $('#single-update-progress-'+i);
var pb = div.find('#single-update-pb-'+i);
var state = div.find('#small-state-text-'+i);
div.show();
var perc = parseInt(data['process_list']['PROGRESS']);
if (data['process_list']['MAX_STAGE'] == 0) { // if MAX_STAGE == 0, progress could not be determined
perc = 5;
if (data['failed_num'].indexOf(data['current']) >= 0) { // do not animate if failed
state.text('Failed');
perc = 0;
} else {
state.text('Unkown or No state');
pb.addClass('back-and-forth-animation');
}
} else {
perc = perc == 0 ? 1 : perc; // for UI, always set min progress to 1
pb.removeClass('back-and-forth-animation');
state.text(data['process_list']['STATE']);
}
pb.css('width', perc+'%');
}
function pad(num, size){ return ('000000000' + num).substr(-size); }