Added support for the infrastructure for on demand access/create the

server connection.

The BaseDriver and BaseConnection are two abstract classes, which allows
us to replace the existing driver with the currently used. The current
implementation supports to connect the PostgreSQL and Postgres Plus
Advanced Server using the psycopg2 driver.
This commit is contained in:
Ashesh Vashi 2015-10-20 12:33:18 +05:30
parent b52d72f176
commit e27e39a8f3
34 changed files with 2625 additions and 417 deletions

View File

@ -25,3 +25,4 @@ pytz==2014.10
six==1.9.0
speaklater==1.3
wsgiref==0.1.2
pycrypto==2.6.1

View File

@ -7,7 +7,7 @@
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
# config.py - Core application configuration settings
# config.py - Core application configuration settings
#
##########################################################################
@ -34,7 +34,7 @@ APP_SUFFIX = 'dev'
# Copyright string for display in the app
APP_COPYRIGHT = 'Copyright 2014 - 2015, The pgAdmin Development Team'
# Path to the online help.
# Path to the online help.
HELP_PATH = '../../../docs/en_US/_build/html/'
# Languages we support in the UI
@ -49,11 +49,11 @@ APP_VERSION = '%s.%s.%s-%s' % (APP_MAJOR, APP_MINOR, APP_REVISION, APP_SUFFIX)
# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING!
# List of modules to skip when dynamically loading
MODULE_BLACKLIST = [ 'test' ]
MODULE_BLACKLIST = ['test']
# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING!
# List of treeview browser nodes to skip when dynamically loading
NODE_BLACKLIST = [ ]
NODE_BLACKLIST = []
##########################################################################
# Log settings
@ -63,19 +63,19 @@ NODE_BLACKLIST = [ ]
DEBUG = False
# Application log level - one of:
# CRITICAL 50
# ERROR 40
# WARNING 30
# SQL 25
# INFO 20
# DEBUG 10
# NOTSET 0
# CRITICAL 50
# ERROR 40
# WARNING 30
# SQL 25
# INFO 20
# DEBUG 10
# NOTSET 0
CONSOLE_LOG_LEVEL = WARNING
FILE_LOG_LEVEL = INFO
# Log format.
CONSOLE_LOG_FORMAT='%(asctime)s: %(levelname)s\t%(name)s:\t%(message)s'
FILE_LOG_FORMAT='%(asctime)s: %(levelname)s\t%(name)s:\t%(message)s'
# Log format.
CONSOLE_LOG_FORMAT = '%(asctime)s: %(levelname)s\t%(name)s:\t%(message)s'
FILE_LOG_FORMAT = '%(asctime)s: %(levelname)s\t%(name)s:\t%(message)s'
# Log file name
LOG_FILE = 'pgadmin4.log'
@ -84,6 +84,8 @@ LOG_FILE = 'pgadmin4.log'
# Server settings
##########################################################################
PG_DEFAULT_DRIVER = 'psycopg2'
# The server mode determines whether or not we're running on a web server
# requiring user authentication, or desktop mode which uses an automatic
# default login.
@ -102,11 +104,11 @@ DEFAULT_SERVER_PORT = 5050
# Enable CSRF protection?
CSRF_ENABLED = True
# Secret key for signing CSRF data. Override this in config_local.py if
# Secret key for signing CSRF data. Override this in config_local.py if
# running on a web server
CSRF_SESSION_KEY = 'SuperSecret1'
# Secret key for signing cookies. Override this in config_local.py if
# Secret key for signing cookies. Override this in config_local.py if
# running on a web server
SECRET_KEY = 'SuperSecret2'
@ -118,7 +120,7 @@ SECURITY_PASSWORD_SALT = 'SuperSecret3'
SECURITY_PASSWORD_HASH = 'pbkdf2_sha512'
# Should HTML be minified on the fly when not in debug mode?
MINIFY_HTML = True;
MINIFY_HTML = True
##########################################################################
# User account and settings storage
@ -126,12 +128,15 @@ MINIFY_HTML = True;
# The schema version number for the configuration database
# DO NOT CHANGE UNLESS YOU ARE A PGADMIN DEVELOPER!!
SETTINGS_SCHEMA_VERSION = 3
SETTINGS_SCHEMA_VERSION = 7
# The default path to the SQLite database used to store user accounts and
# settings. This default places the file in the same directory as this
# config file, but generates an absolute path for use througout the app.
SQLITE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'pgadmin4.db')
SQLITE_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'pgadmin4.db'
)
##########################################################################
# Mail server settings
@ -150,9 +155,12 @@ MAIL_PASSWORD = 'SuperSecret'
##########################################################################
# These settings define the content of password reset emails
SECURITY_EMAIL_SUBJECT_PASSWORD_RESET = "Password reset instructions for %s" % APP_NAME
SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE = "Your %s password has been reset" % APP_NAME
SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE = "Your password for %s has been changed" % APP_NAME
SECURITY_EMAIL_SUBJECT_PASSWORD_RESET = "Password reset instructions for %s" \
% APP_NAME
SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE = "Your %s password has been reset" \
% APP_NAME
SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE = \
"Your password for %s has been changed" % APP_NAME
##########################################################################
# Local config settings

View File

@ -41,9 +41,11 @@ if config.SERVER_MODE is True:
# Check if the database exists. If it does not, tell the user and exit.
if not os.path.isfile(config.SQLITE_PATH):
print "The configuration database %s does not exist.\n" % config.SQLITE_PATH
print "The configuration database %s does not exist.\n" \
% config.SQLITE_PATH
print "Please run 'python %s' to create it.\nExiting..." % os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'setup.py')
os.path.dirname(os.path.realpath(__file__)), 'setup.py'
)
sys.exit(1)
##########################################################################
@ -55,6 +57,8 @@ app = create_app()
if config.DEBUG:
app.debug = True
else:
app.debug = False
# Start the web server. The port number should have already been set by the
# runtime if we're running in desktop mode, otherwise we'll just use the
@ -70,6 +74,6 @@ else:
server_port = config.DEFAULT_SERVER_PORT
try:
app.run(port=server_port)
app.run(port=server_port, use_reloader=(config.SERVER_MODE and app.debug))
except IOError:
app.logger.error("Error starting the app server: %s", sys.exc_info())

View File

@ -19,7 +19,7 @@ from htmlmin.minify import html_minify
from settings.settings_model import db, Role, User, Version
from importlib import import_module
from werkzeug.local import LocalProxy
from pgadmin.utils import PgAdminModule
from pgadmin.utils import PgAdminModule, driver
from werkzeug.utils import find_modules
import sys
import os
@ -34,8 +34,9 @@ class PgAdmin(Flask):
def find_submodules(self, basemodule):
for module_name in find_modules(basemodule, True):
if module_name in self.config['MODULE_BLACKLIST']:
self.logger.info('Skipping blacklisted module: %s' %
module_name)
self.logger.info(
'Skipping blacklisted module: %s' % module_name
)
continue
self.logger.info('Examining potential module: %s' % module_name)
module = import_module(module_name)
@ -82,6 +83,7 @@ class PgAdmin(Flask):
for key, values in menu_items.items()}
return menu_items
def _find_blueprint():
if request.blueprint:
return current_app.blueprints[request.blueprint]
@ -104,9 +106,10 @@ def create_app(app_name=config.APP_NAME):
app.logger.setLevel(logging.DEBUG)
app.logger.handlers = []
# We also need to update the handler on the webserver in order to see request.
# Setting the level prevents werkzeug from setting up it's own stream handler
# thus ensuring all the logging goes through the pgAdmin logger.
# We also need to update the handler on the webserver in order to see
# request. Setting the level prevents werkzeug from setting up it's own
# stream handler thus ensuring all the logging goes through the pgAdmin
# logger.
logger = logging.getLogger('werkzeug')
logger.setLevel(logging.INFO)
@ -125,9 +128,9 @@ def create_app(app_name=config.APP_NAME):
logger.addHandler(ch)
# Log the startup
app.logger.info('################################################################################')
app.logger.info('########################################################')
app.logger.info('Starting %s v%s...', config.APP_NAME, config.APP_VERSION)
app.logger.info('################################################################################')
app.logger.info('########################################################')
app.logger.debug("Python syspath: %s", sys.path)
##########################################################################
@ -149,7 +152,9 @@ def create_app(app_name=config.APP_NAME):
# Setup authentication
##########################################################################
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + config.SQLITE_PATH.replace('\\', '/')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{0}'.format(
config.SQLITE_PATH.replace('\\', '/')
)
# Only enable password related functionality in server mode.
if config.SERVER_MODE is True:
@ -200,7 +205,10 @@ def create_app(app_name=config.APP_NAME):
# the sysadmin a hint. We'll continue to try to login anyway as
# that'll through a nice 500 error for us.
if user is None:
app.logger.error('The desktop user %s was not found in the configuration database.' % config.DESKTOP_USER)
app.logger.error(
'The desktop user %s was not found in the configuration database.'
% config.DESKTOP_USER
)
abort(401)
login_user(user)
@ -224,11 +232,15 @@ def create_app(app_name=config.APP_NAME):
"""Inject a reference to the current blueprint, if any."""
return {
'current_app': current_app,
'current_blueprint': current_blueprint }
'current_blueprint': current_blueprint
}
# Load all available serve drivers
driver.init_app(app)
##########################################################################
# All done!
##########################################################################
app.logger.debug('URL map: %s' % app.url_map)
return app

View File

@ -62,13 +62,13 @@ def index():
info['app_mode'] = gettext('Desktop')
info['current_user'] = current_user.email
return render_template(MODULE_NAME + '/index.html', info=info)
return render_template(MODULE_NAME + '/index.html', info=info, _=gettext)
@blueprint.route("/about.js")
@login_required
def script():
"""render the required javascript"""
return Response(response=render_template("about/about.js"),
return Response(response=render_template("about/about.js", _=gettext),
status=200,
mimetype="application/javascript")

View File

@ -14,36 +14,38 @@ from pgadmin.settings import get_setting
from flask import current_app, render_template, url_for, make_response
from flask.ext.security import login_required
from flask.ext.login import current_user
from flask.ext.babel import gettext
from flaskext.gravatar import Gravatar
MODULE_NAME = 'browser'
class BrowserModule(PgAdminModule):
class BrowserModule(PgAdminModule):
def get_own_stylesheets(self):
stylesheets = []
# Add browser stylesheets
for (endpoint, filename) in [
('static', 'css/codemirror/codemirror.css'),
('static', 'css/jQuery-contextMenu/jquery.contextMenu.css'),
('static', 'css/wcDocker/wcDocker.css' if \
current_app.debug else \
'css/wcDocker/wcDocker.min.css'),
('browser.static', 'css/browser.css'),
('browser.static', 'css/aciTree/css/aciTree.css')
]:
('static', 'css/codemirror/codemirror.css'),
('static', 'css/jQuery-contextMenu/jquery.contextMenu.css'),
('static', 'css/wcDocker/wcDocker.css' if current_app.debug
else 'css/wcDocker/wcDocker.min.css'),
('browser.static', 'css/browser.css'),
('browser.static', 'css/aciTree/css/aciTree.css')
]:
stylesheets.append(url_for(endpoint, filename=filename))
stylesheets.append(url_for('browser.browser_css'))
return stylesheets
def get_own_javascripts(self):
scripts = list()
scripts.append({
'name': 'alertify',
'path': url_for('static', filename='js/alertifyjs/alertify' if current_app.debug \
else 'js/alertifyjs/alertify.min'),
'path': url_for(
'static',
filename='js/alertifyjs/alertify' if current_app.debug
else 'js/alertifyjs/alertify.min'
),
'exports': 'alertify',
'preloaded': True
})
@ -62,8 +64,10 @@ class BrowserModule(PgAdminModule):
})
scripts.append({
'name': 'jqueryui.position',
'path': url_for('static',
filename='js/jQuery-contextMenu/jquery.ui.position'),
'path': url_for(
'static',
filename='js/jQuery-contextMenu/jquery.ui.position'
),
'deps': ['jquery'],
'exports': 'jQuery.ui.position',
'preloaded': True
@ -71,35 +75,42 @@ class BrowserModule(PgAdminModule):
})
scripts.append({
'name': 'jquery.contextmenu',
'path': url_for('static',
filename='js/jQuery-contextMenu/jquery.contextMenu'),
'path': url_for(
'static',
filename='js/jQuery-contextMenu/jquery.contextMenu'
),
'deps': ['jquery', 'jqueryui.position'],
'exports': 'jQuery.contextMenu',
'preloaded': True
})
scripts.append({
'name': 'jquery.aciplugin',
'path': url_for('browser.static',
filename='js/aciTree/jquery.aciPlugin.min'),
'path': url_for(
'browser.static',
filename='js/aciTree/jquery.aciPlugin.min'
),
'deps': ['jquery'],
'exports': 'aciPluginClass',
'preloaded': True
})
scripts.append({
'name': 'jquery.acitree',
'path': url_for('browser.static',
filename='js/aciTree/jquery.aciTree' \
if current_app.debug else \
'js/aciTree/jquery.aciTree.min'),
'path': url_for(
'browser.static',
filename='js/aciTree/jquery.aciTree' if
current_app.debug else 'js/aciTree/jquery.aciTree.min'
),
'deps': ['jquery', 'jquery.aciplugin'],
'exports': 'aciPluginClass.plugins.aciTree',
'preloaded': True
})
scripts.append({
'name': 'wcdocker',
'path': url_for('static',
filename='js/wcDocker/wcDocker' if current_app.debug else \
'js/wcDocker/wcDocker.min'),
'path': url_for(
'static',
filename='js/wcDocker/wcDocker' if current_app.debug
else 'js/wcDocker/wcDocker.min'
),
'deps': ['jquery.contextmenu'],
'exports': '',
'preloaded': True
@ -109,8 +120,11 @@ class BrowserModule(PgAdminModule):
['pgadmin.browser', 'js/browser'],
['pgadmin.browser.error', 'js/error'],
['pgadmin.browser.node', 'js/node']]:
scripts.append({ 'name': name,
'path': url_for('browser.index') + script, 'preloaded': True })
scripts.append({
'name': name,
'path': url_for('browser.index') + script,
'preloaded': True
})
for name, end in [
['pgadmin.browser.menu', 'js/menu'],
@ -128,6 +142,7 @@ class BrowserModule(PgAdminModule):
blueprint = BrowserModule(MODULE_NAME, __name__)
class BrowserPluginModule(PgAdminModule):
"""
Base class for browser submodules.
@ -139,21 +154,19 @@ class BrowserPluginModule(PgAdminModule):
def __init__(self, import_name, **kwargs):
kwargs.setdefault("url_prefix", self.node_path)
kwargs.setdefault("static_url_path", '/static')
super(BrowserPluginModule, self).__init__("NODE-%s" % self.node_type,
super(BrowserPluginModule, self).__init__(
"NODE-%s" % self.node_type,
import_name,
**kwargs)
**kwargs
)
@property
@abstractmethod
def jssnippets(self):
"""
Returns a snippet of javascript to include in the page
"""
# TODO: move those methods to BrowserModule subclass ?
return []
def get_own_javascripts(self):
scripts = []
@ -168,17 +181,21 @@ class BrowserPluginModule(PgAdminModule):
return scripts
def generate_browser_node(self, node_id, parent_id, label, icon, inode):
def generate_browser_node(
self, node_id, parent_id, label, icon, inode, node_type, **kwargs
):
obj = {
"id": "%s/%s" % (self.node_type, node_id),
"label": label,
"icon": icon,
"inode": inode,
"_type": self.node_type,
"_id": node_id,
"refid": parent_id
}
"id": "%s/%s" % (node_type, node_id),
"label": label,
"icon": icon,
"inode": inode,
"_type": node_type,
"_id": node_id,
"refid": parent_id,
"module": 'pgadmin.node.%s' % node_type
}
for key in kwargs:
obj.setdefault(key, kwargs[key])
return obj
@ -187,15 +204,18 @@ class BrowserPluginModule(PgAdminModule):
"""
Returns a snippet of css to include in the page
"""
snippets = [render_template("browser/css/node.css",
node_type=self.node_type)]
snippets = [
render_template(
"browser/css/node.css",
node_type=self.node_type,
_=gettext
)]
for submodule in self.submodules:
snippets.extend(submodule.csssnippets)
return snippets
@abstractmethod
def get_nodes(self):
"""
@ -223,8 +243,7 @@ class BrowserPluginModule(PgAdminModule):
@property
def node_path(self):
return url_for('browser.index') + 'nodes/' + self.node_type
return self.browser_url_prefix + self.node_type
@property
def javascripts(self):
@ -243,8 +262,12 @@ def index():
force_default=False,
use_ssl=False,
base_url=None)
return render_template(MODULE_NAME + "/index.html",
username=current_user.email)
return render_template(
MODULE_NAME + "/index.html",
username=current_user.email,
_=gettext
)
@blueprint.route("/js/browser.js")
@login_required
@ -257,21 +280,25 @@ def browser_js():
render_template(
'browser/js/browser.js',
layout=layout,
jssnippets=snippets),
jssnippets=snippets,
_=gettext
),
200, {'Content-Type': 'application/x-javascript'})
@blueprint.route("/js/error.js")
@login_required
def error_js():
return make_response(
render_template('browser/js/error.js'),
render_template('browser/js/error.js', _=gettext),
200, {'Content-Type': 'application/x-javascript'})
@blueprint.route("/js/node.js")
@login_required
def node_js():
return make_response(
render_template('browser/js/node.js'),
render_template('browser/js/node.js', _=gettext),
200, {'Content-Type': 'application/x-javascript'})
@ -283,7 +310,9 @@ def browser_css():
for submodule in blueprint.submodules:
snippets.extend(submodule.csssnippets)
return make_response(
render_template('browser/css/browser.css', snippets=snippets),
render_template(
'browser/css/browser.css', snippets=snippets, _=gettext
),
200, {'Content-Type': 'text/css'})

View File

@ -20,6 +20,7 @@ from pgadmin.utils.ajax import make_json_response, \
from pgadmin.browser import BrowserPluginModule
from pgadmin.utils.menu import MenuItem
from pgadmin.settings.settings_model import db, ServerGroup
from pgadmin.browser.utils import NodeView
class ServerGroupModule(BrowserPluginModule):
@ -30,13 +31,14 @@ class ServerGroupModule(BrowserPluginModule):
"""Return a JSON document listing the server groups for the user"""
groups = ServerGroup.query.filter_by(user_id=current_user.id)
for group in groups:
group = self.generate_browser_node(
yield self.generate_browser_node(
"%d" % (group.id),
None,
group.name,
"icon-%s" % self.node_type,
True)
yield group
True,
self.node_type
)
@property
def node_type(self):
@ -46,10 +48,6 @@ class ServerGroupModule(BrowserPluginModule):
def script_load(self):
return None
@property
def node_path(self):
return BrowserPluginModule.browser_url_prefix + self.node_type
class ServerGroupMenuItem(MenuItem):
@ -65,29 +63,19 @@ class ServerGroupPluginModule(BrowserPluginModule):
__metaclass__ = ABCMeta
@abstractmethod
def get_nodes(self, *arg, **kwargs):
pass
@property
def node_path(self):
return BrowserPluginModule.browser_url_prefix + self.node_type
blueprint = ServerGroupModule( __name__, static_url_path='')
# Initialise the module
from pgadmin.browser.utils import NodeView
blueprint = ServerGroupModule(__name__, static_url_path='')
class ServerGroupView(NodeView):
node_type = ServerGroupModule.NODE_TYPE
parent_ids = []
ids = [{'type':'int', 'id':'gid'}]
ids = [{'type': 'int', 'id': 'gid'}]
def list(self):
res = []
@ -95,7 +83,6 @@ class ServerGroupView(NodeView):
res.append(g)
return make_json_response(result=res)
def delete(self, gid):
"""Delete a server group node in the settings database"""
@ -108,18 +95,22 @@ class ServerGroupView(NodeView):
return make_json_response(
status=417,
success=0,
errormsg=gettext('The specified server group could not be found.'))
errormsg=gettext(
'The specified server group could not be found.'
)
)
else:
try:
for sg in servergroup:
db.session.delete(sg)
db.session.commit()
except Exception as e:
return make_json_response(status=410, success=0, errormsg=e.message)
return make_json_response(
status=410, success=0, errormsg=e.message
)
return make_json_response(result=request.form)
def update(self, gid):
"""Update the server-group properties"""
@ -134,18 +125,22 @@ class ServerGroupView(NodeView):
return make_json_response(
status=417,
success=0,
errormsg=gettext('The specified server group could not be found.'))
errormsg=gettext(
'The specified server group could not be found.'
)
)
else:
try:
if u'name' in data:
servergroup.name = data[u'name']
db.session.commit()
except Exception as e:
return make_json_response(status=410, success=0, errormsg=e.message)
return make_json_response(
status=410, success=0, errormsg=e.message
)
return make_json_response(result=request.form)
def properties(self, gid):
"""Update the server-group properties"""
@ -159,11 +154,15 @@ class ServerGroupView(NodeView):
return make_json_response(
status=417,
success=0,
errormsg=gettext('The specified server group could not be found.'))
errormsg=gettext(
'The specified server group could not be found.'
)
)
else:
return ajax_response(response={'id': sg.id, 'name': sg.name},
status=200)
return ajax_response(
response={'id': sg.id, 'name': sg.name},
status=200
)
def create(self):
data = request.form if request.form else json.loads(request.data)
@ -179,14 +178,17 @@ class ServerGroupView(NodeView):
data[u'id'] = sg.id
data[u'name'] = sg.name
return jsonify(node=blueprint.generate_browser_node(
"%d" % (sg.id),
None,
sg.name,
"icon-%s" % self.node_type,
True))
return jsonify(
node=self.blueprint.generate_browser_node(
"%d" % (sg.id),
None,
sg.name,
"icon-%s" % self.node_type,
True,
self.node_type
)
)
except Exception as e:
print 'except'
return make_json_response(
status=410,
success=0,
@ -198,31 +200,18 @@ class ServerGroupView(NodeView):
success=0,
errormsg=gettext('No server group name was specified'))
def nodes(self, gid):
"""Build a list of treeview nodes from the child nodes."""
nodes = []
for module in blueprint.submodules:
nodes.extend(module.get_nodes(server_group=gid))
return make_json_response(data=nodes)
def sql(self, gid):
return make_json_response(status=422)
def modified_sql(self, gid):
return make_json_response(status=422)
def statistics(self, gid):
return make_json_response(status=422)
def dependencies(self, gid):
return make_json_response(status=422)
def dependents(self, gid):
return make_json_response(status=422)

View File

@ -0,0 +1,44 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from pgadmin.browser.server_groups.servers import ServerTypeModule
from pgadmin.browser.utils import PGChildModule
from flask.ext.babel import gettext
class PPASServer(ServerTypeModule, PGChildModule):
NODE_TYPE = "ppas"
@property
def node_type(self):
return self.NODE_TYPE
@property
def jssnippets(self):
return []
@property
def type(self):
return "PPAS"
@property
def description(self):
return "Postgres Plus Advanced Server"
@property
def driver(self):
return "psycopg2"
@property
def priority(self):
return 1
def instanceOf(self, ver):
return ver.startswith("EnterpriseDB")
blueprint = PPASServer(__name__, static_url_path='/static')

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

View File

@ -0,0 +1,43 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from pgadmin.browser.server_groups.servers import ServerTypeModule
from pgadmin.browser.utils import PGChildModule
class PGServer(ServerTypeModule, PGChildModule):
NODE_TYPE = "pg"
@property
def node_type(self):
return self.NODE_TYPE
@property
def jssnippets(self):
return []
@property
def type(self):
return "PG"
@property
def description(self):
return "PostgreSQL"
@property
def driver(self):
return "psycopg2"
@property
def priority(self):
return 10
def instanceOf(self, ver):
return True
blueprint = PGServer(__name__, static_url_path='/static')

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

View File

@ -7,17 +7,21 @@
#
##########################################################################
import json
from flask import render_template, request, make_response, jsonify
from abc import ABCMeta, abstractmethod, abstractproperty
from flask import render_template, request, make_response, jsonify, current_app
from flask.ext.security import login_required, current_user
from pgadmin.settings.settings_model import db, Server, ServerGroup
from pgadmin.settings.settings_model import db, Server, ServerGroup, User
from pgadmin.utils.menu import MenuItem
from pgadmin.utils.ajax import make_json_response, \
make_response as ajax_response
from pgadmin.browser.utils import NodeView, generate_browser_node
make_response as ajax_response, internal_server_error, success_return, \
unauthorized, bad_request, precondition_required, forbidden
from pgadmin.browser.utils import NodeView
import traceback
from flask.ext.babel import gettext
import pgadmin.browser.server_groups as sg
from pgadmin.utils.crypto import encrypt, decrypt
from pgadmin.browser import BrowserPluginModule
from config import PG_DEFAULT_DRIVER
class ServerModule(sg.ServerGroupPluginModule):
@ -35,27 +39,48 @@ class ServerModule(sg.ServerGroupPluginModule):
"""
return sg.ServerGroupModule.NODE_TYPE
def get_nodes(self, server_group):
def get_nodes(self, gid):
"""Return a JSON document listing the server groups for the user"""
servers = Server.query.filter_by(user_id=current_user.id,
servergroup_id=server_group)
servergroup_id=gid)
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
# TODO: Move this JSON generation to a Server method
for server in servers:
node = generate_browser_node(
"%d" % (server.id),
"%d" % server_group,
server.name,
"icon-%s-not-connected" % self.NODE_TYPE,
True,
self.NODE_TYPE)
manager = driver.connection_manager(server.id)
conn = manager.connection()
module = getattr(manager, "module", None)
connected = conn.connected()
yield node
yield self.generate_browser_node(
"%d" % (server.id),
"%d" % gid,
server.name,
"icon-server-not-connected" if not connected else
"icon-{0}".format(module.NODE_TYPE),
True,
self.NODE_TYPE,
connected=connected,
server_type=module.type if module is not None else "PG"
)
@property
def jssnippets(self):
return []
@property
def csssnippets(self):
"""
Returns a snippet of css to include in the page
"""
snippets = [render_template("css/servers.css")]
for submodule in self.submodules:
snippets.extend(submodule.csssnippets)
return snippets
class ServerMenuItem(MenuItem):
def __init__(self, **kwargs):
@ -66,8 +91,83 @@ class ServerMenuItem(MenuItem):
blueprint = ServerModule(__name__)
class ServerTypeModule(BrowserPluginModule):
"""
Base class for different server types.
"""
__metaclass__ = ABCMeta
@abstractproperty
def type(self):
pass
@abstractproperty
def description(self):
pass
@abstractproperty
def priority(self):
pass
def get_nodes(self, manager=None, sid=None):
assert(sid is not None)
nodes = []
for module in self.submodules:
if isinstance(module, PGChildModule):
if manager and module.BackendSupported(manager):
nodes.extend(module.get_nodes(sid=sid, manager=manager))
else:
nodes.extend(module.get_nodes(sid=sid))
return nodes
@abstractmethod
def instanceOf(self, version):
pass
def __str__(self):
return "Type: {0},Description:{1}".format(self.type, self.description)
@property
def csssnippets(self):
"""
Returns a snippet of css to include in the page
"""
snippets = [
render_template(
"css/node.css",
node_type=self.node_type
)
]
for submodule in self.submodules:
snippets.extend(submodule.csssnippets)
return snippets
def get_own_javascripts(self):
scripts = []
for module in self.submodules:
scripts.extend(module.get_own_javascripts())
return scripts
@property
def script_load(self):
"""
Load the module script for all server types, when a server node is
initialized.
"""
return ServerTypeModule.NODE_TYPE
class ServerNode(NodeView):
node_type = ServerModule.NODE_TYPE
parent_ids = [{'type': 'int', 'id': 'gid'}]
ids = [{'type': 'int', 'id': 'sid'}]
operations = dict({
@ -80,7 +180,9 @@ class ServerNode(NodeView):
'stats': [{'get': 'statistics'}],
'deps': [{'get': 'dependencies', 'post': 'dependents'}],
'module.js': [{}, {}, {'get': 'module_js'}],
'connect': [{'get': 'connect_status', 'post': 'connect', 'delete': 'disconnect'}]
'connect': [{
'get': 'connect_status', 'post': 'connect', 'delete': 'disconnect'
}]
})
def list(self, gid):
@ -89,20 +191,32 @@ class ServerNode(NodeView):
servers = Server.query.filter_by(user_id=current_user.id,
servergroup_id=gid)
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
for server in servers:
manager = driver.connection_manager(server.id)
conn = manager.connection()
module = getattr(manager, "module", None)
connected = conn.connected()
res.append(
generate_browser_node(
"%s" % server.id,
"%s" % gid,
self.blueprint.generate_browser_node(
"%d" % (server.id),
"%d" % gid,
server.name,
"icon-%s-not-connected" % ServerModule.NODE_TYPE,
"icon-server-not-connected" if not connected else
"icon-{0}".format(module.NODE_TYPE),
True,
ServerModule.NODE_TYPE)
)
self.node_type,
connected=connected,
server_type=module.type if module is not None else 'PG'
)
)
return make_json_response(result=res)
def delete(self, gid, sid):
"""Delete a server node in the settings database"""
"""Delete a server node in the settings database."""
servers = Server.query.filter_by(user_id=current_user.id, id=sid)
# TODO:: A server, which is connected, can not be deleted
@ -130,7 +244,8 @@ class ServerNode(NodeView):
def update(self, gid, sid):
"""Update the server settings"""
server = Server.query.filter_by(user_id=current_user.id, id=sid).first()
server = Server.query.filter_by(
user_id=current_user.id, id=sid).first()
if server is None:
return make_json_response(
@ -138,9 +253,8 @@ class ServerNode(NodeView):
errormsg=gettext("Couldn't find the given server.")
)
# TODO::
# Not all parameters can be modified, while the server is connected
possible_args = {
# Not all parameters can be modified, while the server is connected
config_param_map = {
'name': 'name',
'host': 'host',
'port': 'port',
@ -148,21 +262,49 @@ class ServerNode(NodeView):
'username': 'username',
'sslmode': 'sslmode',
'gid': 'servergroup_id',
'comment': 'comment'
'comment': 'comment',
'role': 'role'
}
disp_lbl = {
'name': gettext('name'),
'host': gettext('Host name/address'),
'port': gettext('Port'),
'db': gettext('Maintenance database'),
'username': gettext('Username'),
'sslmode': gettext('SSL Mode'),
'comment': gettext('Comments'),
'role': gettext('Role')
}
idx = 0
data = request.form if request.form else json.loads(request.data)
for arg in possible_args:
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
not_allowed = {}
if conn.connected():
for arg in {
'host', 'port', 'db', 'username', 'sslmode', 'role'
}:
if arg in data:
return forbidden(
errormsg=gettext(
"'{0}' is not allowed to modify, when server is connected."
).format(disp_lbl[arg])
)
for arg in config_param_map:
if arg in data:
setattr(server, possible_args[arg], data[arg])
setattr(server, config_param_map[arg], data[arg])
idx += 1
if idx == 0:
return make_json_response(
success=0,
errormsg=gettext('No parameters were chagned!')
errormsg=gettext('No parameters were changed!')
)
try:
@ -173,11 +315,14 @@ class ServerNode(NodeView):
errormsg=e.message
)
manager.update(server)
return make_json_response(
success=1,
data={
'id': server.id,
'gid': server.servergroup_id
'gid': server.servergroup_id,
'icon': 'icon-server-not-connected'
}
)
@ -198,6 +343,14 @@ class ServerNode(NodeView):
id=server.servergroup_id
).first()
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(sid)
conn = manager.connection()
connected = conn.connected()
module = getattr(manager, 'module', None)
return ajax_response(
response={
'id': server.id,
@ -209,9 +362,10 @@ class ServerNode(NodeView):
'gid': server.servergroup_id,
'group-name': sg.name,
'comment': server.comment,
# TODO:: Make sure - we do have correct values here
'connected': True,
'version': 'PostgreSQL 9.3 (linux-x64)'
'role': server.role,
'connected': connected,
'version': manager.ver,
'server_type': module.type if module is not None else 'PG'
}
)
@ -223,7 +377,8 @@ class ServerNode(NodeView):
u'port',
u'db',
u'username',
u'sslmode'
u'sslmode',
u'role'
]
data = request.form if request.form else json.loads(request.data)
@ -247,19 +402,25 @@ class ServerNode(NodeView):
port=data[u'port'],
maintenance_db=data[u'db'],
username=data[u'username'],
ssl_mode=data['sslmode'],
comment=data['comment'] if 'comment' in data else None
ssl_mode=data[u'sslmode'],
comment=data[u'comment'] if u'comment' in data else None,
role=data[u'role'] if u'role' in data else None
)
db.session.add(server)
db.session.commit()
return jsonify(node=generate_browser_node(
'%s' % server.id,
'%s' % gid,
'%s' % server.name,
"icon-{0}-not-connected".format(ServerModule.NODE_TYPE),
True,
ServerModule.NODE_TYPE))
return jsonify(
node=self.blueprint.generate_browser_node(
"%d" % (server.id),
"%d" % gid,
server.name,
"icon-server-not-connected",
True,
self.node_type,
connected=False,
server_type='PG' # Default server type
)
)
except Exception as e:
return make_json_response(
@ -270,12 +431,27 @@ class ServerNode(NodeView):
def nodes(self, gid, sid):
"""Build a list of treeview nodes from the child nodes."""
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
if not conn.connected():
return precondition_required(
gettext(
"Please make a connection to the server first!"
)
)
nodes = []
# TODO::
# We can have nodes for the server object, only when
# the server is connected at the moment.
for module in blueprint.submodules:
nodes.extend(module.get_nodes(server=sid))
# We will rely on individual server type modules to generate nodes for
# them selves.
module = getattr(manager, 'module', None)
if module:
nodes.extend(module.get_nodes(sid=sid, manager=manager))
return make_json_response(data=nodes)
def sql(self, gid, sid):
@ -299,9 +475,180 @@ class ServerNode(NodeView):
Override this property for your own logic.
"""
return make_response(
render_template("servers/servers.js"),
render_template(
"servers/servers.js",
server_types=sorted(
[
m for m in self.blueprint.submodules
if isinstance(m, ServerTypeModule)
],
key=lambda x: x.priority
),
_=gettext
),
200, {'Content-Type': 'application/x-javascript'}
)
def connect_status(self, gid, sid):
"""Check and return the connection status."""
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.get_connection()
return make_json_response(data={'connected': conn.connected()})
def connect(self, gid, sid):
"""
Connect the Server and return the connection object.
Verification Process before Connection:
Verify requested server.
Check the server password is already been stored in the
database or not.
If Yes, connect the server and return connection.
If No, Raise HTTP error and ask for the password.
In case of 'Save Password' request from user, excrypted Pasword
will be stored in the respected server database and
establish the connection OR just connect the server and do not
store the password.
"""
current_app.logger.info(
'Connection Request for server#{0}'.format(sid)
)
# Fetch Server Details
server = Server.query.filter_by(id=sid).first()
if server is None:
return bad_request(gettext("Server Not Found."))
# Fetch User Details.
user = User.query.filter_by(id=current_user.id).first()
if user is None:
return unauthorized(gettext("Unauthorized Request."))
data = request.form if request.form else json.loads(request.data) if \
request.data else {}
password = None
save_password = False
if 'password' not in data:
if server.password is None:
# Return the password template in case password is not
# provided, or password has not been saved earlier.
return make_json_response(
success=0,
status=428,
result=render_template(
'servers/password.html',
server_label=server.name,
username=server.username,
_=gettext
)
)
else:
password = data['password'] if 'password' in data else None
save_password = \
data['save_password'] if password and \
'save_password' in data else False
# Encrypt the password before saving with user's login password key.
try:
password = encrypt(password, user.password) \
if password is not None else server.password
except Exception as e:
return internal_server_error(errormsg=e.message)
# Connect the Server
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
try:
status, errmsg = conn.connect(
password=password,
modules=[
m for m in self.blueprint.submodules
if isinstance(m, ServerTypeModule)
]
)
except Exception as e:
# TODO::
# Ask the password again (if existing password couldn't be
# descrypted)
return internal_server_error(errormsg=e.message)
if not status:
current_app.logger.error(
"Could not connected to server(#{0}) - '{1}'.\nError: {2}".format(
server.id, server.name, errmsg
)
)
return make_json_response(
success=0,
status=401,
result=render_template(
'servers/password.html',
server_label=server.name,
username=server.username,
errmsg=errmsg,
_=gettext
)
)
else:
if save_password:
try:
# Save the encrypted password using the user's login
# password key.
setattr(server, 'password', password)
db.session.commit()
except Exception as e:
# Release Connection
manager.release(database=server.maintenance_db)
conn = None
return internal_server_error(errormsg=e.message)
current_app.logger.info('Connection Established for server: \
%s - %s' % (server.id, server.name))
return make_json_response(
success=1,
info=gettext("Server Connected."),
data={
'icon': 'icon-{0}'.format(
manager.module.NODE_TYPE
),
'connected': True
}
)
def disconnect(self, gid, sid):
"""Disconnect the Server."""
server = Server.query.filter_by(id=sid).first()
if server is None:
return bad_request(gettext("Server Not Found."))
# Release Connection
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
status = manager.release()
if not status:
return unauthorized(gettext("Server Could Not Disconnect."))
else:
return make_json_response(
success=1,
info=gettext("Server Disconnected."),
data={
'icon': 'icon-server-not-connected',
'connected': False
}
)
ServerNode.register_node_view(blueprint)

View File

@ -0,0 +1,7 @@
.icon-{{node_type}} {
background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/%s.png' % node_type )}}') !important;
background-repeat: no-repeat;
align-content: center;
vertical-align: middle;
height: 1.3em;
}

View File

@ -0,0 +1,17 @@
.icon-server {
background-image: url('{{ url_for('NODE-server.static', filename='img/server.png') }}') !important;
border-radius: 10px
}
.icon-server-not-connected {
background-image: url('{{ url_for('NODE-server.static', filename='img/serverbad.png') }}') !important;
border-radius: 10px
}
.icon-server-connecting {
background-image: url('{{ url_for('browser.static', filename='css/aciTree/image/load-node.gif')}}') !important;
background-repeat: no-repeat;
align-content: center;
vertical-align: middle;
height: 1.3em;
}

View File

@ -0,0 +1,19 @@
<form name="frmPassword" id="frmPassword" style="height: 100%; width: 100%" onsubmit="return false;">
<div>{% if errmsg %}
<div class="highlight has-error">
<div class='control-label'>{{ errmsg }}</div>
</div>{% endif %}
<div><b>{{ _('Please enter the password for the user \'{0}\' to connect the server - "{1}"').format(username, server_label) }}</b></div>
<div style="padding: 5px; height: 1px;"></div>
<div style="width: 100%">
<span style="width: 25%;display: inline-table;">Password</span>
<span style="width: 73%;display: inline-block;">
<input style="width:100%" id="password" class="form-control" name="password" type="password">
</span>
<span style="margin-left: 25%; padding-top: 15px;width: 45%;display: inline-block;">
<input id="save_password" name="save_password" type="checkbox">&nbsp;&nbsp;Save Password
</span>
</div>
<div style="padding: 5px; height: 1px;"></div>
</div>
</form>

View File

@ -1,6 +1,6 @@
define(
['jquery', 'underscore', 'pgadmin', 'pgadmin.browser', 'alertify'],
function($, _, pgAdmin, pgBrowser, alertify) {
['jquery', 'underscore', 'underscore.string', 'pgadmin', 'pgadmin.browser', 'alertify'],
function($, _, S, pgAdmin, pgBrowser, alertify) {
if (!pgBrowser.Nodes['server']) {
pgAdmin.Browser.Nodes['server'] = pgAdmin.Browser.Node.extend({
@ -29,76 +29,99 @@ function($, _, pgAdmin, pgBrowser, alertify) {
name: 'drop_server', node: 'server', module: this,
applies: ['object', 'context'], callback: 'delete_obj',
category: 'drop', priority: 3, label: '{{ _('Drop Server...') }}',
icon: 'fa fa-trash'
icon: 'fa fa-trash', enable: 'is_not_connected'
},{
name: 'connect_server', node: 'server', module: this,
applies: ['object', 'context'], callback: 'connect_server',
category: 'connect', priority: 4, label: '{{ _('Connect Server...') }}',
icon: 'fa fa-link', enable : 'is_not_connected'
},
{
name: 'disconnect_server', node: 'server', module: this,
applies: ['object', 'context'], callback: 'disconnect_server',
category: 'drop', priority: 5, label: '{{ _('Disconnect Server...') }}',
icon: 'fa fa-chain-broken', enable : 'is_connected'
}]);
},
is_not_connected: function(node) {
return (node && node.connected != true);
},
is_connected: function(node) {
return (node && node.connected == true);
},
callbacks: {
// Add a server
create_server: function (item) {
var alert = alertify.prompt(
'{{ _('Create a server') }}',
'{{ _('Enter a name for the new server') }}',
'',
function(evt, value) {
var d = tree.itemData(item);
if (d._type != 'server-group') {
d = tree.itemData(tree.parent(item));
}
$.post(
"{{ url_for('browser.index') }}server/obj/" + d.refid + '/' + d.id + '/',
{ name: value }
)
.done(function(data) {
if (data.success == 0) {
report_error(data.errormsg, data.info);
} else {
var item = {
id: data.data.id,
label: data.data.name,
inode: true,
open: false,
icon: 'icon-server-not-connected'
}
tree.append(null, {
itemData: item
});
/* Connect the server */
connect_server: function(args){
var input = args || {};
obj = this,
t = pgBrowser.tree,
i = input.item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
if (!d)
return false;
connect_to_server(obj, d, t, i);
return false;
},
/* Disconnect the server */
disconnect_server: function(args) {
var input = args || {};
obj = this,
t = pgBrowser.tree,
i = input.item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
if (!d)
return false;
alertify.confirm(
'{{ _('Disconnect the server') }}',
S('{{ _('Are you sure you want to disconnect the server - %%s ?') }}').sprintf(d.label).value(),
function(evt) {
$.ajax({
url: obj.generate_url('connect', d, true),
type:'DELETE',
success: function(res) {
if (res.success == 1) {
alertify.success("{{ _('" + res.info + "') }}");
t.removeIcon(i);
d.connected = false;
d.icon = 'icon-server-not-connected';
t.addIcon(i, {icon: d.icon});
t.unload(i);
t.setInode(i);
}
});
},
null
);
alert.show();
},
error: function(xhr, status, error) {
try {
var err = $.parseJSON(xhr.responseText);
if (err.success == 0) {
msg = S('{{ _(' + err.errormsg + ')}}').value();
alertify.error("{{ _('" + err.errormsg + "') }}");
}
} catch (e) {}
t.unload(i);
}
});
},
function(evt) {
return true;
});
return false;
},
/* Connect the server (if not connected), before opening this node */
beforeopen: function(o) {
o.browser.tree.removeIcon(o.item);
if (o.data.connected) {
o.browser.tree.addIcon(o.item, {icon: 'icon-server-connected'});
} else {
o.browser.tree.addIcon(o.item, {icon: 'icon-server-not-connected'});
}
var data = o.data;
if(!data || data._type != 'server') {
return false;
}
o.browser.tree.addIcon(o.item, {icon: data.icon});
if (!data.connected) {
alertify.confirm(
'{{ _('Connect to server') }}',
'{{ _('Do you want to connect the server?') }}',
function(evt) {
$.post(
"{{ url_for('browser.index') }}server/connect/" + data.refid + '/'
).done(function(data) {
if (data.success == 0) {
report_error(data.errormsg, data.info);
}
}).fail(function() {});
return true;
},
function(evt) {
return true;
}
);
connect_to_server(this, data, o.browser.tree, o.item);
return false;
}
return true;
@ -107,40 +130,61 @@ function($, _, pgAdmin, pgBrowser, alertify) {
model: pgAdmin.Browser.Node.Model.extend({
defaults: {
id: undefined,
name: undefined,
sslmode: 'prefer'
name: null,
sslmode: 'prefer',
host: null,
port: 5432,
db: null,
username: null,
role: null
},
schema: [{
id: 'id', label: 'ID', type: 'int', group: null,
id: 'id', label: '{{ _('ID') }}', type: 'int', group: null,
mode: ['properties']
},{
id: 'name', label:'Name', type: 'text', group: null,
id: 'name', label:'{{ _('Name') }}', type: 'text', group: null,
mode: ['properties', 'edit', 'create']
},{
id: 'connected', label:'Connected', type: 'text', group: null,
id: 'connected', label:'{{ _('Connected') }}', type: 'text', group: null,
mode: ['properties']
},{
id: 'version', label:'Version', type: 'text', group: null,
id: 'version', label:'{{ _('Version') }}', type: 'text', group: null,
mode: ['properties'], show: 'isConnected'
},{
id: 'comment', label:'Comments:', type: 'multiline', group: null,
mode: ['properties', 'edit', 'create'], disable: 'notEditMode'
id: 'comment', label:'{{ _('Comments:') }}', type: 'multiline', group: null,
mode: ['properties', 'edit', 'create'], disabled: 'notEditMode'
},{
id: 'host', label:'Host Name/Address', type: 'text', group: "Connection",
mode: ['properties', 'edit', 'create']
id: 'host', label:'{{ _('Host Name/Address') }}', type: 'text', group: "Connection",
mode: ['properties', 'edit', 'create'], disabled: 'isConnected'
},{
id: 'port', label:'Port', type: 'int', group: "Connection",
mode: ['properties', 'edit', 'create']
id: 'port', label:'{{ _('Port') }}', type: 'int', group: "Connection",
mode: ['properties', 'edit', 'create'], disabled: 'isConnected'
},{
id: 'db', label:'Maintenance Database', type: 'text', group: "Connection",
mode: ['properties', 'edit', 'create']
id: 'db', label:'{{ _('Maintenance Database') }}', type: 'text', group: "Connection",
mode: ['properties', 'edit', 'create'], disabled: 'isConnected'
},{
id: 'username', label:'User Name', type: 'text', group: "Connection",
mode: ['properties', 'edit', 'create']
id: 'username', label:'{{ _('User Name') }}', type: 'text', group: "Connection",
mode: ['properties', 'edit', 'create'], disabled: 'isConnected'
},{
id: 'sslmode', label:'SSL Mode', type: 'options', group: "Connection",
mode: ['properties', 'edit', 'create'],
'options': [{label:'Allow', value:'allow'}, {label: 'Prefer', value:'prefer'}, {label: 'Require', value: 'require'}, {label: 'Disable', value:'disable'}, {label:'Verify-CA', value: 'verify-ca'}, {label:'Verify-Full', value:'verify-full'}]
id: 'role', label:'{{ _('Role') }}', type: 'text', group: "Connection",
mode: ['properties', 'edit', 'create'], disabled: 'isConnected'
},{
id: 'sslmode', label:'{{ _('SSL Mode') }}', type: 'options', group: "Connection",
mode: ['properties', 'edit', 'create'], disabled: 'isConnected',
'options': [
{label: 'Allow', value: 'allow'},
{label: 'Prefer', value: 'prefer'},
{label: 'Require', value: 'require'},
{label: 'Disable', value: 'disable'},
{label: 'Verify-CA', value: 'verify-ca'},
{label: 'Verify-Full', value: 'verify-full'}
]
},{
id: 'server_type', label: '{{ _('Server Type') }}', type: 'options',
mode: ['properties'], show: 'isConnected',
'options': [{% set cnt = 1 %}{% for server_type in server_types %}{% if cnt != 1 %},{% endif %}
{label: '{{ server_type.description }}', value: '{{ server_type.type}}'}{% set cnt = cnt + 1 %}{% endfor %}
]
}],
validate: function(attrs, options) {
if (!this.isNew() && 'id' in this.changed) {
@ -151,13 +195,145 @@ function($, _, pgAdmin, pgBrowser, alertify) {
}
return null;
},
isConnected: function(mode) {
return mode == 'properties' && this.get('connected');
isConnected: function(model) {
return model.get('connected');
}
})
});
function connect_to_server(obj, data, tree, item) {
var onFailure = function(xhr, status, error, _model, _data, _tree, _item) {
tree.setInode(_item);
tree.addIcon(_item, {icon: 'icon-server-not-connected'});
alertify.pgNotifier('error', xhr, error, function(msg) {
setTimeout(function() {
alertify.dlgServerPass(
'{{ _('Connect to Server') }}',
msg, _model, _data, _tree, _item
).resizeTo();
}, 100);
});
},
onSuccess = function(res, model, data, tree, item) {
tree.deselect(item);
tree.setInode(item);
if (res && res.data) {
if(typeof res.data.connected == 'boolean') {
data.connected = res.data.connected;
}
if (typeof res.data.icon == 'string') {
tree.removeIcon(item);
data.icon = res.data.icon;
tree.addIcon(item, {icon: data.icon});
}
alertify.success(res.info);
setTimeout(function() {tree.select(item);}, 10);
setTimeout(function() {tree.open(item);}, 100);
}
};
// Ask Password and send it back to the connect server
if (!alertify.dlgServerPass) {
alertify.dialog('dlgServerPass', function factory() {
return {
main: function(title, message, model, data, tree, item) {
this.set('title', title);
this.message = message;
this.tree = tree;
this.nodeData = data;
this.nodeItem = item;
this.nodeModel = model;
},
setup:function() {
return {
buttons:[
{
text: "{{ _('OK') }}", key: 13, className: "btn btn-primary"
},
{
text: "{{ _('Cancel') }}", className: "btn btn-danger"
}
],
focus: { element: '#password', select: true },
options: {
modal: 0, resizable: false, maximizable: false, pinnable: false
}
};
},
build:function() {},
prepare:function() {
this.setContent(this.message);
},
callback: function(closeEvent) {
var _sdata = this.nodeData,
_tree = this.tree,
_item = this.nodeItem,
_model = this.nodeModel;
if (closeEvent.button.text == "{{ _('OK') }}") {
var _url = _model.generate_url('connect', _sdata, true);
_tree.setLeaf(_item);
_tree.removeIcon(_item);
_tree.addIcon(_item, {icon: 'icon-server-connecting'});
$.ajax({
type: 'POST',
timeout: 30000,
url: _url,
data: $('#frmPassword').serialize(),
success: function(res) {
return onSuccess(
res, _model, _sdata, _tree, _item
);
},
error: function(xhr, status, error) {
return onFailure(
xhr, status, error, _model, _sdata, _tree, _item
);
}
});
} else {
_tree.setInode(_item);
_tree.removeIcon(_item);
_tree.addIcon(_item, {icon: 'icon-server-not-connected'});
}
}
};
});
}
alertify.confirm(
'{{ _('Connect to server') }}',
'{{ _('Do you want to connect the server?') }}',
function(evt) {
url = obj.generate_url("connect", data, true);
$.post(url)
.done(
function(res) {
if (res.success == 1) {
return onSuccess(res, obj, data, tree, item);
}
})
.fail(
function(xhr, status, error) {
return onFailure(xhr, status, error, obj, data, tree, item);
});
},
notEditMode: function(mode) {
return mode != 'edit';
}})
});
function() {});
}
/* Send PING to indicate that session is alive */
function server_status(server_id)
{
url = "/ping";
$.post(url)
.done(function(data) { return true})
.fail(function(xhr, status, error) { return false})
}
}
return pgBrowser.Nodes['server'];

View File

@ -29,7 +29,7 @@ function($, _, pgAdmin, Backbone) {
model: pgAdmin.Browser.Node.Model.extend({
defaults: {
id: undefined,
name: undefined
name: null
},
schema: [
{id: 'id', label: 'ID', type: 'int', group: null, mode: ['properties']},

View File

@ -119,7 +119,6 @@ OWNER TO helpdesk;\n';
width: 500,
isCloseable: false,
isPrivate: true,
// TODO:: Revove demoSql later
content: '<textarea id="sql-textarea" name="sql-textarea"></textarea>',
events: sqlPanelEvents
}),
@ -163,7 +162,7 @@ OWNER TO helpdesk;\n';
width: 500,
isCloseable: false,
isPrivate: true,
url: 'http://www.pgadmin.org'
url: 'about:blank' // http://www.pgadmin.org'
})/* Add hooked-in frames by extensions */{% for panel_item in current_app.panels %}{% if panel_item.isIframe %},
'{{ panel_item.name }}' : new pgAdmin.Browser.Frame({
name: '{{ panel_item.name }}',
@ -280,7 +279,6 @@ OWNER TO helpdesk;\n';
});
}
}
},
init: function() {
var obj=this;
@ -343,6 +341,7 @@ OWNER TO helpdesk;\n';
mode: "text/x-sql",
readOnly: true
});
// TODO:: Revove demoSql later
$('#sql-textarea').val(demoSql);
// Initialise the treeview
@ -378,7 +377,7 @@ OWNER TO helpdesk;\n';
var o = undefined;
_.each(menus, function(m) {
if (name == (m.module.type + '_' + m.callback)) {
if (name == (m.module.type + '_' + m.name)) {
o = m;
}
});
@ -406,20 +405,22 @@ OWNER TO helpdesk;\n';
_.each(
_.sortBy(menus, function(m) { return m.priority; }),
function(m) {
if (m.category == 'create' && !m.disabled(d))
createMenu[m.module.type + '_' + m.callback] = { name: m.label };
if (m.category == 'create' && !m.disabled(d)) {
createMenu[m.module.type + '_' + m.name] = { name: m.label, icon: m.module.type };
}
});
if (_.size(createMenu)) {
menu["create"] = { "name": "{{ _('Create') }}" };
menu["create"] = { name: "{{ _('Create') }}", icon: 'fa fa-magic' };
menu["create"]["items"] = createMenu;
}
_.each(
_.sortBy(menus, function(m) { return m.priority; }),
function(m) {
if (m.category != 'create' && !m.disabled(d))
menu[m.module.type + '_' + m.callback] = { name: m.label };
if (m.category != 'create' && !m.disabled(d)) {
menu[m.module.type + '_' + m.name] = { name: m.label, icon: m.icon };
}
});
return _.size(menu) ? {
@ -516,6 +517,16 @@ OWNER TO helpdesk;\n';
}
};
geneate_menus();
// Ping the server every 5 minutes
setInterval(function() {
$.ajax({
url: '{{ url_for('misc.ping') }}',
type:'POST',
success: function() {},
error: function() {}
});
}, 300000);
},
// load the module right now
load_module: function(name, path, c) {
@ -532,7 +543,6 @@ OWNER TO helpdesk;\n';
if (c)
c.loaded += 1;
}, function() {
/* TODO:: Show proper error */
obj.report_error(
'{{ _('Error loading script - ') }}' + path);
});
@ -557,7 +567,7 @@ OWNER TO helpdesk;\n';
}
if (_.has(menus, m.name)) {
console.log(m.name +
console && console.log && console.log(m.name +
' has been ignored!\nIt is already exist in the ' +
a +
' list of menus!');
@ -605,6 +615,10 @@ OWNER TO helpdesk;\n';
navbar.children('#mnu_obj').removeClass('hide');
});
obj.enable_disable_menus();
},
messages: {
'server_lost': '{{ _('Connection to the server has been lost!') }}',
'click_for_detailed_msg': '{{ _('%s<br><br>click here for details!') }}'
}
});

View File

@ -15,7 +15,7 @@ function(_, alertify, pgAdmin) {
</h4>\
</div>\
<div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne">\
<div class="panel-body" style="overflow: scroll;">' + message + '</div>\
<div class="panel-body" style="overflow: auto;">' + unescape(message) + '</div>\
</div>\
</div>';
@ -28,7 +28,7 @@ function(_, alertify, pgAdmin) {
</h4>\
</div>\
<div id="collapseTwo" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingTwo">\
<div class="panel-body" style="overflow: scroll;">' + unescape(info) + '</div>\
<div class="panel-body" style="overflow: auto;">' + unescape(info) + '</div>\
</div>\
</div>\
</div>'

View File

@ -1,6 +1,7 @@
define(
['jquery', 'underscore', 'underscore.string', 'pgadmin', 'pgadmin.browser.menu',
'backbone', 'alertify', 'backform', 'pgadmin.backform', 'wcdocker'],
'backbone', 'alertify', 'backform', 'pgadmin.backform', 'wcdocker',
'pgadmin.alertifyjs'],
function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
var pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
@ -54,7 +55,7 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
'numeric': 'uneditable-input',
'date': 'date',
'boolean': 'bool-text',
'options': 'uneditable-input',
'options': Backform.ReadonlyOptionControl,
'multiline': 'textarea'
},
'edit': {
@ -102,6 +103,11 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
applies: ['object', 'context'], callback: 'show_obj_properties',
priority: 3, label: '{{ _("Properties...") }}',
data: {'action': 'edit'}, icon: 'fa fa-pencil-square-o'
}, {
name: 'refresh', node: this.type, module: this,
applies: ['object', 'context'], callback: 'refresh_node',
priority: 2, label: '{{ _("Refresh...") }}',
icon: 'fa fa-refresh'
}]);
},
///////
@ -161,9 +167,9 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
name: f.id, label: f.label,
control: controlType[type][f.type],
// Do we need to show this control in this mode?
show: f.show && newModel[f.show] &&
visible: f.show && newModel[f.show] &&
typeof newModel[f.show] == "function" ?
newModel[f.show] : undefined,
newModel[f.show] : f.show,
// This can be disabled in some cases (if not hidden)
disabled: (type == 'properties' ? true : (
f.disabled && newModel[f.disabled] &&
@ -217,8 +223,15 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
}
}
})
.error(function() {
// TODO:: Handle the error message properly.
.error(function(m, jqxhr) {
// TODO:: We may not want to continue from here
console.log(arguments);
Alertify.pgNotifier(
"error", jqxhr,
S(
"{{ _("Error fetching the properties - %%s!") }}"
).sprintf(jqxhr.statusText).value()
);
});
} else {
// Yay - render the view now!
@ -440,6 +453,7 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
t.select(n);
}
}
return true;
},
error: function(jqx) {
var msg = jqx.responseText;
@ -462,9 +476,10 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
},
// Callback called - when a node is selected in browser tree.
selected: function(o) {
// Show (One of these, whose panel is open)
// Show the information about the selected node in the below panels,
// which are visible at this time:
// + Properties
// + Query
// + Query (if applicable, otherwise empty)
// + Dependents
// + Dependencies
// + Statistics
@ -482,32 +497,39 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
// is active).
this.showProperties(o.item, o.data,
pgBrowser.panels['properties'].panel);
} else if ('sql' in br.panels &&
}
if ('sql' in br.panels &&
br.panels['sql'] &&
br.panels['sql'].panel &&
br.panels['sql'].panel.isVisible()) {
// Show reverse engineered query for this object (when
// the 'sql' tab is active.)
} else if ('statistics' in br.panels &&
// TODO:: Show reverse engineered query for this object (when 'sql'
// tab is active.)
}
if ('statistics' in br.panels &&
br.panels['statistics'] &&
br.panels['statistics'].panel &&
br.panels['statistics'].panel.isVisible()) {
// Show statistics for this object (when the
// 'statistics' tab is active.)
} else if ('dependencies' in br.panels &&
// TODO:: Show statistics for this object (when the 'statistics'
// tab is active.)
}
if ('dependencies' in br.panels &&
br.panels['dependencies'] &&
br.panels['dependencies'].panel &&
br.panels['dependencies'].panel.isVisible()) {
// Show dependencies for this object (when the
// TODO:: Show dependencies for this object (when the
// 'dependencies' tab is active.)
} else if ('dependents' in br.panels &&
}
if ('dependents' in br.panels &&
br.panels['dependents'] &&
br.panels['dependents'].panel &&
br.panels['dependents'].panel.isVisible()) {
// Show dependents for this object (when the
// 'dependents' tab is active.)
// TODO:: Show dependents for this object (when the 'dependents'
// tab is active.)
}
}
},
refresh_node: function(args) {
this.callbacks.selected();
}
},
/**********************************************************************
@ -644,19 +666,17 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
if (c && !_.isEmpty(c)) {
m.save({} ,{
attrs: (m.isNew() ?
m.attributes :
m.changedAttributes()),
attrs: m.attributes,
success: function() {
onSaveFunc.call();
},
error: function() {
/* Reset the changed attributes on failure */
m.changed = c;
/* TODO:: Alert for the user on error */
console.log('ERROR:');
console.log(arguments);
error: function(m, jqxhr) {
Alertify.pgNotifier(
"error", jqxhr,
S(
"{{ _("Error during saving properties - %%s!") }}"
).sprintf(jqxhr.statusText).value()
);
}
});
}
@ -707,14 +727,8 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
if (view.model.tnode) {
var d = _.extend({}, view.model.tnode),
func = function(i) {
/* Register this panel for this node */
pgBrowser.Node.panels =
pgBrowser.Node.panels || {};
pgBrowser.Node.panels[d.id] = panel;
panel.title(that.title(d));
setTimeout(function() {
that.showProperties(i, d, panel,
'properties');
closePanel();
}, 0);
tree.setVisible(i);
tree.select(i);
@ -818,7 +832,7 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
opURL = {
'create': 'obj', 'drop': 'obj', 'edit': 'obj',
'properties': 'obj', 'depends': 'deps',
'statistics': 'stats'
'statistics': 'stats', 'collections': 'nodes'
};
if (d._type == this.type) {
@ -856,5 +870,105 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
})
});
pgBrowser.Collection = _.extend(_.clone(pgAdmin.Browser.Node), {
///////
// Initialization function
// Generally - used to register the menus for this type of node.
//
// Also, look at pgAdmin.Browser.add_menus(...) function.
//
// Collection will not have 'Properties' menu.
//
// NOTE: Override this for each node for initialization purpose
Init: function() {
if (this.node_initialized)
return;
this.node_initialized = true;
pgAdmin.Browser.add_menus([{
name: 'refresh', node: this.type, module: this,
applies: ['object', 'context'], callback: 'refresh_collection',
priority: 2, label: '{{ _("Refresh...") }}',
icon: 'fa fa-refresh'
}]);
},
callbacks: {
refresh_collection: function() {
// TODO:: Refresh the collection node
console.log(arguments);
},
selected: function(o) {
// Show (Node information on these panels, which one is visible.)
// + Properties (list down the children nodes in pages)
// + Query (Remove existing SQL)
// + Dependents (Remove dependents)
// + Dependencies (Remove dependencies)
// + Statistics (TODO:: Check the current implementation in pgAdmin 3)
// Update the menu items
pgAdmin.Browser.enable_disable_menus.apply(o.browser, [o.item]);
if (o && o.data && o.browser) {
var br = o.browser;
if ('properties' in br.panels &&
br.panels['properties'] &&
br.panels['properties'].panel &&
br.panels['properties'].panel.isVisible()) {
// Show object properties (only when the 'properties' tab
// is active).
this.showProperties(o.item, o.data,
pgBrowser.panels['properties'].panel);
}
if ('sql' in br.panels &&
br.panels['sql'] &&
br.panels['sql'].panel &&
br.panels['sql'].panel.isVisible()) {
// TODO::
// Remove the information from the sql pane
}
if ('statistics' in br.panels &&
br.panels['statistics'] &&
br.panels['statistics'].panel &&
br.panels['statistics'].panel.isVisible()) {
// TODO::
// Remove information from the statistics pane
}
if ('dependencies' in br.panels &&
br.panels['dependencies'] &&
br.panels['dependencies'].panel &&
br.panels['dependencies'].panel.isVisible()) {
// TODO::
// Remove information from the dependencies pane
}
if ('dependents' in br.panels &&
br.panels['dependents'] &&
br.panels['dependents'].panel &&
br.panels['dependents'].panel.isVisible()) {
// TODO::
// Remove information from the dependents pane
}
}
}
},
/**********************************************************************
* A hook (not a callback) to show object properties in given HTML
* element.
*
* This has been used for the showing, editing properties of the node.
* This has also been used for creating a node.
**/
showProperties: function(item, data, panel) {
var that = this,
tree = pgAdmin.Browser.tree,
j = panel.$container.find('.obj_properties').first(),
view = j.data('obj-view'),
content = $('<div></div>')
.addClass('pg-prop-content col-xs-12');
// TODO:: Show list of children in paging mode
return;
}
});
return pgAdmin.Browser.Node;
});

View File

@ -9,24 +9,105 @@
"""Browser helper utilities"""
from abc import ABCMeta, abstractmethod, abstractproperty
from collections import OrderedDict
import flask
from flask.views import View, MethodViewType, with_metaclass
from flask.ext.babel import gettext
from config import PG_DEFAULT_DRIVER
from pgadmin.browser import PgAdminModule
from pgadmin.utils.ajax import make_json_response
import gettext
def generate_browser_node(node_id, parent_id, label, icon, inode, node_type):
obj = {
"id": "%s/%s" % (node_type, node_id),
"label": label,
"icon": icon,
"inode": inode,
"_type": node_type,
"_id": node_id,
"refid": parent_id,
"module": 'pgadmin.node.%s' % node_type
}
return obj
class NodeAttr(object):
"""
"""
__metaclass__ = ABCMeta
@abstractmethod
def validate(self, mode, value):
pass
@abstractmethod
def schema(self):
pass
class PGChildModule(object):
"""
class PGChildModule(ServerChildModule)
This is a base class for children/grand-children of PostgreSQL, and
all Postgres Plus version (i.e. Postgres Plus Advanced Server, Green Plum,
etc).
Method:
------
* BackendSupported(manager)
- Return True when it supports certain version.
Uses the psycopg2 server connection manager as input for checking the
compatibility of the current module.
* AddAttr(attr)
- This adds the attribute supported for specific version only. It takes
NodeAttr as input, and update max_ver, min_ver variables for this module.
"""
def __init__(self, *args, **kwargs):
self.min_ver = 1000000000
self.max_ver = 0
self.attributes = {}
super(PGChildModule, self).__init__(*args, **kwargs)
def BackendSupported(self, mangaer):
sversion = getattr(mangaer, 'sversion', None)
if (sversion is None or not isinstance(sversion, int)):
return False
if (self.min_ver is None and self.max_ver is None):
return True
assert(self.max_ver is None or isinstance(self.max_ver, int))
assert(self.min_ver is None or isinstance(self.min_ver, int))
if self.min_ver is None or self.min_ver <= sversion:
if self.max_ver is None or self.max_ver >= sversion:
return True
return False
@abstractmethod
def get_nodes(self, sid=None, **kwargs):
pass
def AddAttr(self, attr):
assert(isinstance(attr, PGNodeAttr))
name = getattr(attr, 'name', None)
assert(name is not None and isinstance(name, str))
assert(name not in self.attributes)
# TODO:: Check for naming convention
min_ver = getattr(attr, 'min_ver', None)
assert(min_ver is None or isinstance(min_ver, int))
max_ver = getattr(attr, 'max_ver', None)
assert(max_ver is None or isinstance(max_ver, int))
self.attributes[name] = attr
if max_ver is None:
self.max_ver = None
elif self.max_var is not None and self.max_ver < max_ver:
self.max_ver < max_ver
if min_ver is None:
self.min_ver = None
elif self.min_ver is not None and self.min_ver > min_ver:
self.min_ver = min_ver
class NodeView(with_metaclass(MethodViewType, View)):
@ -94,7 +175,10 @@ class NodeView(with_metaclass(MethodViewType, View)):
for meth in ops:
meths.append(meth.upper())
if len(meths) > 0:
cmds.append({'cmd': op, 'req': idx == 0, 'with_id': idx != 2, 'methods': meths})
cmds.append({
'cmd': op, 'req': (idx == 0),
'with_id': (idx != 2), 'methods': meths
})
idx += 1
return cmds
@ -148,28 +232,28 @@ class NodeView(with_metaclass(MethodViewType, View)):
assert self.cmd in self.operations, \
"Unimplemented Command ({0}) for {1}".format(
self.cmd,
str(self.__class__.__name__)
)
self.cmd,
str(self.__class__.__name__)
)
has_args, has_id = self.check_args(**kwargs)
assert self.cmd in self.operations and \
(has_id and len(self.operations[self.cmd]) > 0 and \
meth in self.operations[self.cmd][0]) or \
(not has_id and len(self.operations[self.cmd]) > 1 and \
meth in self.operations[self.cmd][1]) or \
(len(self.operations[self.cmd]) > 2 and \
meth in self.operations[self.cmd][2]), \
assert (self.cmd in self.operations and
(has_id and len(self.operations[self.cmd]) > 0 and
meth in self.operations[self.cmd][0]) or
(not has_id and len(self.operations[self.cmd]) > 1 and
meth in self.operations[self.cmd][1]) or
(len(self.operations[self.cmd]) > 2 and
meth in self.operations[self.cmd][2])), \
"Unimplemented method ({0}) for command ({1}), which {2} an id".format(
meth, self.cmd,
'requires' if has_id else 'does not require'
)
meth, self.cmd,
'requires' if has_id else 'does not require'
)
meth = self.operations[self.cmd][0][meth] if has_id else \
self.operations[self.cmd][1][meth] if has_args and \
meth in self.operations[self.cmd][1] else \
self.operations[self.cmd][2][meth]
self.operations[self.cmd][1][meth] if has_args and \
meth in self.operations[self.cmd][1] else \
self.operations[self.cmd][2][meth]
method = getattr(self, meth, None)
@ -177,7 +261,7 @@ class NodeView(with_metaclass(MethodViewType, View)):
return make_json_response(
status=406,
success=0,
errormsg=gettext.gettext(
errormsg=gettext(
"Unimplemented method ({0}) for this url ({1})".format(
meth, flask.request.path)
)
@ -187,6 +271,7 @@ class NodeView(with_metaclass(MethodViewType, View)):
@classmethod
def register_node_view(cls, blueprint):
cls.blueprint = blueprint
id_url, url = cls.get_node_urls()
commands = cls.generate_ops()
@ -221,7 +306,36 @@ class NodeView(with_metaclass(MethodViewType, View)):
"""
return flask.make_response(
flask.render_template(
"{0}/{1}.js".format(self.node_type)
"{0}/{0}.js".format(self.node_type)
),
200, {'Content-Type': 'application/x-javascript'}
)
def nodes(self, *args, **kwargs):
"""Build a list of treeview nodes from the child nodes."""
nodes = []
for module in self.blueprint.submodules:
nodes.extend(module.get_nodes(*args, **kwargs))
return make_json_response(data=nodes)
class PGChildNodeView(NodeView):
def nodes(self, sid=None, **kwargs):
"""Build a list of treeview nodes from the child nodes."""
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
nodes = []
for module in self.blueprint.submodules:
if isinstance(module, ServerChildModule):
if sid is not None and manager is not None and \
module.BackendSupported(manager):
nodes.extend(module.get_nodes(*args, **kwargs))
else:
nodes.extend(module.get_nodes(*args, **kwargs))
return make_json_response(data=nodes)

View File

@ -8,9 +8,13 @@
##########################################################################
"""A blueprint module providing utility functions for the application."""
MODULE_NAME = 'misc'
import datetime
from flask import session, current_app
from pgadmin.utils import PgAdminModule
import pgadmin.utils.driver as driver
MODULE_NAME = 'misc'
# Initialise the module
blueprint = PgAdminModule(MODULE_NAME, __name__,
@ -19,7 +23,11 @@ blueprint = PgAdminModule(MODULE_NAME, __name__,
##########################################################################
# A special URL used to "ping" the server
##########################################################################
@blueprint.route("/ping")
@blueprint.route("/ping", methods=('get', 'post'))
def ping():
"""Generate a "PING" response to indicate that the server is alive."""
driver.ping()
return "PING"

View File

@ -14,8 +14,8 @@ things:
1) Increment SETTINGS_SCHEMA_VERSION in config.py
2) Modify setup.py to ensure that the appropriate changes are made to the config
database to upgrade it to the new version.
2) Modify setup.py to ensure that the appropriate changes are made to the
config database to upgrade it to the new version.
"""
from flask.ext.sqlalchemy import SQLAlchemy
@ -24,9 +24,11 @@ from flask.ext.security import UserMixin, RoleMixin
db = SQLAlchemy()
# Define models
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
roles_users = db.Table(
'roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))
)
class Version(db.Model):
@ -35,6 +37,7 @@ class Version(db.Model):
name = db.Column(db.String(32), primary_key=True)
value = db.Column(db.Integer(), nullable=False)
class Role(db.Model, RoleMixin):
"""Define a security role"""
__tablename__ = 'role'
@ -42,6 +45,7 @@ class Role(db.Model, RoleMixin):
name = db.Column(db.String(128), unique=True, nullable=False)
description = db.Column(db.String(256), nullable=False)
class User(db.Model, UserMixin):
"""Define a user object"""
__tablename__ = 'user'
@ -53,6 +57,7 @@ class User(db.Model, UserMixin):
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
class Setting(db.Model):
"""Define a setting object"""
__tablename__ = 'setting'
@ -60,6 +65,7 @@ class Setting(db.Model):
setting = db.Column(db.String(256), primary_key=True)
value = db.Column(db.String(1024))
class ServerGroup(db.Model):
"""Define a server group for the treeview"""
__tablename__ = 'servergroup'
@ -92,6 +98,8 @@ class Server(db.Model):
nullable=False)
maintenance_db = db.Column(db.String(64), nullable=False)
username = db.Column(db.String(64), nullable=False)
password = db.Column(db.String(64), nullable=True)
role = db.Column(db.String(64), nullable=True)
ssl_mode = db.Column(
db.String(16),
db.CheckConstraint(

View File

@ -416,16 +416,6 @@ iframe {
-o-text-overow: ellipsis;
}
.icon-server-connected {
background-image: url('/browser/server/static/img/server.png') !important;
border-radius: 10px
}
.icon-server-not-connected {
background-image: url('/browser/server/static/img/serverbad.png') !important;
border-radius: 10px
}
.form-control {
color: #333333;
}
@ -435,3 +425,26 @@ iframe {
fieldset[disabled] .form-control {
color: #666666;
}
.highlight {
padding: 5px;
margin-bottom: 5px;
background-color: #F7F7F9;
border: 1px solid #E1E1E8;
border-radius: 3px;
}
.highlight.has-error {
border-color: #A94442;
}
.icon-fa::before {
font: normal normal normal 14px/1 FontAwesome;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transform: translate(0, 0);
display: inline-block;
margin-left: -18px;
margin-right: 9px;
}

View File

@ -5,4 +5,118 @@ function(alertify) {
alertify.defaults.theme.ok = "btn btn-primary";
alertify.defaults.theme.cancel = "btn btn-danger";
alertify.defaults.theme.input = "form-control";
alertify.pgIframeDialog || alertify.dialog('pgIframeDialog', function() {
var iframe;
return {
// dialog constructor function, this will be called when the user calls
// alertify.pgIframeDialog(message)
main:function(message){
//set the videoId setting and return current instance for chaining.
return this.set({
'pg_msg': message
});
},
// we only want to override two options (padding and overflow).
setup: function(){
return {
options:{
//disable both padding and overflow control.
padding : !1,
overflow: !1,
}
};
},
// This will be called once the DOM is ready and will never be invoked
// again. Here we create the iframe to embed the video.
build:function() {
// create the iframe element
iframe = document.createElement('iframe');
iframe.src = "";
iframe.frameBorder = "no";
iframe.width = "100%";
iframe.height = "100%";
// add it to the dialog
this.elements.content.appendChild(iframe);
//give the dialog initial height (half the screen height).
this.elements.body.style.minHeight = screen.height * .5 + 'px';
},
// dialog custom settings
settings:{
pg_msg: undefined
},
// listen and respond to changes in dialog settings.
settingUpdated: function(key, oldValue, newValue){
switch(key){
case 'pg_msg':
var doc = iframe.contentWindow || iframe.contentDocument;
if (doc.document) {
doc = doc.document;
}
doc.open();
doc.write(newValue);
doc.close();
break;
}
},
// listen to internal dialog events.
hooks: {
// triggered when a dialog option gets update.
// warning! this will not be triggered for settings updates.
onupdate: function(option,oldValue, newValue){
switch(option){
case 'resizable':
if(newValue){
this.elements.content.removeAttribute('style');
iframe && iframe.removeAttribute('style');
}else{
this.elements.content.style.minHeight = 'inherit';
iframe && (iframe.style.minHeight = 'inherit');
}
break;
}
}
}
};
});
alertify.pgNotifier = function(type, xhr, promptmsg, onJSONResult) {
var msg = xhr.responseText,
contentType = xhr.getResponseHeader('Content-Type');
if (xhr.status == 0) {
msg = window.pgAdmin.Browser.messages.server_lost;
}
if (contentType) {
try {
if (contentType.indexOf('text/json') == 0) {
resp = $.parseJSON(msg);
if (resp.result != null && (!resp.errormsg || resp.errormsg == '') &&
onJSONResult && typeof(onJSONResult) == 'function') {
return onJSONResult(resp.result);
}
msg = resp.result || resp.errormsg || "Unknown error";
}
} catch (exc) {
}
if (contentType.indexOf('text/html') == 0) {
alertify.notify(
S(
window.pgAdmin.Browser.messages.click_for_detailed_msg
).sprintf(promptmsg).value(), type, 0, function() {
alertify.pgIframeDialog().show().set({ frameless: false }).set('pg_msg', msg);
});
return;
}
}
alertify.alert().show().set('message', msg).set('title', promptmsg);
};
});

View File

@ -66,6 +66,25 @@
'</div>'
].join("\n"));
var ReadonlyOptionControl = Backform.ReadonlyOptionControl = Backform.SelectControl.extend({
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
'<% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>',
' <% if (option.value === rawValue) { %>',
' <span class="<%=Backform.controlClassName%> uneditable-input" disabled><%-option.label%></span>',
' <% } %>',
'<% } %>',
'</div>'
].join("\n")),
events: {},
getValueFromDOM: function() {
return this.formatter.toRaw(this.$el.find("span").text(), this.model);
}
});
// Backform Dialog view (in bootstrap tabbular form)
// A collection of field models.
var Dialog = Backform.Dialog = Backform.Form.extend({

View File

@ -35,6 +35,7 @@
<script type="text/javascript">
require.config({
baseUrl: '',
waitSeconds: 0,
shim: {
"backbone": {
"deps": ['underscore', 'jquery'],

View File

@ -12,7 +12,9 @@
from flask import Response
import json
def make_json_response(success=1, errormsg='', info='', result=None, data=None, status=200):
def make_json_response(success=1, errormsg='', info='', result=None,
data=None, status=200):
"""Create a HTML response document describing the results of a request and
containing the data."""
doc = dict()
@ -22,12 +24,71 @@ def make_json_response(success=1, errormsg='', info='', result=None, data=None,
doc['result'] = result
doc['data'] = data
return Response(response=json.dumps(doc),
status=status,
mimetype="text/json")
return Response(
response=json.dumps(doc),
status=status,
mimetype="text/json"
)
def make_response(response=None, status=200):
"""Create a JSON response handled by the backbone models."""
return Response(response=json.dumps(response),
status=status,
mimetype="text/json")
return Response(
response=json.dumps(response),
status=status,
mimetype="text/json"
)
def internal_server_error(errormsg=''):
"""Create a response with HTTP status code 500 - Internal Server Error."""
return make_json_response(
status=500,
success=0,
errormsg=errormsg
)
def forbidden(errmsg=''):
"""Create a response with HTTP status code 403 - Forbidden."""
return make_json_response(
status=403,
success=0,
errormsg=errmsg
)
def unauthorized(errormsg=''):
"""Create a response with HTTP status code 401 - Unauthorized."""
return make_json_response(
status=401,
success=0,
errormsg=errormsg
)
def bad_request(errormsg=''):
"""Create a response with HTTP status code 400 - Bad Request."""
return make_json_response(
status=400,
success=0,
errormsg=errormsg
)
def precondition_required(errormsg=''):
"""Create a response with HTTP status code 428 - Precondition Required."""
return make_json_response(
status=428,
success=0,
errormsg=errormsg
)
def success_return(message=''):
"""Create a response with HTTP status code 200 - OK."""
return make_json_response(
status=200,
success=1,
info=message
)

View File

@ -0,0 +1,69 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
#########################################################################
"""This File Provides Cryptography."""
from Crypto.Cipher import AES
from Crypto import Random
import base64
padding_string = '}'
def encrypt(plaintext, key):
"""
Encrypt the plaintext with AES method.
Parameters:
plaintext -- String to be encrypted.
key -- Key for encryption.
"""
iv = Random.new().read(AES.block_size)
cipher = AES.new(pad(key), AES.MODE_CFB, iv)
excrypted = base64.b64encode(iv + cipher.encrypt(plaintext))
return excrypted
def decrypt(ciphertext, key):
"""
Decrypt the AES encrypted string.
Parameters:
ciphertext -- Encrypted string with AES method.
key -- key to decrypt the encrypted string.
"""
global padding_string
ciphertext = base64.b64decode(ciphertext)
iv = ciphertext[:AES.block_size]
cipher = AES.new(pad(key), AES.MODE_CFB, iv)
decrypted = cipher.decrypt(ciphertext[AES.block_size:])
l = decrypted.count(padding_string)
return decrypted[:len(decrypted)-l]
def pad(str):
"""Add padding to the key."""
global padding_string
str_len = len(str)
# Key must be maximum 32 bytes long, so take first 32 bytes
if str_len > 32:
return str[:32]
# If key size id 16, 24 or 32 bytes then padding not require
if str_len == 16 or str_len == 24 or str_len == 32:
return str
# Add padding to make key 32 bytes long
return str + ((32 - len(str) % 32) * padding_string)

View File

@ -0,0 +1,33 @@
from flask import current_app
from .registry import DriverRegistry
def get_driver(type):
drivers = getattr(current_app, '_pgadmin_server_drivers', None)
if drivers is None or not isinstance(drivers, dict):
drivers = dict()
if type in drivers:
return drivers[type]
driver = DriverRegistry.create(type)
if driver is not None:
drivers[type] = driver
setattr(current_app, '_pgadmin_server_drivers', drivers)
return driver
def init_app(app):
drivers = dict()
setattr(app, '_pgadmin_server_drivers', drivers)
DriverRegistry.load_drivers()
def ping():
drivers = getattr(current_app, '_pgadmin_server_drivers', None)
for type in drivers:
drivers[type].gc()

View File

@ -0,0 +1,152 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from abc import ABCMeta, abstractmethod, abstractproperty
from flask import session
from .registry import DriverRegistry
class BaseDriver(object):
"""
class BaseDriver(object):
This is a base class for different server types.
Inherit this class to implement different type of database driver
implementation.
(For PostgreSQL/Postgres Plus Advanced Server, we will be using psycopg2)
Abstract Properties:
-------- ----------
* Version (string):
Current version string for the database server
Abstract Methods:
-------- -------
* get_connection(*args, **kwargs)
- It should return a Connection class object, which may/may not be
connected to the database server.
* release_connection(*args, **kwargs)
- Implement the connection release logic
* gc()
- Implement this function to release the connections assigned in the
session, which has not been pinged from more than the idle timeout
configuration.
"""
__metaclass__ = DriverRegistry
@abstractproperty
def Version(cls):
pass
@abstractmethod
def get_connection(self, *args, **kwargs):
pass
@abstractmethod
def release_connection(self, *args, **kwargs):
pass
@abstractmethod
def gc(self):
pass
class BaseConnection(object):
"""
class BaseConnection(object)
It is a base class for database connection. A different connection
drive must implement this to expose abstract methods for this server.
General idea is to create a wrapper around the actaul driver
implementation. It will be instantiated by the driver factory
basically. And, they should not be instantiated directly.
Abstract Methods:
-------- -------
* connect(**kwargs)
- Define this method to connect the server using that particular driver
implementation.
* execute_scalar(query, params)
- Implement this method to execute the given query and returns single
datum result.
* execute_2darray(query, params)
- Implement this method to execute the given query and returns the result
as a 2 dimentional array.
* execute_dict(query, params)
- Implement this method to execute the given query and returns the result
as an array of dict (column name -> value) format.
* connected()
- Implement this method to get the status of the connection. It should
return True for connected, otherwise False
* reset()
- Implement this method to reconnect the database server (if possible)
* transaction_status()
- Implement this method to get the transaction status for this
connection. Range of return values different for each driver type.
* ping()
- Implement this method to ping the server. There are times, a connection
has been lost, but - the connection driver does not know about it. This
can be helpful to figure out the actual reason for query failure.
* _release()
- Implement this method to release the connection object. This should not
be directly called using the connection object itself.
NOTE: Please use BaseDriver.release_connection(...) for releasing the
connection object for better memory management, and connection pool
management.
"""
__metaclass__ = ABCMeta
@abstractmethod
def connect(self, **kwargs):
pass
@abstractmethod
def execute_scalar(self, query, params=None):
pass
@abstractmethod
def execute_2darray(self, query, params=None):
pass
@abstractmethod
def execute_dict(self, query, params=None):
pass
@abstractmethod
def connected(self):
pass
@abstractmethod
def reset(self):
pass
@abstractmethod
def transaction_status(self):
pass
@abstractmethod
def ping(self):
pass
@abstractmethod
def _release(self):
pass

View File

@ -0,0 +1,601 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from datetime import datetime
import psycopg2
import psycopg2.extras
from flask import g, current_app, session
from flask.ext.babel import gettext
from flask.ext.security import current_user
from ..abstract import BaseDriver, BaseConnection
from pgadmin.settings.settings_model import Server, User
from pgadmin.utils.crypto import encrypt, decrypt
_ = gettext
class Connection(BaseConnection):
"""
class Connection(object)
A wrapper class, which wraps the psycopg2 connection object, and
delegate the execution to the actual connection object, when required.
Methods:
-------
* connect(**kwargs)
- Connect the PostgreSQL/Postgres Plus servers using the psycopg2 driver
* execute_scalar(query, params)
- Execute the given query and returns single datum result
* execute_2darray(query, params)
- Execute the given query and returns the result as a 2 dimentional
array.
* execute_dict(query, params)
- Execute the given query and returns the result as an array of dict
(column name -> value) format.
* connected()
- Get the status of the connection.
Returns True if connected, otherwise False.
* reset()
- Reconnect the database server (if possible)
* transaction_status()
- Trasaction Status
* ping()
- Ping the server.
* _release()
- Release the connection object of psycopg2
"""
def __init__(self, manager, conn_id, db, auto_reconnect=True):
assert(manager is not None)
assert(conn_id is not None)
self.conn_id = conn_id
self.manager = manager
self.db = db if db is not None else manager.db
self.conn = None
self.auto_reconnect = auto_reconnect
super(Connection, self).__init__()
def connect(self, **kwargs):
if self.conn:
if self.conn.closed:
self.conn = None
else:
return True, None
pg_conn = None
password = None
mgr = self.manager
if 'password' in kwargs:
encpass = kwargs['password']
else:
encpass = getattr(mgr, 'password', None)
if encpass:
# Fetch Logged in User Details.
user = User.query.filter_by(id=current_user.id).first()
if user is None:
return unauthorized(gettext("Unauthorized Request."))
password = decrypt(encpass, user.password)
try:
import os
os.environ['PGAPPNAME'] = 'pgAdmin IV - {0}'.format(self.conn_id)
pg_conn = psycopg2.connect(
host=mgr.host,
port=mgr.port,
database=self.db,
user=mgr.user,
password=password
)
except psycopg2.Error as e:
msg = e.pgerror if e.pgerror else e.message \
if e.message else e.diag.message_detail \
if e.diag.message_detail else str(e)
return False, msg
pg_conn.autocommit = True
self.conn = pg_conn
self.conn.set_client_encoding("UNICODE")
status, res = self.execute_scalar("""
SET DateStyle=ISO;
SET client_min_messages=notice;
SET bytea_output=escape;
""")
if not status:
self.conn.close()
self.conn = None
return False, res
if mgr.role:
status, res = self.execute_scalar("SET ROLE TO %s", [mgr.role])
if not status:
self.conn.close()
self.conn = None
return False, \
_("Failed to setup the role with error message:\n{0}").format(
res
)
if mgr.ver is None:
status, res = self.execute_scalar("SELECT version()")
if status:
mgr.ver = res
mgr.sversion = int(pg_conn.server_version)
else:
self.conn.close()
self.conn = None
return False, res
if 'password' in kwargs:
mgr.password = kwargs['password']
if 'modules' in kwargs and isinstance(kwargs['modules'], list):
for m in sorted(
kwargs['modules'], key=lambda module: module.priority
):
if m.instanceOf(mgr.ver):
mgr.server_type = m.type
mgr.module = m
break
return True, None
def __cursor(self):
cur = getattr(g, self.conn_id, None)
if self.connected() and cur and not cur.closed:
return True, cur
if not self.connected():
status = False
errmsg = ""
if self.auto_reconnect:
status, errmsg = self.connect()
errmsg = gettext(
"""
Attempt to reconnect it failed with the below error:
{0}
""").format(errmsg)
if not status:
msg = gettext("Connection was lost!\n{0}").format(errmsg)
current_app.logger.error(msg)
return False, msg
try:
cur = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
except psycopg2.Error as pe:
errmsg = gettext("""
Failed to create cursor for psycopg2 connection with error message for the \
server#{1}:{2}:
{0}""").format(str(pe), self.manager.sid, self.db)
current_app.logger.error(errmsg)
self.conn.close()
self.conn = None
if self.auto_reconnect:
status, errmsg = self.connect()
if not status:
msg = gettext(
"""
Connection for server#{0} with database "{1}" was lost.
Attempt to reconnect it failed with the below error:
{2}"""
).format(self.driver.server_id, self.database, errmsg)
current_app.logger.error(msg)
return False, errmsg
else:
return False, errmsg
setattr(g, self.conn_id, cur)
return True, cur
def execute_scalar(self, query, params=None):
status, cur = self.__cursor()
if not status:
return False, str(cur)
try:
cur.execute(query, params)
except psycopg2.Error as pe:
cur.close()
return False, str(pe)
if cur.rowcount > 0:
res = cur.fetchone()
if len(res) > 0:
return True, res[0]
return True, None
def execute_2darray(self, query, params=None):
status, cur = self.__cursor()
if not status:
return False, str(cur)
try:
cur.execute(query, params)
except psycopg2.Error as pe:
cur.close()
return False, str(pe)
import copy
# Get Resultset Column Name, Type and size
columns = [copy.deepcopy(desc.__dict__) for desc in cur.description]
rows = []
for row in cur:
rows.append(row)
return True, {'columns': columns, 'rows': rows}
def execute_dict(self, query, params=None):
status, cur = self.__cursor()
if not status:
return False, str(cur)
try:
cur.execute(query, params)
except psycopg2.Error as pe:
cur.close()
return False, str(pe)
import copy
# Get Resultset Column Name, Type and size
columns = [copy.deepcopy(desc.__dict__) for desc in cur.description]
rows = []
for row in cur:
rows.append(dict(row))
return True, {'columns': columns, 'rows': rows}
def connected(self):
if self.conn:
if not self.conn.closed:
return True
self.conn = None
return False
def reset(self):
if self.conn:
if self.conn.closed:
self.conn = None
pg_conn = None
mgr = self.manager
password = getattr(mgr, 'password', None)
if password:
# Fetch Logged in User Details.
user = User.query.filter_by(id=current_user.id).first()
if user is None:
return unauthorized(gettext("Unauthorized Request."))
password = decrypt(password, user.password)
try:
pg_conn = psycopg2.connect(
host=mgr.host,
port=mgr.port,
database=db,
user=mgr.user,
password=password
)
except psycopg2.Error as e:
msg = e.pgerror if e.pgerror else e.message \
if e.message else e.diag.message_detail \
if e.diag.message_detail else str(e)
current_app.logger.error(
gettext(
"""
Failed to reset the connection of the server due to following error:
{0}"""
).Format(msg)
)
return False, msg
self.conn = pg_conn
return True, None
def transaction_status(self):
if self.conn:
return self.conn.get_transaction_status()
return None
def ping(self):
return self.execute_scalar('SELECT 1')
def _release(self):
if self.conn:
self.conn.close()
self.conn = None
class ServerManager(object):
"""
class ServerManager
This class contains the information about the given server.
And, acts as connection manager for that particular session.
"""
def __init__(self, server):
self.module = None
self.ver = None
self.sversion = None
self.connections = dict()
self.update(server)
def update(self, server):
assert(server is not None)
assert(isinstance(server, Server))
self.module = None
self.ver = None
self.sversion = None
self.password = None
self.sid = server.id
self.stype = None
self.host = server.host
self.port = server.port
self.db = server.maintenance_db
self.user = server.username
self.password = server.password
self.role = server.role
self.ssl_mode = server.ssl_mode
self.pinged = datetime.now()
for con in self.connections:
self.connections[con]._release()
self.connections = dict()
def ServerVersion(self):
return self.ver
@property
def version(self):
return self.sversion
def MajorVersion(self):
if self.sversion is not None:
return int(self.sversion / 10000)
raise Exception("Information is not available!")
def MinorVersion(self):
if self.sversion:
return int(int(self.sversion / 100) % 100)
raise Exception("Information is not available!")
def PatchVersion(self):
if self.sversion:
return int(int(self.sversion / 100) / 100)
raise Exception("Information is not available!")
def connection(self, database=None, conn_id=None, auto_reconnect=True):
my_id = ('CONN:' + str(conn_id)) if conn_id is not None else \
('DB:' + str(database))
self.pinged = datetime.now()
if my_id in self.connections:
return self.connections[my_id]
else:
self.connections[my_id] = Connection(
self, my_id, database, auto_reconnect
)
return self.connections[my_id]
def release(self, database=None, conn_id=None):
my_id = ('CONN:' + str(conn_id)) if conn_id is not None else \
('DB:' + str(database)) if database is not None else None
if my_id is not None:
if my_id in self.connections:
self.connections[my_id]._release()
del self.connections[my_id]
return True
else:
return False
for con in self.connections:
self.connections[con]._release()
self.connections = dict()
self.password = None
return True
class Driver(BaseDriver):
"""
class Driver(BaseDriver):
This driver acts as a wrapper around psycopg2 connection driver
implementation. We will be using psycopg2 for makeing connection with
the PostgreSQL/Postgres Plus Advanced Server (EnterpriseDB).
Properties:
----------
* Version (string):
Version of psycopg2 driver
Methods:
-------
* get_connection(sid, database, conn_id, auto_reconnect)
- It returns a Connection class object, which may/may not be connected
to the database server for this sesssion
* release_connection(seid, database, conn_id)
- It releases the connection object for the given conn_id/database for this
session.
* connection_manager(sid, reset)
- It returns the server connection manager for this session.
"""
def __init__(self, **kwargs):
self.managers = dict()
super(Driver, self).__init__()
def connection_manager(self, sid=None):
"""
connection_manager(...)
Returns the ServerManager object for the current session. It will
create new ServerManager object (if necessary).
Parameters:
sid
- Server ID
"""
assert (sid is not None and isinstance(sid, int))
managers = None
import datetime
if session['_id'] not in self.managers:
self.managers[session['_id']] = managers = dict()
else:
managers = self.managers[session['_id']]
managers['pinged'] = datetime.datetime.now()
if sid not in managers:
from pgadmin.settings.settings_model import Server
s = Server.query.filter_by(id=sid).first()
managers[str(sid)] = ServerManager(s)
return managers[str(sid)]
return managers[str(sid)]
def Version(cls):
"""
Version(...)
Returns the current version of psycopg2 driver
"""
version = getattr(psycopg2, '__version__', None)
if version:
return version
raise Exception(
"Driver Version information for psycopg2 is not available!"
)
def get_connection(
self, sid, database=None, conn_id=None, auto_reconnect=True
):
"""
get_connection(...)
Returns the connection object for the certain connection-id/database
for the specific server, identified by sid. Create a new Connection
object (if require).
Parameters:
sid
- Server ID
database
- Database, on which the connection needs to be made
If provided none, maintenance_db for the server will be used,
while generating new Connection object.
conn_id
- Identification String for the Connection This will be used by
certain tools, which will require a dedicated connection for it.
i.e. Debugger, Query Tool, etc.
auto_reconnect
- This parameters define, if we should attempt to reconnect the
database server automatically, when connection has been lost for
any reason. Certain tools like Debugger will require a permenant
connection, and it stops working on disconnection.
"""
manager = connection_manager(sid)
return manager.connection(database, conn_id, connect, **kwargs)
def release_connection(self, sid, database=None, conn_id=None):
"""
Release the connection for the given connection-id/database in this
session.
"""
return connection_manager(sid).release(database, conn_id)
def gc(self):
"""
Release the connections for the sessions, which have not pinged the
server for more than config.MAX_SESSION_IDLE_TIME.
"""
import datetime
from config import MAX_SESSION_IDLE_TIME
assert(MAX_SESSION_IDLE_TIME is not None and
isinstance(MAX_SESSION_IDLE_TIME, int))
# Mininum session idle is 20 minutes
max_idle_time = max(MAX_SESSION_IDLE_TIME, 20)
session_idle_timeout = datetime.timedelta(minutes=max_idle_time)
curr_time = datetime.datetime.now()
for sess in self.managers:
sess_mgr = self.managers[sess]
if sess == session['_id']:
sess_mgr['pinged'] = curr_time
continue
if (curr_time - sess_mgr['pinged'] >= session_idle_timeout):
for mgr in [m for m in sess_mgr if isinstance(m,
ServerManager)]:
mgr.release()

View File

@ -0,0 +1,84 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from abc import ABCMeta
from flask.ext.babel import gettext
def _decorate_cls_name(module_name):
l = len(__package__) + 1
if len(module_name) > l and module_name.startswith(__package__):
return module_name[l:]
return module_name
class DriverRegistry(ABCMeta):
"""
class DriverRegistry(object)
Every Driver will be registered automatically by its module name.
This uses factory pattern to genreate driver object based on its name
automatically.
Class-level Methods:
----------- -------
* __init__(...)
- It will be used to register type of drivers. You don't need to call
this function explicitly. This will be automatically executed, whenever
we create class and inherit from BaseDriver, it will register it as
available driver in DriverRegistry. Because - the __metaclass__ for
BaseDriver is set it to DriverRegistry, and it will create new instance
of this DriverRegistry per class.
* create(type, *args, **kwargs)
- Create type of driver object for this server, from the available
driver list (if available, or raise exception).
* load_drivers():
- Use this function from init_app(...) to load all available drivers in
the registry.
"""
registry = dict()
drivers = dict()
def __init__(cls, name, bases, d):
# Register this type of driver, based on the module name
# Avoid registering the BaseDriver itself
if name != 'BaseDriver':
DriverRegistry.registry[_decorate_cls_name(d['__module__'])] = cls
ABCMeta.__init__(cls, name, bases, d)
@classmethod
def create(cls, name, **kwargs):
if name in DriverRegistry.drivers:
return DriverRegistry.drivers[name]
if name in DriverRegistry.registry:
DriverRegistry.drivers[name] = \
(DriverRegistry.registry[name])(**kwargs)
return DriverRegistry.drivers[name]
raise NotImplementedError(
gettext("Driver - '{0}' has not been implemented!").format(name)
)
@classmethod
def load_drivers(cls):
DriverRegistry.registry = dict()
from importlib import import_module
from werkzeug.utils import find_modules
for module_name in find_modules(__package__, True):
module = import_module(module_name)

View File

@ -10,34 +10,47 @@
"""Perform the initial setup of the application, by creating the auth
and settings database."""
import os
import sys
import getpass
import random
import string
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, SQLAlchemyUserDatastore
from flask.ext.security.utils import encrypt_password
from pgadmin.settings.settings_model import db, Role, User, Server, ServerGroup, Version
import getpass, os, random, sys, string
from pgadmin.settings.settings_model import db, Role, User, Server, \
ServerGroup, Version
# Configuration settings
import config
def do_setup(app):
"""Create a new settings database from scratch"""
if config.SERVER_MODE is False:
print("NOTE: Configuring authentication for DESKTOP mode.")
email = config.DESKTOP_USER
p1 = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(32)])
p1 = ''.join([
random.choice(string.ascii_letters + string.digits)
for n in xrange(32)
])
else:
print("NOTE: Configuring authentication for SERVER mode.\n")
# Prompt the user for their default username and password.
print("Enter the email address and password to use for the initial pgAdmin user account:\n")
print("""
Enter the email address and password to use for the initial pgAdmin user \
account:\n""")
email = ''
while email == '':
email = raw_input("Email address: ")
pprompt = lambda: (getpass.getpass(), getpass.getpass('Retype password: '))
def pprompt():
return getpass.getpass(), getpass.getpass('Retype password:')
p1, p2 = pprompt()
while p1 != p2:
@ -52,7 +65,10 @@ def do_setup(app):
password = encrypt_password(p1)
db.create_all()
user_datastore.create_role(name='Administrators', description='pgAdmin Administrators Role')
user_datastore.create_role(
name='Administrators',
description='pgAdmin Administrators Role'
)
user_datastore.create_user(email=email, password=password)
db.session.flush()
user_datastore.add_role_to_user(email, 'Administrators')
@ -63,14 +79,20 @@ def do_setup(app):
db.session.merge(server_group)
# Set the schema version
version = Version(name='ConfigDB', value=config.SETTINGS_SCHEMA_VERSION)
version = Version(
name='ConfigDB', value=config.SETTINGS_SCHEMA_VERSION
)
db.session.merge(version)
db.session.commit()
# Done!
print("")
print("The configuration database has been created at %s" % config.SQLITE_PATH)
print(
"The configuration database has been created at {0}".format(
config.SQLITE_PATH
)
)
def do_upgrade(app, datastore, security, version):
@ -80,12 +102,80 @@ def do_upgrade(app, datastore, security, version):
# version.
#######################################################################
# Changes introduced in schema version 2
if int(version.value) < 2:
# Create the 'server' table
db.metadata.create_all(db.engine, tables=[Server.__table__])
elif int(version.value) < 3:
db.engine.execute('ALTER TABLE server ADD COLUMN comment TEXT(1024)');
with app.app_context():
version = Version.query.filter_by(name='ConfigDB').first()
# Pre-flight checks
if int(version.value) > int(config.SETTINGS_SCHEMA_VERSION):
print("""
The database schema version is {0}, whilst the version required by the \
software is {1}.
Exiting...""".format(version.value, config.SETTINGS_SCHEMA_VERSION))
sys.exit(1)
elif int(version.value) == int(config.SETTINGS_SCHEMA_VERSION):
print("""
The database schema version is {0} as required.
Exiting...""".format(version.value))
sys.exit(1)
app.logger.info(
"NOTE: Upgrading database schema from version %d to %d." %
(version.value, config.SETTINGS_SCHEMA_VERSION)
)
#######################################################################
# Run whatever is required to update the database schema to the current
# version. Always use "< REQUIRED_VERSION" as the test for readability
#######################################################################
# Changes introduced in schema version 2
if int(version.value) < 2:
# Create the 'server' table
db.metadata.create_all(db.engine, tables=[Server.__table__])
if int(version.value) < 3:
db.engine.execute(
'ALTER TABLE server ADD COLUMN comment TEXT(1024)'
)
if int(version.value) < 4:
db.engine.execute(
'ALTER TABLE server ADD COLUMN password TEXT(64)'
)
if int(version.value) < 5:
db.engine.execute('ALTER TABLE server ADD COLUMN role text(64)')
if int(version.value) == 6:
db.engine.execute("ALTER TABLE server RENAME TO server_old")
db.engine.execute("""
CREATE TABLE server (
id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
servergroup_id INTEGER NOT NULL,
name VARCHAR(128) NOT NULL,
host VARCHAR(128) NOT NULL,
port INTEGER NOT NULL CHECK (port >= 1024 AND port <= 65534),
maintenance_db VARCHAR(64) NOT NULL,
username VARCHAR(64) NOT NULL,
ssl_mode VARCHAR(16) NOT NULL CHECK (
ssl_mode IN (
'allow', 'prefer', 'require', 'disable', 'verify-ca', 'verify-full'
)),
comment VARCHAR(1024), password TEXT(64), role text(64),
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES user (id),
FOREIGN KEY(servergroup_id) REFERENCES servergroup (id)
)""")
db.engine.execute("""
INSERT INTO server (
id, user_id, servergroup_id, name, host, port, maintenance_db, username,
ssl_mode, comment, password, role
) SELECT
id, user_id, servergroup_id, name, host, port, maintenance_db, username,
ssl_mode, comment, password, role
FROM server_old""")
db.engine.execute("DROP TABLE server_old")
# Finally, update the schema version
version.value = config.SETTINGS_SCHEMA_VERSION
db.session.merge(version)
# Finally, update the schema version
version.value = config.SETTINGS_SCHEMA_VERSION
@ -94,8 +184,10 @@ def do_upgrade(app, datastore, security, version):
db.session.commit()
# Done!
print("")
print("The configuration database %s has been upgraded to version %d" % (config.SQLITE_PATH, config.SETTINGS_SCHEMA_VERSION))
app.logger.info(
"The configuration database %s has been upgraded to version %d" %
(config.SQLITE_PATH, config.SETTINGS_SCHEMA_VERSION)
)
###############################################################################
# Do stuff!
@ -103,24 +195,31 @@ def do_upgrade(app, datastore, security, version):
if __name__ == '__main__':
app = Flask(__name__)
app.config.from_object(config)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + config.SQLITE_PATH.replace('\\', '/')
app.config['SQLALCHEMY_DATABASE_URI'] = \
'sqlite:///' + config.SQLITE_PATH.replace('\\', '/')
db.init_app(app)
print("pgAdmin 4 - Application Initialisation")
print("======================================\n")
local_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config_local.py')
local_config = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'config_local.py'
)
if not os.path.isfile(local_config):
print("The configuration file %s does not exist.\n" % local_config)
print("Before running this application, ensure that config_local.py has been created")
print("and sets values for SECRET_KEY, SECURITY_PASSWORD_SALT and CSRF_SESSION_KEY")
print("at bare minimum. See config.py for more information and a complete list of")
print("settings. Exiting...")
print("""
The configuration file - {0} does not exist.
Before running this application, ensure that config_local.py has been created
and sets values for SECRET_KEY, SECURITY_PASSWORD_SALT and CSRF_SESSION_KEY
at bare minimum. See config.py for more information and a complete list of
settings. Exiting...""".format(local_config))
sys.exit(1)
# Check if the database exists. If it does, tell the user and exit.
if os.path.isfile(config.SQLITE_PATH):
print("The configuration database %s already exists.\nEntering upgrade mode...\n" % config.SQLITE_PATH)
print("""
The configuration database %s already exists.
Entering upgrade mode...""".format(config.SQLITE_PATH))
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
@ -132,15 +231,23 @@ if __name__ == '__main__':
# Pre-flight checks
if int(version.value) > int(config.SETTINGS_SCHEMA_VERSION):
print("The database schema version is %d, whilst the version required by the software is %d.\nExiting..."
% (version.value, config.SETTINGS_SCHEMA_VERSION))
print("""
The database schema version is %d, whilst the version required by the \
software is %d.
Exiting...""".format(version.value, config.SETTINGS_SCHEMA_VERSION))
sys.exit(1)
elif int(version.value) == int(config.SETTINGS_SCHEMA_VERSION):
print("The database schema version is %d as required.\nExiting..." % (version.value))
print("""
The database schema version is %d as required.
Exiting...""".format(version.value))
sys.exit(1)
print("NOTE: Upgrading database schema from version %d to %d." % (version.value, config.SETTINGS_SCHEMA_VERSION))
print("NOTE: Upgrading database schema from version %d to %d." % (
version.value, config.SETTINGS_SCHEMA_VERSION
))
do_upgrade(app, user_datastore, security, version)
else:
print("The configuration database %s does not exist.\nEntering initial setup mode...\n" % config.SQLITE_PATH)
print("""
The configuration database - {0} does not exist.
Entering initial setup mode...""".format(config.SQLITE_PATH))
do_setup(app)