Ensure that Utilities(Backup/Restore/Maintenence/Import-Export) should not be started

if binary path is wrong and also added 'Stop Process' button to cancel the process.
pull/14/head
Akshay Joshi 2018-10-22 12:35:21 +05:30
parent 370df47042
commit 6bc6bc7f60
49 changed files with 731 additions and 101 deletions

View File

@ -113,6 +113,8 @@ When youve specified the details that will be incorporated into the pg_dump c
.. image:: images/backup_messages.png
:alt: Backup success notification popup
Use the **Stop Process** button to stop the Backup process.
If the backup is successful, a popup window will confirm success. Click *Click here for details* on the popup window to launch the *Process Watcher*. The *Process Watcher* logs all the activity associated with the backup and provides additional information for troubleshooting.
.. image:: images/backup_process_watcher.png

View File

@ -24,6 +24,8 @@ Click the *Backup* button to build and execute a command based on your selection
.. image:: images/backup_globals_messages.png
:alt: Backup globals success notification popup
Use the **Stop Process** button to stop the Backup process.
If the backup is successful, a popup window will confirm success. Click *Click here for details* on the popup window to launch the *Process Watcher*. The *Process Watcher* logs all the activity associated with the backup and provides additional information for troubleshooting.
.. image:: images/backup_globals_process_watcher.png

View File

@ -78,6 +78,8 @@ Click the *Backup* button to build and execute a command based on your selection
.. image:: images/backup_server_messages.png
:alt: Backup server success notification popup
Use the **Stop Process** button to stop the Backup process.
If the backup is successful, a popup window will confirm success. Click *Click here for details* on the popup window to launch the *Process Watcher*. The *Process Watcher* logs all the activity associated with the backup and provides additional information for troubleshooting.
.. image:: images/backup_server_process_watcher.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 259 KiB

View File

@ -53,6 +53,8 @@ After completing the *Import/Export data* dialog, click the *OK* button to perfo
.. image:: images/import_export_complete.png
:alt: Import Export data completion notification
Use the **Stop Process** button to stop the Import/Export process.
Use the *Click here for details* link on the notification to open the *Process Watcher* and review detailed information about the execution of the command that performed the import or export:
.. image:: images/import_export_pw.png

View File

@ -34,6 +34,8 @@ pgAdmin will inform you when the background process completes:
.. image:: images/maintenance_complete.png
:alt: Maintenance completion notification
Use the **Stop Process** button to stop the Maintenance process.
Use the *Click here for details* link on the notification to open the *Process Watcher* and review detailed information about the execution of the command that performed the import or export:
.. image:: images/maintenance_pw.png

View File

@ -17,6 +17,7 @@ Features
Bug fixes
*********
| `Bug #3232 <https://redmine.postgresql.org/issues/3232>`_ - Ensure that Utilities(Backup/Restore/Maintenence/Import-Export) should not be started if binary path is wrong and also added 'Stop Process' button to cancel the process.
| `Bug #3638 <https://redmine.postgresql.org/issues/3638>`_ - Fix syntax error when creating new pgAgent schedules with a start date/time and exception.
| `Bug #3674 <https://redmine.postgresql.org/issues/3674>`_ - Cleanup session files periodically.
| `Bug #3660 <https://redmine.postgresql.org/issues/3660>`_ - Rename the 'SQL Editor' section of the Preferences to 'Query Tool' as it applies to the whole tool, not just the editor.

View File

@ -84,6 +84,8 @@ When youve specified the details that will be incorporated into the pg_restor
.. image:: images/restore_messages.png
:alt: Restore dialog notifications
Use the **Stop Process** button to stop the Restore process.
Click *Click here for details* on the popup to launch the *Process Watcher*. The *Process Watcher* logs all the activity associated with the restore, and provides additional information for troubleshooting should the restore command encounter problems.
.. image:: images/restore_process_watcher.png

View File

@ -28,6 +28,7 @@ speaklater==1.3
sqlparse==0.2.4
WTForms==2.1
Flask-Paranoid==0.2.0
psutil==5.4.7
################################################################
# Modules specifically requires for Python2.7 or greater version

View File

@ -0,0 +1,34 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2018, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
""" Added utility pid to stop process
Revision ID: ece2e76bf60e
Revises: ca00ec32581b
Create Date: 2018-10-18 14:45:13.483068
"""
from pgadmin.model import db
# revision identifiers, used by Alembic.
revision = 'ece2e76bf60e'
down_revision = 'ca00ec32581b'
branch_labels = None
depends_on = None
def upgrade():
db.engine.execute(
'ALTER TABLE process ADD COLUMN utility_pid INTEGER'
)
def downgrade():
pass

View File

@ -44,7 +44,8 @@ class BGProcessModule(PgAdminModule):
"""
return [
'bgprocess.status', 'bgprocess.detailed_status',
'bgprocess.acknowledge', 'bgprocess.list'
'bgprocess.acknowledge', 'bgprocess.list',
'bgprocess.stop_process'
]
@ -104,3 +105,18 @@ def acknowledge(pid):
return success_return()
except LookupError as lerr:
return gone(errormsg=str(lerr))
@blueprint.route('/stop/<pid>', methods=['PUT'], endpoint='stop_process')
@login_required
def stop_process(pid):
"""
User has stopped the process
:param pid: Process ID
"""
try:
BatchProcess.stop_process(pid)
return success_return()
except LookupError as lerr:
return gone(errormsg=str(lerr))

View File

@ -321,6 +321,14 @@ def execute():
process = Popen(
command, stdout=PIPE, stderr=PIPE, stdin=None, **kwargs
)
args.update({
'start_time': get_current_time(),
'stdout': process_stdout.log,
'stderr': process_stderr.log,
'pid': process.pid
})
update_status(**args)
_log('Status updated after starting child process...')
_log('Attaching the loggers to stdout, and stderr...')
# Attach the stream to the process logger, and start logging.

View File

@ -14,6 +14,7 @@ Introduce a function to run the process executor in detached mode.
import csv
import os
import sys
import psutil
from abc import ABCMeta, abstractproperty, abstractmethod
from datetime import datetime
from pickle import dumps, loads
@ -523,6 +524,10 @@ class BatchProcess(object):
if 'end_time' in data and data['end_time']:
p.end_time = data['end_time']
# get the pid of the utility.
if 'pid' in data:
p.utility_pid = data['pid']
return True, True
except ValueError as e:
@ -657,3 +662,26 @@ class BatchProcess(object):
if 'env' in kwargs:
self.env.update(kwargs['env'])
@staticmethod
def stop_process(_pid):
"""
"""
p = Process.query.filter_by(
user_id=current_user.id, pid=_pid
).first()
if p is None:
raise LookupError(
_("Could not find a process with the specified ID.")
)
try:
process = psutil.Process(p.utility_pid)
process.terminate()
except psutil.Error as e:
current_app.logger.warning(
_("Unable to kill the background process '{0}'").format(
p.utility_pid)
)
current_app.logger.exception(e)

View File

@ -95,6 +95,10 @@ define('misc.bgprocess', [
return url_for('bgprocess.acknowledge', {
'pid': this.id,
});
case 'stop_process':
return url_for('bgprocess.stop_process', {
'pid': this.id,
});
default:
return url_for('bgprocess.list');
}
@ -258,7 +262,22 @@ define('misc.bgprocess', [
$('<div></div>', {
class: 'pg-bg-start',
}).append(
$('<div></div>').text(self.stime.toString())
$('<div></div>', {
class: 'row align-items-center',
}).append(
$('<div></div>', {
class: 'col-9',
}).text(self.stime.toString())
).append(
$('<div></div>', {
class: 'col-3',
}).append(
$('<button></button>', {
type: 'button',
class: 'btn btn-danger btn-sm float-right bg-process-stop',
}).text('Stop Process')
)
)
).append(
$('<div class="pg-bg-etime"></div>')
)
@ -313,6 +332,9 @@ define('misc.bgprocess', [
return;
});
// On Click event to stop the process.
content.find('.bg-process-stop').off('click').on('click', self.stop_process.bind(this));
}
// TODO:: Formatted execution time
self.container.find('.pg-bg-etime').empty().append(
@ -332,6 +354,14 @@ define('misc.bgprocess', [
} else if (self.exit_code == 1) {
$status_bar.addClass('bg-failed');
}
// Enable/Disable stop process button
var $btn_stop_process = $(self.container.find('.bg-process-stop'));
if (isNaN(parseInt(self.exit_code))) {
$btn_stop_process.removeClass('disabled');
} else {
$btn_stop_process.addClass('disabled');
}
} else {
self.show_detailed_view.apply(self);
}
@ -359,7 +389,18 @@ define('misc.bgprocess', [
),
$logs = container.find('.bg-process-watcher'),
$header = container.find('.bg-process-details'),
$footer = container.find('.bg-process-footer');
$footer = container.find('.bg-process-footer'),
$btn_stop_process = container.find('.bg-process-stop');
// Enable/Disable stop process button
if (isNaN(parseInt(self.exit_code))) {
$btn_stop_process.removeClass('disabled');
} else {
$btn_stop_process.addClass('disabled');
}
// On Click event to stop the process.
$btn_stop_process.off('click').on('click', self.stop_process.bind(this));
if (is_new) {
// set logs
@ -439,6 +480,25 @@ define('misc.bgprocess', [
console.warn(arguments);
});
},
stop_process: function() {
var self = this;
$.ajax({
type: 'PUT',
timeout: 30000,
url: self.bgprocess_url('stop_process'),
cache: false,
async: true,
contentType: 'application/json',
})
.done(function() {
return;
})
.fail(function() {
console.warn(arguments);
});
},
});
_.extend(
@ -538,10 +598,18 @@ define('misc.bgprocess', [
isPrivate: true,
content: '<div class="bg-process-details">' +
'<p class="bg-detailed-desc"></p>' +
'<div class="bg-process-stats">' +
'<span><b>' + gettext('Start time') + ': </b>' +
'<span class="bgprocess-start-time"></span>' +
'</span></div>' +
'<div class="bg-process-stats" style="padding-bottom: 5px">' +
'<div class="row align-items-center">' +
'<div class="col-9">' +
'<span><b>' + gettext('Start time') + ': </b>' +
'<span class="bgprocess-start-time"></span>' +
'</span>' +
'</div>' +
'<div class="col-3">' +
'<button type="button" class="btn btn-danger btn-sm float-right bg-process-stop">' +
gettext('Stop Process') + '</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="bg-process-watcher">' +
'</div>' +

View File

@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy
#
##########################################################################
SCHEMA_VERSION = 19
SCHEMA_VERSION = 20
##########################################################################
#
@ -258,6 +258,7 @@ class Process(db.Model):
end_time = db.Column(db.String(), nullable=True)
exit_code = db.Column(db.Integer(), nullable=True)
acknowledge = db.Column(db.String(), nullable=True)
utility_pid = db.Column(db.Integer, nullable=False)
class Keys(db.Model):

View File

@ -19,7 +19,7 @@ from flask_babelex import gettext as _
from flask_security import login_required, current_user
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
from pgadmin.utils import PgAdminModule, get_storage_directory, html, \
fs_short_path, document_dir
fs_short_path, document_dir, is_utility_exists
from pgadmin.utils.ajax import make_json_response, bad_request
from config import PG_DEFAULT_DRIVER
@ -63,7 +63,8 @@ class BackupModule(PgAdminModule):
Returns:
list: URL endpoints for backup module
"""
return ['backup.create_server_job', 'backup.create_object_job']
return ['backup.create_server_job', 'backup.create_object_job',
'backup.utility_exists']
# Create blueprint for BackupModule class
@ -320,6 +321,13 @@ def create_backup_objects_job(sid):
utility = manager.utility('backup') if backup_obj_type == 'objects' \
else manager.utility('backup_server')
ret_val = is_utility_exists(utility)
if ret_val:
return make_json_response(
success=0,
errormsg=ret_val
)
args = [
'--file',
backup_file,
@ -461,3 +469,44 @@ def create_backup_objects_job(sid):
return make_json_response(
data={'job_id': jid, 'Success': 1}
)
@blueprint.route(
'/utility_exists/<int:sid>/<backup_obj_type>', endpoint='utility_exists'
)
@login_required
def check_utility_exists(sid, backup_obj_type):
"""
This function checks the utility file exist on the given path.
Args:
sid: Server ID
backup_obj_type: Type of the object
Returns:
None
"""
server = Server.query.filter_by(
id=sid, user_id=current_user.id
).first()
if server is None:
return make_json_response(
success=0,
errormsg=_("Could not find the specified server.")
)
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(server.id)
utility = manager.utility('backup') if backup_obj_type == 'objects' \
else manager.utility('backup_server')
ret_val = is_utility_exists(utility)
if ret_val:
return make_json_response(
success=0,
errormsg=ret_val
)
return make_json_response(success=1)

View File

@ -10,6 +10,8 @@
import gettext from '../../../../static/js/gettext';
import Backform from '../../../../static/js/backform.pgadmin';
import {Dialog} from '../../../../static/js/alertify/dialog';
import url_for from 'sources/url_for';
import axios from 'axios/index';
export class BackupDialog extends Dialog {
constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
@ -19,6 +21,13 @@ export class BackupDialog extends Dialog {
);
}
url_for_utility_exists(id, params){
return url_for('backup.utility_exists', {
'sid': id,
'backup_obj_type': params == null ? 'objects' : 'servers',
});
}
draw(action, aciTreeItem, params) {
const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
@ -30,17 +39,40 @@ export class BackupDialog extends Dialog {
return;
}
const typeOfDialog = BackupDialog.typeOfDialog(params);
const baseUrl = this.url_for_utility_exists(serverInformation._id, params);
// Check pg_dump or pg_dumpall utility exists or not.
let that = this;
let service = axios.create({});
service.get(
baseUrl
).then(function(res) {
if (!res.data.success) {
that.alertify.alert(
gettext('Utility not found'),
res.data.errormsg
);
return;
}
if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
const typeOfDialog = BackupDialog.typeOfDialog(params);
if (!that.canExecuteOnCurrentDatabase(aciTreeItem)) {
return;
}
const dialog = that.createOrGetDialog(
BackupDialog.dialogTitle(typeOfDialog),
typeOfDialog
);
dialog(true).resizeTo('60%', '50%');
}).catch(function() {
that.alertify.alert(
gettext('Utility not found'),
gettext('Failed to fetch Utility information')
);
return;
}
const dialog = this.createOrGetDialog(
BackupDialog.dialogTitle(typeOfDialog),
typeOfDialog
);
dialog(true).resizeTo('60%', '50%');
});
}
static typeOfDialog(params) {

View File

@ -147,9 +147,16 @@ export class BackupDialogWrapper extends DialogWrapper {
service.post(
baseUrl,
this.view.model.toJSON()
).then(function () {
dialog.alertify.success(gettext('Backup job created.'), 5);
dialog.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialog);
).then(function (res) {
if (res.data.success) {
dialog.alertify.success(gettext('Backup job created.'), 5);
dialog.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialog);
} else {
dialog.alertify.alert(
gettext('Backup job creation failed.'),
res.data.errormsg
);
}
}).catch(function (error) {
try {
const err = error.response.data;

View File

@ -10,10 +10,11 @@
import sys
import simplejson as json
import os
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from pgadmin.utils import server_utils as server_utils
from pgadmin.utils import server_utils as server_utils, is_utility_exists
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
@ -639,13 +640,22 @@ class BackupCreateJobTest(BaseTestGenerator):
]
def setUp(self):
if self.server['default_binary_paths'] is None:
if 'default_binary_paths' not in self.server or \
self.server['type'] not in self.server['default_binary_paths'] or \
self.server['default_binary_paths'][self.server['type']] == '':
self.skipTest(
"default_binary_paths is not set for the server {0}".format(
self.server['name']
)
)
binary_path = os.path.join(
self.server['default_binary_paths'][self.server['type']],
'pg_dump')
retVal = is_utility_exists(binary_path)
if retVal is not None:
self.skipTest(retVal)
@patch('pgadmin.tools.backup.Server')
@patch('pgadmin.tools.backup.BackupMessage')
@patch('pgadmin.tools.backup.filename_with_file_manager_path')

View File

@ -11,6 +11,7 @@ import os
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from pgadmin.utils import is_utility_exists
import pgadmin.tools.backup.tests.test_backup_utils as backup_utils
@ -38,13 +39,22 @@ class BackupJobTest(BaseTestGenerator):
]
def setUp(self):
if self.server['default_binary_paths'] is None:
if 'default_binary_paths' not in self.server or \
self.server['type'] not in self.server['default_binary_paths'] or\
self.server['default_binary_paths'][self.server['type']] == '':
self.skipTest(
"default_binary_paths is not set for the server {0}".format(
self.server['name']
)
)
binary_path = os.path.join(
self.server['default_binary_paths'][self.server['type']],
'pg_dump')
retVal = is_utility_exists(binary_path)
if retVal is not None:
self.skipTest(retVal)
def runTest(self):
self.server_id = parent_node_dict["server"][-1]["server_id"]
url = self.url.format(self.server_id)

View File

@ -17,7 +17,7 @@ from flask_babelex import gettext as _
from flask_security import login_required, current_user
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
from pgadmin.utils import PgAdminModule, get_storage_directory, html, \
fs_short_path, document_dir, IS_WIN
fs_short_path, document_dir, IS_WIN, is_utility_exists
from pgadmin.utils.ajax import make_json_response, bad_request
from config import PG_DEFAULT_DRIVER
@ -58,7 +58,7 @@ class ImportExportModule(PgAdminModule):
Returns:
list: URL endpoints for backup module
"""
return ['import_export.create_job']
return ['import_export.create_job', 'import_export.utility_exists']
blueprint = ImportExportModule(MODULE_NAME, __name__)
@ -231,6 +231,12 @@ def create_import_export_job(sid):
# Get the utility path from the connection manager
utility = manager.utility('sql')
ret_val = is_utility_exists(utility)
if ret_val:
return make_json_response(
success=0,
errormsg=ret_val
)
# Get the storage path from preference
storage_dir = get_storage_directory()
@ -323,3 +329,41 @@ def create_import_export_job(sid):
return make_json_response(
data={'job_id': jid, 'success': 1}
)
@blueprint.route(
'/utility_exists/<int:sid>', endpoint='utility_exists'
)
@login_required
def check_utility_exists(sid):
"""
This function checks the utility file exist on the given path.
Args:
sid: Server ID
Returns:
None
"""
server = Server.query.filter_by(
id=sid, user_id=current_user.id
).first()
if server is None:
return make_json_response(
success=0,
errormsg=_("Could not find the specified server.")
)
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(server.id)
utility = manager.utility('sql')
ret_val = is_utility_exists(utility)
if ret_val:
return make_json_response(
success=0,
errormsg=ret_val
)
return make_json_response(success=1)

View File

@ -534,6 +534,11 @@ Backform, commonUtils, supportedNodes
if (res.success) {
Alertify.success(gettext('Import/export job created.'), 5);
pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
} else {
Alertify.alert(
gettext('Import/export job creation failed.'),
res.errormsg
);
}
})
.fail(function(xhr) {
@ -652,12 +657,38 @@ Backform, commonUtils, supportedNodes
});
}
// Open the Alertify dialog for the import/export module
Alertify.ImportDialog(
S(
gettext('Import/Export data - table \'%s\'')
).sprintf(treeInfo.table.label).value(), node, i, d
).set('resizable', true).resizeTo('70%', '80%');
const baseUrl = url_for('import_export.utility_exists', {
'sid': server_data._id,
});
// Check psql utility exists or not.
$.ajax({
url: baseUrl,
type:'GET',
})
.done(function(res) {
if (!res.success) {
Alertify.alert(
gettext('Utility not found'),
res.errormsg
);
return;
}
// Open the Alertify dialog for the import/export module
Alertify.ImportDialog(
S(
gettext('Import/Export data - table \'%s\'')
).sprintf(treeInfo.table.label).value(), node, i, d
).set('resizable', true).resizeTo('70%', '80%');
})
.fail(function() {
Alertify.alert(
gettext('Utility not found'),
gettext('Failed to fetch Utility information')
);
return;
});
},
};

View File

@ -13,9 +13,9 @@ import simplejson as json
from flask import url_for, Response, render_template, request, current_app
from flask_babelex import gettext as _
from flask_security import login_required
from flask_security import login_required, current_user
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
from pgadmin.utils import PgAdminModule, html
from pgadmin.utils import PgAdminModule, html, is_utility_exists
from pgadmin.utils.ajax import bad_request, make_json_response
from pgadmin.utils.driver import get_driver
@ -68,7 +68,7 @@ class MaintenanceModule(PgAdminModule):
Returns:
list: URL endpoints for backup module
"""
return ['maintenance.create_job']
return ['maintenance.create_job', 'maintenance.utility_exists']
blueprint = MaintenanceModule(MODULE_NAME, __name__)
@ -214,6 +214,12 @@ def create_maintenance_job(sid, did):
)
utility = manager.utility('sql')
ret_val = is_utility_exists(utility)
if ret_val:
return make_json_response(
success=0,
errormsg=ret_val
)
# Create the command for the vacuum operation
query = render_template(
@ -262,3 +268,41 @@ def create_maintenance_job(sid, did):
data={'job_id': jid, 'status': True,
'info': 'Maintenance job created.'}
)
@blueprint.route(
'/utility_exists/<int:sid>', endpoint='utility_exists'
)
@login_required
def check_utility_exists(sid):
"""
This function checks the utility file exist on the given path.
Args:
sid: Server ID
Returns:
None
"""
server = Server.query.filter_by(
id=sid, user_id=current_user.id
).first()
if server is None:
return make_json_response(
success=0,
errormsg=_("Could not find the specified server.")
)
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(server.id)
utility = manager.utility('sql')
ret_val = is_utility_exists(utility)
if ret_val:
return make_json_response(
success=0,
errormsg=ret_val
)
return make_json_response(success=1)

View File

@ -392,7 +392,10 @@ define([
Alertify.success(res.data.info);
pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
} else {
Alertify.error(res.data.errmsg);
Alertify.alert(
gettext('Maintenance job creation failed.'),
res.errormsg
);
}
})
.fail(function() {
@ -467,8 +470,33 @@ define([
});
}
// Open the Alertify dialog
Alertify.MaintenanceDialog('Maintenance...').set('resizable', true).resizeTo('60%', '80%');
const baseUrl = url_for('maintenance.utility_exists', {
'sid': server_data._id,
});
// Check psql utility exists or not.
$.ajax({
url: baseUrl,
type:'GET',
})
.done(function(res) {
if (!res.success) {
Alertify.alert(
gettext('Utility not found'),
res.errormsg
);
return;
}
// Open the Alertify dialog
Alertify.MaintenanceDialog('Maintenance...').set('resizable', true).resizeTo('60%', '80%');
})
.fail(function() {
Alertify.alert(
gettext('Utility not found'),
gettext('Failed to fetch Utility information')
);
return;
});
},
};

View File

@ -10,9 +10,11 @@
import time
import random
import simplejson as json
import os
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from pgadmin.utils import is_utility_exists
class MaintenanceJobTest(BaseTestGenerator):
@ -38,13 +40,21 @@ class MaintenanceJobTest(BaseTestGenerator):
]
def setUp(self):
if self.server['default_binary_paths'] is None:
if 'default_binary_paths' not in self.server or \
self.server['type'] not in self.server['default_binary_paths'] or\
self.server['default_binary_paths'][self.server['type']] == '':
self.skipTest(
"default_binary_paths is not set for the server {0}".format(
self.server['name']
)
)
binary_path = os.path.join(
self.server['default_binary_paths'][self.server['type']], 'psql')
retVal = is_utility_exists(binary_path)
if retVal is not None:
self.skipTest(retVal)
def runTest(self):
self.server_id = parent_node_dict["database"][-1]["server_id"]
self.db_id = parent_node_dict["database"][-1]["db_id"]

View File

@ -8,11 +8,12 @@
##########################################################################
import sys
import os
import simplejson as json
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from pgadmin.utils import server_utils as server_utils
from pgadmin.utils import server_utils as server_utils, is_utility_exists
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
@ -128,13 +129,21 @@ class MaintenanceCreateJobTest(BaseTestGenerator):
]
def setUp(self):
if self.server['default_binary_paths'] is None:
if 'default_binary_paths' not in self.server or \
self.server['type'] not in self.server['default_binary_paths'] or\
self.server['default_binary_paths'][self.server['type']] == '':
self.skipTest(
"default_binary_paths is not set for the server {0}".format(
self.server['name']
)
)
binary_path = os.path.join(
self.server['default_binary_paths'][self.server['type']], 'psql')
retVal = is_utility_exists(binary_path)
if retVal is not None:
self.skipTest(retVal)
@patch('pgadmin.tools.maintenance.Server')
@patch('pgadmin.tools.maintenance.Message')
@patch('pgadmin.tools.maintenance.BatchProcess')

View File

@ -18,7 +18,7 @@ from flask_babelex import gettext as _
from flask_security import login_required, current_user
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
from pgadmin.utils import PgAdminModule, get_storage_directory, html, \
fs_short_path, document_dir
fs_short_path, document_dir, is_utility_exists
from pgadmin.utils.ajax import make_json_response, bad_request
from config import PG_DEFAULT_DRIVER
@ -56,7 +56,7 @@ class RestoreModule(PgAdminModule):
Returns:
list: URL endpoints for backup module
"""
return ['restore.create_job']
return ['restore.create_job', 'restore.utility_exists']
# Create blueprint for RestoreModule class
@ -231,6 +231,12 @@ def create_restore_job(sid):
)
utility = manager.utility('restore')
ret_val = is_utility_exists(utility)
if ret_val:
return make_json_response(
success=0,
errormsg=ret_val
)
args = []
@ -365,7 +371,39 @@ def create_restore_job(sid):
)
"""
TODO://
Add browser tree
"""
@blueprint.route(
'/utility_exists/<int:sid>', endpoint='utility_exists'
)
@login_required
def check_utility_exists(sid):
"""
This function checks the utility file exist on the given path.
Args:
sid: Server ID
Returns:
None
"""
server = Server.query.filter_by(
id=sid, user_id=current_user.id
).first()
if server is None:
return make_json_response(
success=0,
errormsg=_("Could not find the specified server.")
)
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(server.id)
utility = manager.utility('restore')
ret_val = is_utility_exists(utility)
if ret_val:
return make_json_response(
success=0,
errormsg=ret_val
)
return make_json_response(success=1)

View File

@ -11,6 +11,8 @@ import gettext from '../../../../static/js/gettext';
import {sprintf} from 'sprintf-js';
import Backform from '../../../../static/js/backform.pgadmin';
import {Dialog} from '../../../../static/js/alertify/dialog';
import url_for from 'sources/url_for';
import axios from 'axios/index';
export class RestoreDialog extends Dialog {
constructor(pgBrowser, $, alertify, RestoreModel, backform = Backform) {
@ -19,6 +21,12 @@ export class RestoreDialog extends Dialog {
pgBrowser, $, alertify, RestoreModel, backform);
}
url_for_utility_exists(id){
return url_for('restore.utility_exists', {
'sid': id,
});
}
draw(action, aciTreeItem) {
const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
@ -31,23 +39,43 @@ export class RestoreDialog extends Dialog {
return;
}
if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
const baseUrl = this.url_for_utility_exists(serverInformation._id);
// Check pg_restore utility exists or not.
let that = this;
let service = axios.create({});
service.get(
baseUrl
).then(function(res) {
if (!res.data.success) {
that.alertify.alert(
gettext('Utility not found'),
res.data.errormsg
);
return;
}
if (!that.canExecuteOnCurrentDatabase(aciTreeItem)) {
return;
}
let aciTreeItem1 = aciTreeItem || that.pgBrowser.treeMenu.selected();
let item = that.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem1);
const data = item.getData();
const node = that.pgBrowser.Nodes[data._type];
if (!node)
return;
let title = sprintf(gettext('Restore (%s: %s)'), node.label, data.label);
that.createOrGetDialog(title, 'restore');
that.alertify.pg_restore(title, aciTreeItem1, data, node).resizeTo('65%', '60%');
}).catch(function() {
that.alertify.alert(
gettext('Utility not found'),
gettext('Failed to fetch Utility information')
);
return;
}
let aciTreeItem1 = aciTreeItem || this.pgBrowser.treeMenu.selected();
let item = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem1);
const data = item.getData();
const node = this.pgBrowser.Nodes[data._type];
if (!node)
return;
let title = sprintf(gettext('Restore (%s: %s)'), node.label, data.label);
this.createOrGetDialog(title, 'restore');
this.alertify.pg_restore(title, aciTreeItem1, data, node).resizeTo('65%', '60%');
});
}
dialogName() {

View File

@ -136,9 +136,16 @@ export class RestoreDialogWrapper extends DialogWrapper {
service.post(
baseUrl,
this.view.model.toJSON()
).then(function () {
dialogWrapper.alertify.success(gettext('Restore job created.'), 5);
dialogWrapper.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialogWrapper);
).then(function (res) {
if (res.data.success) {
dialogWrapper.alertify.success(gettext('Restore job created.'), 5);
dialogWrapper.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialogWrapper);
} else {
dialogWrapper.alertify.alert(
gettext('Restore job creation failed.'),
res.data.errormsg
);
}
}).catch(function (error) {
try {
const err = error.response.data;

View File

@ -17,7 +17,7 @@ import simplejson as json
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from pgadmin.utils import server_utils as server_utils
from pgadmin.utils import server_utils as server_utils, is_utility_exists
import pgadmin.tools.backup.tests.test_backup_utils as backup_utils
@ -62,13 +62,22 @@ class RestoreJobTest(BaseTestGenerator):
]
def setUp(self):
if self.server['default_binary_paths'] is None:
if 'default_binary_paths' not in self.server or \
self.server['type'] not in self.server['default_binary_paths'] or\
self.server['default_binary_paths'][self.server['type']] == '':
self.skipTest(
"default_binary_paths is not set for the server {0}".format(
self.server['name']
)
)
binary_path = os.path.join(
self.server['default_binary_paths'][self.server['type']],
'pg_restore')
retVal = is_utility_exists(binary_path)
if retVal is not None:
self.skipTest(retVal)
def create_backup(self):
url = self.backup_options['url'].format(self.server_id)
job_id = backup_utils.create_backup_job(self.tester, url,

View File

@ -9,10 +9,11 @@
import sys
import simplejson as json
import os
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from pgadmin.utils import server_utils as server_utils
from pgadmin.utils import server_utils as server_utils, is_utility_exists
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
@ -291,13 +292,22 @@ class RestoreCreateJobTest(BaseTestGenerator):
]
def setUp(self):
if self.server['default_binary_paths'] is None:
if 'default_binary_paths' not in self.server or \
self.server['type'] not in self.server['default_binary_paths'] or\
self.server['default_binary_paths'][self.server['type']] == '':
self.skipTest(
"default_binary_paths is not set for the server {0}".format(
self.server['name']
)
)
binary_path = os.path.join(
self.server['default_binary_paths'][self.server['type']],
'pg_restore')
retVal = is_utility_exists(binary_path)
if retVal is not None:
self.skipTest(retVal)
@patch('pgadmin.tools.restore.Server')
@patch('pgadmin.tools.restore.current_user')
@patch('pgadmin.tools.restore.RestoreMessage')

View File

@ -284,6 +284,18 @@ def get_complete_file_path(file):
return file if os.path.isfile(file) else None
def is_utility_exists(file):
"""
This function will check the utility file exists on given path.
:return:
"""
error_msg = None
if not os.path.exists(file):
error_msg = gettext(u"'%s' file not found. Please correct the Binary"
u" Path in the Preferences dialog" % file)
return error_msg
# Shortcut configuration for Accesskey
ACCESSKEY_FIELDS = [
{

View File

@ -8,6 +8,8 @@
//////////////////////////////////////////////////////////////
import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
import {TreeFake} from '../tree/tree_fake';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios/index';
const context = describe;
@ -93,7 +95,9 @@ describe('BackupDialog', () => {
});
describe('#draw', () => {
let networkMock;
beforeEach(() => {
networkMock = new MockAdapter(axios);
alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
alertifySpy['backup_objects'] = jasmine.createSpy('backup_objects');
backupDialog = new BackupDialog(
@ -106,6 +110,10 @@ describe('BackupDialog', () => {
pgBrowser.get_preference = jasmine.createSpy('get_preferences');
});
afterEach(() => {
networkMock.restore();
});
context('there are no ancestors of the type server', () => {
it('does not create a dialog', () => {
pgBrowser.treeMenu.selectNode([{id: 'root'}]);
@ -183,19 +191,27 @@ describe('BackupDialog', () => {
alertifySpy['backup_objects'].and
.returnValue(backupDialogResizeToSpy);
pgBrowser.get_preference.and.returnValue({value: '/some/path'});
spyOn(backupDialog, 'url_for_utility_exists').and.returnValue('/backup/utility_exists/10/objects');
networkMock.onGet('/backup/utility_exists/10/objects').reply(200, {'success': 1});
});
it('displays the dialog', () => {
it('displays the dialog', (done) => {
backupDialog.draw(null, [{id: 'serverTreeNode'}], null);
expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
setTimeout(() => {
expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
done();
}, 0);
});
context('database label contain "="', () => {
it('should create alert dialog with backup error', () => {
it('should create alert dialog with backup error', (done) => {
backupDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
expect(alertifySpy.alert).toHaveBeenCalledWith('Backup Error',
'Databases with = symbols in the name cannot be backed up or restored using this utility.');
setTimeout(() => {
expect(alertifySpy.alert).toHaveBeenCalledWith('Backup Error',
'Databases with = symbols in the name cannot be backed up or restored using this utility.');
done();
}, 0);
});
});
});

View File

@ -385,7 +385,7 @@ describe('BackupDialogWrapper', () => {
networkMock.onPost('/backup/job/10').reply((request) => {
dataSentToServer = request.data;
return [200, {}];
return [200, {'success': 1}];
});
});
@ -485,7 +485,7 @@ describe('BackupDialogWrapper', () => {
networkMock.onPost('/backup/job/10/object').reply((request) => {
dataSentToServer = request.data;
return [200, {}];
return [200, {'success': 1}];
});
});

View File

@ -8,6 +8,8 @@
//////////////////////////////////////////////////////////////
import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
import {TreeFake} from '../tree/tree_fake';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios/index';
const context = describe;
@ -48,7 +50,9 @@ describe('GlobalServerBackupDialog', () => {
});
describe('#draw', () => {
let networkMock;
beforeEach(() => {
networkMock = new MockAdapter(axios);
alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
alertifySpy['BackupDialog_globals'] = jasmine.createSpy('BackupDialog_globals');
alertifySpy['BackupDialog_server'] = jasmine.createSpy('BackupDialog_server');
@ -62,6 +66,10 @@ describe('GlobalServerBackupDialog', () => {
pgBrowser.get_preference = jasmine.createSpy('get_preferences');
});
afterEach(() => {
networkMock.restore();
});
context('there are no ancestors of the type server', () => {
it('does not create a dialog', () => {
pgBrowser.treeMenu.selectNode([{id: 'level1'}]);
@ -144,21 +152,29 @@ describe('GlobalServerBackupDialog', () => {
alertifySpy['BackupDialog_server'].and
.returnValue(serverResizeToSpy);
pgBrowser.get_preference.and.returnValue({value: '/some/path'});
spyOn(backupDialog, 'url_for_utility_exists').and.returnValue('/backup/utility_exists/10/servers');
networkMock.onGet('/backup/utility_exists/10/servers').reply(200, {'success': 1});
});
context('dialog for global backup', () => {
it('displays the dialog', () => {
it('displays the dialog', (done) => {
backupDialog.draw(null, [serverTreeNode], {globals: true});
expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
setTimeout(() => {
expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
done();
}, 0);
});
});
context('dialog for server backup', () => {
it('displays the dialog', () => {
it('displays the dialog', (done) => {
backupDialog.draw(null, [serverTreeNode], {server: true});
expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
setTimeout(() => {
expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
done();
}, 0);
});
});
});

View File

@ -8,6 +8,8 @@
//////////////////////////////////////////////////////////////
import {TreeFake} from '../tree/tree_fake';
import {RestoreDialog} from '../../../pgadmin/tools/restore/static/js/restore_dialog';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios/index';
const context = describe;
@ -81,7 +83,9 @@ describe('RestoreDialog', () => {
});
describe('#draw', () => {
let networkMock;
beforeEach(() => {
networkMock = new MockAdapter(axios);
alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
alertifySpy['pg_restore'] = jasmine.createSpy('pg_restore');
restoreDialog = new RestoreDialog(
@ -94,6 +98,10 @@ describe('RestoreDialog', () => {
pgBrowser.get_preference = jasmine.createSpy('get_preferences');
});
afterEach(() => {
networkMock.restore();
});
context('there are no ancestors of the type server', () => {
it('does not create a dialog', () => {
pgBrowser.treeMenu.selectNode([{id: 'root'}]);
@ -172,28 +180,36 @@ describe('RestoreDialog', () => {
.returnValue(spy);
pgBrowser.get_preference.and.returnValue({value: '/some/path'});
pgBrowser.Nodes.server.label = 'some-server-label';
spyOn(restoreDialog, 'url_for_utility_exists').and.returnValue('/restore/utility_exists/10/objects');
networkMock.onGet('/restore/utility_exists/10/objects').reply(200, {'success': 1});
});
it('displays the dialog', () => {
it('displays the dialog', (done) => {
restoreDialog.draw(null, [{id: 'serverTreeNode'}], {server: true});
expect(alertifySpy['pg_restore']).toHaveBeenCalledWith(
'Restore (some-server-label: some-tree-label)',
[{id: 'serverTreeNode'}],
{
_id: 10,
_type: 'server',
label: 'some-tree-label',
},
pgBrowser.Nodes.server
);
expect(spy.resizeTo).toHaveBeenCalledWith('65%', '60%');
setTimeout(() => {
expect(alertifySpy['pg_restore']).toHaveBeenCalledWith(
'Restore (some-server-label: some-tree-label)',
[{id: 'serverTreeNode'}],
{
_id: 10,
_type: 'server',
label: 'some-tree-label',
},
pgBrowser.Nodes.server
);
expect(spy.resizeTo).toHaveBeenCalledWith('65%', '60%');
done();
}, 0);
});
context('database label contain "="', () => {
it('should create alert dialog with restore error', () => {
it('should create alert dialog with restore error', (done) => {
restoreDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
expect(alertifySpy.alert).toHaveBeenCalledWith('Restore Error',
'Databases with = symbols in the name cannot be backed up or restored using this utility.');
setTimeout(() => {
expect(alertifySpy.alert).toHaveBeenCalledWith('Restore Error',
'Databases with = symbols in the name cannot be backed up or restored using this utility.');
done();
}, 0);
});
});
});

View File

@ -397,7 +397,7 @@ describe('RestoreDialogWrapper', () => {
beforeEach(() => {
networkMock.onPost('/restore/job/10').reply((request) => {
dataSentToServer = request.data;
return [200, {}];
return [200, {'success': 1}];
});
});

View File

@ -436,6 +436,9 @@ def create_server(server):
server_id = cur.lastrowid
conn.commit()
conn.close()
type = get_server_type(server)
server['type'] = type
# Add server info to parent_node_dict
regression.parent_node_dict["server"].append(
{
@ -899,3 +902,35 @@ def create_schema(server, db_name, schema_name):
except Exception:
traceback.print_exc(file=sys.stderr)
def get_server_type(server):
"""
This function will return the type of the server (PPAS, PG or GPDB)
:param server:
:return:
"""
try:
connection = get_db_connection(
server['db'],
server['username'],
server['db_password'],
server['host'],
server['port'],
server['sslmode']
)
pg_cursor = connection.cursor()
# Get 'version' string
pg_cursor.execute("SELECT version()")
version_string = pg_cursor.fetchone()
connection.close()
if "Greenplum Database" in version_string:
return 'gpdb'
elif "EnterpriseDB" in version_string:
return 'ppas'
return 'pg'
except Exception:
traceback.print_exc(file=sys.stderr)