diff --git a/docs/en_US/backup_dialog.rst b/docs/en_US/backup_dialog.rst index 8eb5cd465..ec89bdaf0 100644 --- a/docs/en_US/backup_dialog.rst +++ b/docs/en_US/backup_dialog.rst @@ -113,6 +113,8 @@ When you’ve 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 diff --git a/docs/en_US/backup_globals_dialog.rst b/docs/en_US/backup_globals_dialog.rst index bde2d46f1..95d200788 100644 --- a/docs/en_US/backup_globals_dialog.rst +++ b/docs/en_US/backup_globals_dialog.rst @@ -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 diff --git a/docs/en_US/backup_server_dialog.rst b/docs/en_US/backup_server_dialog.rst index 234ecfd78..53f992c9b 100644 --- a/docs/en_US/backup_server_dialog.rst +++ b/docs/en_US/backup_server_dialog.rst @@ -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 diff --git a/docs/en_US/images/backup_globals_messages.png b/docs/en_US/images/backup_globals_messages.png index 5692528a5..644871807 100644 Binary files a/docs/en_US/images/backup_globals_messages.png and b/docs/en_US/images/backup_globals_messages.png differ diff --git a/docs/en_US/images/backup_globals_process_watcher.png b/docs/en_US/images/backup_globals_process_watcher.png index 59e22534f..40d3fb176 100644 Binary files a/docs/en_US/images/backup_globals_process_watcher.png and b/docs/en_US/images/backup_globals_process_watcher.png differ diff --git a/docs/en_US/images/backup_messages.png b/docs/en_US/images/backup_messages.png index 09d2aa18f..f50dc2d26 100644 Binary files a/docs/en_US/images/backup_messages.png and b/docs/en_US/images/backup_messages.png differ diff --git a/docs/en_US/images/backup_process_watcher.png b/docs/en_US/images/backup_process_watcher.png index 323a45de5..f68c3ce2c 100644 Binary files a/docs/en_US/images/backup_process_watcher.png and b/docs/en_US/images/backup_process_watcher.png differ diff --git a/docs/en_US/images/backup_server_messages.png b/docs/en_US/images/backup_server_messages.png index c14996281..e1e047f79 100644 Binary files a/docs/en_US/images/backup_server_messages.png and b/docs/en_US/images/backup_server_messages.png differ diff --git a/docs/en_US/images/backup_server_process_watcher.png b/docs/en_US/images/backup_server_process_watcher.png index 9bbc12b48..d4b275179 100644 Binary files a/docs/en_US/images/backup_server_process_watcher.png and b/docs/en_US/images/backup_server_process_watcher.png differ diff --git a/docs/en_US/images/import_export_complete.png b/docs/en_US/images/import_export_complete.png index 6a3d42034..95527e7f4 100644 Binary files a/docs/en_US/images/import_export_complete.png and b/docs/en_US/images/import_export_complete.png differ diff --git a/docs/en_US/images/import_export_pw.png b/docs/en_US/images/import_export_pw.png index a96a32996..d586bb861 100644 Binary files a/docs/en_US/images/import_export_pw.png and b/docs/en_US/images/import_export_pw.png differ diff --git a/docs/en_US/images/maintenance_complete.png b/docs/en_US/images/maintenance_complete.png index d20ae2dd2..9ac90aa56 100644 Binary files a/docs/en_US/images/maintenance_complete.png and b/docs/en_US/images/maintenance_complete.png differ diff --git a/docs/en_US/images/maintenance_pw.png b/docs/en_US/images/maintenance_pw.png index 6966cae18..bb7f13d9b 100644 Binary files a/docs/en_US/images/maintenance_pw.png and b/docs/en_US/images/maintenance_pw.png differ diff --git a/docs/en_US/images/restore_messages.png b/docs/en_US/images/restore_messages.png index 3c4fa3cff..26c6cb2ac 100644 Binary files a/docs/en_US/images/restore_messages.png and b/docs/en_US/images/restore_messages.png differ diff --git a/docs/en_US/images/restore_process_watcher.png b/docs/en_US/images/restore_process_watcher.png index 3f1a6498f..9bd4b83b1 100644 Binary files a/docs/en_US/images/restore_process_watcher.png and b/docs/en_US/images/restore_process_watcher.png differ diff --git a/docs/en_US/import_export_data.rst b/docs/en_US/import_export_data.rst index ef5f5c6c7..4db0be767 100644 --- a/docs/en_US/import_export_data.rst +++ b/docs/en_US/import_export_data.rst @@ -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 diff --git a/docs/en_US/maintenance_dialog.rst b/docs/en_US/maintenance_dialog.rst index 424f87c5c..b022d9bf3 100644 --- a/docs/en_US/maintenance_dialog.rst +++ b/docs/en_US/maintenance_dialog.rst @@ -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 diff --git a/docs/en_US/release_notes_3_5.rst b/docs/en_US/release_notes_3_5.rst index eb51650ed..d7406705b 100644 --- a/docs/en_US/release_notes_3_5.rst +++ b/docs/en_US/release_notes_3_5.rst @@ -17,6 +17,7 @@ Features Bug fixes ********* +| `Bug #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 `_ - Fix syntax error when creating new pgAgent schedules with a start date/time and exception. | `Bug #3674 `_ - Cleanup session files periodically. | `Bug #3660 `_ - Rename the 'SQL Editor' section of the Preferences to 'Query Tool' as it applies to the whole tool, not just the editor. diff --git a/docs/en_US/restore_dialog.rst b/docs/en_US/restore_dialog.rst index 245cccdcb..ef4721d26 100644 --- a/docs/en_US/restore_dialog.rst +++ b/docs/en_US/restore_dialog.rst @@ -84,6 +84,8 @@ When you’ve 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 diff --git a/requirements.txt b/requirements.txt index 38646fbb4..1c076e954 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/web/migrations/versions/ece2e76bf60e_.py b/web/migrations/versions/ece2e76bf60e_.py new file mode 100644 index 000000000..c5a5cfc2f --- /dev/null +++ b/web/migrations/versions/ece2e76bf60e_.py @@ -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 diff --git a/web/pgadmin/misc/bgprocess/__init__.py b/web/pgadmin/misc/bgprocess/__init__.py index 9239cb6ae..28cdc0e67 100644 --- a/web/pgadmin/misc/bgprocess/__init__.py +++ b/web/pgadmin/misc/bgprocess/__init__.py @@ -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/', 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)) diff --git a/web/pgadmin/misc/bgprocess/process_executor.py b/web/pgadmin/misc/bgprocess/process_executor.py index f7271188f..0563d47d1 100644 --- a/web/pgadmin/misc/bgprocess/process_executor.py +++ b/web/pgadmin/misc/bgprocess/process_executor.py @@ -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. diff --git a/web/pgadmin/misc/bgprocess/processes.py b/web/pgadmin/misc/bgprocess/processes.py index c6151e328..a8856f8f0 100644 --- a/web/pgadmin/misc/bgprocess/processes.py +++ b/web/pgadmin/misc/bgprocess/processes.py @@ -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) diff --git a/web/pgadmin/misc/bgprocess/static/js/bgprocess.js b/web/pgadmin/misc/bgprocess/static/js/bgprocess.js index d14c3d1fd..c88cc5533 100644 --- a/web/pgadmin/misc/bgprocess/static/js/bgprocess.js +++ b/web/pgadmin/misc/bgprocess/static/js/bgprocess.js @@ -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', [ $('
', { class: 'pg-bg-start', }).append( - $('
').text(self.stime.toString()) + $('
', { + class: 'row align-items-center', + }).append( + $('
', { + class: 'col-9', + }).text(self.stime.toString()) + ).append( + $('
', { + class: 'col-3', + }).append( + $('', { + type: 'button', + class: 'btn btn-danger btn-sm float-right bg-process-stop', + }).text('Stop Process') + ) + ) ).append( $('
') ) @@ -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: '
' + '

' + - '
' + - '' + gettext('Start time') + ': ' + - '' + - '
' + + '
' + + '
' + + '
' + + '' + gettext('Start time') + ': ' + + '' + + '' + + '
' + + '
' + + '' + + '
' + + '
' + '
' + '
' + '
' + diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index d3b0c5480..57a6360ab 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -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): diff --git a/web/pgadmin/tools/backup/__init__.py b/web/pgadmin/tools/backup/__init__.py index 9548bd451..77504204e 100644 --- a/web/pgadmin/tools/backup/__init__.py +++ b/web/pgadmin/tools/backup/__init__.py @@ -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//', 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) diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog.js b/web/pgadmin/tools/backup/static/js/backup_dialog.js index 2f530d80b..0d1afa51a 100644 --- a/web/pgadmin/tools/backup/static/js/backup_dialog.js +++ b/web/pgadmin/tools/backup/static/js/backup_dialog.js @@ -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) { diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js index 92de82f71..49c7a9be5 100644 --- a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js +++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js @@ -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; diff --git a/web/pgadmin/tools/backup/tests/test_backup_create_job_unit_test.py b/web/pgadmin/tools/backup/tests/test_backup_create_job_unit_test.py index 9fbf1eba0..4897e1e03 100644 --- a/web/pgadmin/tools/backup/tests/test_backup_create_job_unit_test.py +++ b/web/pgadmin/tools/backup/tests/test_backup_create_job_unit_test.py @@ -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') diff --git a/web/pgadmin/tools/backup/tests/test_create_backup_job.py b/web/pgadmin/tools/backup/tests/test_create_backup_job.py index 70628fa9a..3f6a6de5f 100644 --- a/web/pgadmin/tools/backup/tests/test_create_backup_job.py +++ b/web/pgadmin/tools/backup/tests/test_create_backup_job.py @@ -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) diff --git a/web/pgadmin/tools/import_export/__init__.py b/web/pgadmin/tools/import_export/__init__.py index fddcfc069..e3a368ee5 100644 --- a/web/pgadmin/tools/import_export/__init__.py +++ b/web/pgadmin/tools/import_export/__init__.py @@ -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/', 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) diff --git a/web/pgadmin/tools/import_export/static/js/import_export.js b/web/pgadmin/tools/import_export/static/js/import_export.js index 22113746f..2080928a7 100644 --- a/web/pgadmin/tools/import_export/static/js/import_export.js +++ b/web/pgadmin/tools/import_export/static/js/import_export.js @@ -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; + }); }, }; diff --git a/web/pgadmin/tools/maintenance/__init__.py b/web/pgadmin/tools/maintenance/__init__.py index f7fbdb055..2be1482b9 100644 --- a/web/pgadmin/tools/maintenance/__init__.py +++ b/web/pgadmin/tools/maintenance/__init__.py @@ -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/', 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) diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js index e92deac88..d970292cc 100644 --- a/web/pgadmin/tools/maintenance/static/js/maintenance.js +++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js @@ -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; + }); }, }; diff --git a/web/pgadmin/tools/maintenance/tests/test_create_maintenance_job.py b/web/pgadmin/tools/maintenance/tests/test_create_maintenance_job.py index ba1f076da..457b72feb 100644 --- a/web/pgadmin/tools/maintenance/tests/test_create_maintenance_job.py +++ b/web/pgadmin/tools/maintenance/tests/test_create_maintenance_job.py @@ -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"] diff --git a/web/pgadmin/tools/maintenance/tests/test_maintenance_create_job_unit_test.py b/web/pgadmin/tools/maintenance/tests/test_maintenance_create_job_unit_test.py index c51f8f022..f4eb55189 100644 --- a/web/pgadmin/tools/maintenance/tests/test_maintenance_create_job_unit_test.py +++ b/web/pgadmin/tools/maintenance/tests/test_maintenance_create_job_unit_test.py @@ -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') diff --git a/web/pgadmin/tools/restore/__init__.py b/web/pgadmin/tools/restore/__init__.py index f639e1980..0b5fab683 100644 --- a/web/pgadmin/tools/restore/__init__.py +++ b/web/pgadmin/tools/restore/__init__.py @@ -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/', 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) diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog.js b/web/pgadmin/tools/restore/static/js/restore_dialog.js index 4884d9012..3789cbed6 100644 --- a/web/pgadmin/tools/restore/static/js/restore_dialog.js +++ b/web/pgadmin/tools/restore/static/js/restore_dialog.js @@ -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() { diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js index 7b12232ad..f487339de 100644 --- a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js +++ b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js @@ -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; diff --git a/web/pgadmin/tools/restore/tests/test_create_restore_job.py b/web/pgadmin/tools/restore/tests/test_create_restore_job.py index 682d65d33..b0f988be2 100644 --- a/web/pgadmin/tools/restore/tests/test_create_restore_job.py +++ b/web/pgadmin/tools/restore/tests/test_create_restore_job.py @@ -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, diff --git a/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py b/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py index fbe1e9b9b..ffed47145 100644 --- a/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py +++ b/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py @@ -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') diff --git a/web/pgadmin/utils/__init__.py b/web/pgadmin/utils/__init__.py index f5118b42d..7bc2795f9 100644 --- a/web/pgadmin/utils/__init__.py +++ b/web/pgadmin/utils/__init__.py @@ -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 = [ { diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js index 2655059d1..71bdae119 100644 --- a/web/regression/javascript/backup/backup_dialog_spec.js +++ b/web/regression/javascript/backup/backup_dialog_spec.js @@ -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); }); }); }); diff --git a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js index 58705318d..d975cf93c 100644 --- a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js +++ b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js @@ -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}]; }); }); diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js index 86df672e0..f823e5595 100644 --- a/web/regression/javascript/backup/global_server_backup_dialog_spec.js +++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js @@ -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); }); }); }); diff --git a/web/regression/javascript/restore/restore_dialog_spec.js b/web/regression/javascript/restore/restore_dialog_spec.js index 156f56bb3..fe0b7531d 100644 --- a/web/regression/javascript/restore/restore_dialog_spec.js +++ b/web/regression/javascript/restore/restore_dialog_spec.js @@ -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); }); }); }); diff --git a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js index c2a31d55a..420aa3717 100644 --- a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js +++ b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js @@ -397,7 +397,7 @@ describe('RestoreDialogWrapper', () => { beforeEach(() => { networkMock.onPost('/restore/job/10').reply((request) => { dataSentToServer = request.data; - return [200, {}]; + return [200, {'success': 1}]; }); }); diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py index beeb6322a..393d87e92 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -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)