Added ability to use SQL in the 'DB Restriction' field. #2767
parent
0d8b3c4389
commit
627aa5d695
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 121 KiB |
|
|
@ -12,6 +12,7 @@ notes for it.
|
|||
:maxdepth: 1
|
||||
|
||||
|
||||
release_notes_9_3
|
||||
release_notes_9_2
|
||||
release_notes_9_1
|
||||
release_notes_9_0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
***********
|
||||
Version 9.3
|
||||
***********
|
||||
|
||||
Release date: 2025-04-30
|
||||
|
||||
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.2.
|
||||
|
||||
Supported Database Servers
|
||||
**************************
|
||||
**PostgreSQL**: 13, 14, 15, 16 and 17
|
||||
|
||||
**EDB Advanced Server**: 13, 14, 15, 16 and 17
|
||||
|
||||
Bundled PostgreSQL Utilities
|
||||
****************************
|
||||
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 17.2
|
||||
|
||||
|
||||
New features
|
||||
************
|
||||
|
||||
| `Issue #2767 <https://github.com/pgadmin-org/pgadmin4/issues/2767>`_ - Added ability to use SQL in the "DB Restriction" field.
|
||||
| `Issue #8629 <https://github.com/pgadmin-org/pgadmin4/issues/8629>`_ - Added support for font ligatures.
|
||||
|
||||
Housekeeping
|
||||
************
|
||||
|
||||
|
||||
Bug fixes
|
||||
*********
|
||||
|
||||
| `Issue #8443 <https://github.com/pgadmin-org/pgadmin4/issues/8443>`_ - Fixed an issue where the debugger hangs when stepping into nested function/procedure.
|
||||
| `Issue #8556 <https://github.com/pgadmin-org/pgadmin4/issues/8556>`_ - Ensure that graph data is updated even when the Dashboard tab is inactive.
|
||||
|
|
@ -195,11 +195,19 @@ Click the *Advanced* tab to continue.
|
|||
|
||||
Use the fields in the *Advanced* tab to configure a connection:
|
||||
|
||||
* Use the *DB restriction* field to provide a SQL restriction that will be used
|
||||
against the pg_database table to limit the databases that you see. For
|
||||
example, you might enter: *live_db test_db* so that only live_db and test_db
|
||||
are shown in the pgAdmin browser. Separate entries with a comma or tab as you
|
||||
type.
|
||||
* Specify the type of the database restriction that will be used to filter
|
||||
out the databases for restriction in the *DB restriction type* field:
|
||||
|
||||
* Select the *Databases* option to specify the name of the databases
|
||||
that will be used against the pg_database table to limit the databases
|
||||
that you see. This is the default.
|
||||
* Select the *SQL* option to provide a SQL restriction that will be used
|
||||
against the pg_database table to limit the databases that you see.
|
||||
|
||||
* Use the *DB restriction* field to provide a SQL restriction OR Database names
|
||||
that will be used against the pg_database table to limit the databases that you see.
|
||||
For example, you might enter: *live_db test_db* so that only live_db and test_db
|
||||
are shown in the pgAdmin object explorer.
|
||||
* Use the *Password exec command* field to specify a shell command to be executed
|
||||
to retrieve a password to be used for SQL authentication. The ``stdout`` of the
|
||||
command will be used as the SQL password. This may be useful when the password
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""
|
||||
|
||||
Revision ID: 1f0eddc8fc79
|
||||
Revises: e982c040d9b5
|
||||
Create Date: 2025-03-26 15:58:24.131719
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from pgadmin.utils.constants import RESTRICTION_TYPE_DATABASES
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1f0eddc8fc79'
|
||||
down_revision = 'e982c040d9b5'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('server',
|
||||
sa.Column('db_res_type', sa.String(length=32),
|
||||
server_default=RESTRICTION_TYPE_DATABASES))
|
||||
|
||||
|
||||
def downgrade():
|
||||
# pgAdmin only upgrades, downgrade not implemented.
|
||||
pass
|
||||
|
|
@ -34,9 +34,9 @@ from pgadmin.utils.exception import CryptKeyMissing
|
|||
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
|
||||
from pgadmin.browser.server_groups.servers.utils import \
|
||||
(is_valid_ipaddress, get_replication_type, convert_connection_parameter,
|
||||
check_ssl_fields)
|
||||
check_ssl_fields, get_db_restriction)
|
||||
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
|
||||
SERVER_CONNECTION_CLOSED
|
||||
SERVER_CONNECTION_CLOSED, RESTRICTION_TYPE_SQL
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from pgadmin.utils.preferences import Preferences
|
||||
|
|
@ -746,6 +746,7 @@ class ServerNode(PGChildNodeView):
|
|||
'comment': 'comment',
|
||||
'role': 'role',
|
||||
'db_res': 'db_res',
|
||||
'db_res_type': 'db_res_type',
|
||||
'passexec_cmd': 'passexec_cmd',
|
||||
'passexec_expiration': 'passexec_expiration',
|
||||
'bgcolor': 'bgcolor',
|
||||
|
|
@ -776,12 +777,11 @@ class ServerNode(PGChildNodeView):
|
|||
'role': gettext('Role')
|
||||
}
|
||||
|
||||
idx = 0
|
||||
data = request.form if request.form else json.loads(
|
||||
request.data
|
||||
)
|
||||
|
||||
if 'db_res' in data:
|
||||
if 'db_res' in data and isinstance(data['db_res'], list):
|
||||
data['db_res'] = ','.join(data['db_res'])
|
||||
|
||||
# Update connection parameter if any.
|
||||
|
|
@ -948,7 +948,7 @@ class ServerNode(PGChildNodeView):
|
|||
'connected': connected,
|
||||
'version': manager.ver,
|
||||
'server_type': manager.server_type if connected else 'pg',
|
||||
'db_res': server.db_res.split(',') if server.db_res else None
|
||||
'db_res': get_db_restriction(server.db_res_type, server.db_res)
|
||||
})
|
||||
|
||||
return ajax_response(
|
||||
|
|
@ -1031,7 +1031,8 @@ class ServerNode(PGChildNodeView):
|
|||
'server_type': manager.server_type if connected else 'pg',
|
||||
'bgcolor': server.bgcolor,
|
||||
'fgcolor': server.fgcolor,
|
||||
'db_res': server.db_res.split(',') if server.db_res else None,
|
||||
'db_res': get_db_restriction(server.db_res_type, server.db_res),
|
||||
'db_res_type': server.db_res_type,
|
||||
'passexec_cmd':
|
||||
server.passexec_cmd if server.passexec_cmd else None,
|
||||
'passexec_expiration':
|
||||
|
|
@ -1137,6 +1138,12 @@ class ServerNode(PGChildNodeView):
|
|||
data['connection_params'] = connection_params
|
||||
|
||||
server = None
|
||||
db_restriction = None
|
||||
if 'db_res' in data and isinstance(data['db_res'], list):
|
||||
db_restriction = ','.join(data['db_res'])
|
||||
elif 'db_res' in data and 'db_res_type' in data and \
|
||||
data['db_res_type'] == RESTRICTION_TYPE_SQL:
|
||||
db_restriction = data['db_res']
|
||||
|
||||
try:
|
||||
server = Server(
|
||||
|
|
@ -1151,8 +1158,8 @@ class ServerNode(PGChildNodeView):
|
|||
config.ALLOW_SAVE_PASSWORD else 0,
|
||||
comment=data.get('comment', None),
|
||||
role=data.get('role', None),
|
||||
db_res=','.join(data['db_res']) if 'db_res' in data and
|
||||
isinstance(data['db_res'], list) else None,
|
||||
db_res=db_restriction,
|
||||
db_res_type=data.get('db_res_type', None),
|
||||
bgcolor=data.get('bgcolor', None),
|
||||
fgcolor=data.get('fgcolor', None),
|
||||
service=data.get('service', None),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import re
|
|||
from functools import wraps
|
||||
|
||||
import json
|
||||
from flask import render_template, current_app, request, jsonify
|
||||
from flask import render_template, current_app, request, jsonify, Response
|
||||
from flask_babel import gettext as _
|
||||
from flask_security import current_user
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ from pgadmin.browser.server_groups.servers.databases.utils import \
|
|||
parse_sec_labels_from_db, parse_variables_from_db, \
|
||||
get_attributes_from_db_info
|
||||
from pgadmin.browser.server_groups.servers.utils import parse_priv_from_db, \
|
||||
parse_priv_to_db
|
||||
parse_priv_to_db, get_db_disp_restriction
|
||||
from pgadmin.browser.utils import PGChildNodeView
|
||||
from pgadmin.utils.ajax import gone
|
||||
from pgadmin.utils.ajax import make_json_response, \
|
||||
|
|
@ -266,14 +266,7 @@ class DatabaseView(PGChildNodeView):
|
|||
def list(self, gid, sid):
|
||||
last_system_oid = self.retrieve_last_system_oid()
|
||||
|
||||
db_disp_res = None
|
||||
params = None
|
||||
if self.manager and self.manager.db_res:
|
||||
db_disp_res = ", ".join(
|
||||
['%s'] * len(self.manager.db_res.split(','))
|
||||
)
|
||||
params = tuple(self.manager.db_res.split(','))
|
||||
|
||||
db_disp_res, params = get_db_disp_restriction(self.manager)
|
||||
SQL = render_template(
|
||||
"/".join([self.template_path, self._PROPERTIES_SQL]),
|
||||
conn=self.conn,
|
||||
|
|
@ -351,15 +344,7 @@ class DatabaseView(PGChildNodeView):
|
|||
self.manager.did in self.manager.db_info:
|
||||
last_system_oid = self._DATABASE_LAST_SYSTEM_OID
|
||||
|
||||
server_node_res = self.manager
|
||||
|
||||
db_disp_res = None
|
||||
params = None
|
||||
if server_node_res and server_node_res.db_res:
|
||||
db_disp_res = ", ".join(
|
||||
['%s'] * len(server_node_res.db_res.split(','))
|
||||
)
|
||||
params = tuple(server_node_res.db_res.split(','))
|
||||
db_disp_res, params = get_db_disp_restriction(self.manager)
|
||||
SQL = render_template(
|
||||
"/".join([self.template_path, self._NODES_SQL]),
|
||||
last_system_oid=last_system_oid,
|
||||
|
|
@ -411,6 +396,8 @@ class DatabaseView(PGChildNodeView):
|
|||
@check_precondition(action="nodes")
|
||||
def nodes(self, gid, sid, is_schema_diff=False):
|
||||
res = self.get_nodes(gid, sid, is_schema_diff)
|
||||
if isinstance(res, Response):
|
||||
return res
|
||||
|
||||
return make_json_response(
|
||||
data=res,
|
||||
|
|
@ -1251,13 +1238,7 @@ class DatabaseView(PGChildNodeView):
|
|||
"""
|
||||
last_system_oid = self.retrieve_last_system_oid()
|
||||
|
||||
db_disp_res = None
|
||||
params = None
|
||||
if self.manager and self.manager.db_res:
|
||||
db_disp_res = ", ".join(
|
||||
['%s'] * len(self.manager.db_res.split(','))
|
||||
)
|
||||
params = tuple(self.manager.db_res.split(','))
|
||||
db_disp_res, params = get_db_disp_restriction(self.manager)
|
||||
|
||||
conn = self.manager.connection()
|
||||
status, res = conn.execute_dict(render_template(
|
||||
|
|
|
|||
|
|
@ -155,7 +155,8 @@ export default class ServerSchema extends BaseUISchema {
|
|||
connect_now: true,
|
||||
password: undefined,
|
||||
save_password: false,
|
||||
db_res: [],
|
||||
db_res: undefined,
|
||||
db_res_type: 'databases',
|
||||
passexec: undefined,
|
||||
passexec_expiration: undefined,
|
||||
service: undefined,
|
||||
|
|
@ -468,11 +469,41 @@ export default class ServerSchema extends BaseUISchema {
|
|||
readonly: obj.isConnected,
|
||||
},
|
||||
{
|
||||
id: 'db_res', label: gettext('DB restriction'), type: 'select', group: gettext('Advanced'),
|
||||
options: [],
|
||||
id: 'db_res_type', label: gettext('DB restriction type'), type: 'toggle',
|
||||
mode: ['properties', 'edit', 'create'], group: gettext('Advanced'),
|
||||
options: [
|
||||
{'label': gettext('Databases'), value: 'databases'},
|
||||
{'label': gettext('SQL'), value: 'sql'},
|
||||
],
|
||||
readonly: obj.isConnectedOrShared,
|
||||
depChange: ()=>{
|
||||
return {
|
||||
db_res: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'db_res', label: gettext('DB restriction'), group: gettext('Advanced'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: obj.isConnectedOrShared,
|
||||
controlProps: {
|
||||
multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: 'Specify the databases to be restrict...'
|
||||
deps: ['db_res_type'],
|
||||
type: (state) => {
|
||||
if (state.db_res_type == 'databases') {
|
||||
return {
|
||||
type: 'select',
|
||||
options: [],
|
||||
controlProps: {
|
||||
multiple: true,
|
||||
allowClear: false,
|
||||
creatable: true,
|
||||
noDropdown: true,
|
||||
placeholder: 'Specify the databases to be restrict...'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'sql',
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -240,6 +240,45 @@
|
|||
"expected_data": {
|
||||
"status_code": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Add server with post connection sql",
|
||||
"url": "/browser/server/obj/",
|
||||
"is_positive_test": true,
|
||||
"test_data": {
|
||||
"post_connection_sql": "set timezone to 'Asia/Kolkata'"
|
||||
},
|
||||
"mocking_required": false,
|
||||
"mock_data": {},
|
||||
"expected_data": {
|
||||
"status_code": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Add server with DB Restriction (Database names)",
|
||||
"url": "/browser/server/obj/",
|
||||
"is_positive_test": true,
|
||||
"test_data": {
|
||||
"db_res": ["dev", "qa"]
|
||||
},
|
||||
"mocking_required": false,
|
||||
"mock_data": {},
|
||||
"expected_data": {
|
||||
"status_code": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Add server with DB Restriction (SQL)",
|
||||
"url": "/browser/server/obj/",
|
||||
"is_positive_test": true,
|
||||
"test_data": {
|
||||
"db_res": "SELECT datname FROM pg_database\nWHERE datistemplate = false AND datname ILIKE '%myprefix_%'\nORDER BY datname"
|
||||
},
|
||||
"mocking_required": false,
|
||||
"mock_data": {},
|
||||
"expected_data": {
|
||||
"status_code": 200
|
||||
}
|
||||
}
|
||||
],
|
||||
"is_password_saved": [
|
||||
|
|
|
|||
|
|
@ -91,6 +91,14 @@ class AddServerTest(BaseTestGenerator):
|
|||
if 'tags' in self.test_data:
|
||||
self.server['tags'] = self.test_data['tags']
|
||||
|
||||
if 'post_connection_sql' in self.test_data:
|
||||
self.server['post_connection_sql'] = (
|
||||
self.test_data)['post_connection_sql']
|
||||
|
||||
if 'db_res' in self.test_data:
|
||||
self.server['db_res'] = (
|
||||
self.test_data)['db_res']
|
||||
|
||||
if self.is_positive_test:
|
||||
if hasattr(self, 'with_save'):
|
||||
self.server['save_password'] = self.with_save
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ import keyring
|
|||
from flask_login import current_user
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
from flask import render_template
|
||||
from pgadmin.utils.constants import KEY_RING_USERNAME_FORMAT, \
|
||||
KEY_RING_SERVICE_NAME, KEY_RING_TUNNEL_FORMAT, \
|
||||
KEY_RING_DESKTOP_USER, SSL_MODES
|
||||
from pgadmin.utils.constants import (
|
||||
KEY_RING_USERNAME_FORMAT, KEY_RING_SERVICE_NAME, KEY_RING_TUNNEL_FORMAT,
|
||||
KEY_RING_DESKTOP_USER, SSL_MODES, RESTRICTION_TYPE_DATABASES,
|
||||
RESTRICTION_TYPE_SQL)
|
||||
from pgadmin.utils.crypto import encrypt, decrypt
|
||||
from pgadmin.model import db, Server
|
||||
from flask import current_app
|
||||
|
|
@ -680,3 +681,35 @@ def delete_adhoc_servers(sid=None):
|
|||
except Exception:
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
def get_db_restriction(res_type, restriction):
|
||||
"""
|
||||
This function is used to return the database restriction based on
|
||||
restriction type.
|
||||
"""
|
||||
if restriction and res_type == RESTRICTION_TYPE_DATABASES:
|
||||
return restriction.split(',')
|
||||
elif restriction and res_type == RESTRICTION_TYPE_SQL:
|
||||
return restriction
|
||||
return None
|
||||
|
||||
|
||||
def get_db_disp_restriction(manager_obj):
|
||||
"""
|
||||
This function is used to return db disp restriction aand params
|
||||
to run the query.
|
||||
"""
|
||||
db_disp_res = None
|
||||
params = None
|
||||
if (manager_obj and manager_obj.db_res and
|
||||
manager_obj.db_res_type == RESTRICTION_TYPE_DATABASES):
|
||||
db_disp_res = ", ".join(
|
||||
['%s'] * len(manager_obj.db_res.split(','))
|
||||
)
|
||||
params = tuple(manager_obj.db_res.split(','))
|
||||
elif (manager_obj and manager_obj.db_res and
|
||||
manager_obj.db_res_type == RESTRICTION_TYPE_SQL):
|
||||
db_disp_res = manager_obj.db_res
|
||||
|
||||
return db_disp_res, params
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import Loader from 'sources/components/Loader';
|
|||
import { evalFunc } from '../../static/js/utils';
|
||||
import { usePgAdmin } from '../../static/js/PgAdminProvider';
|
||||
import { getSwitchCell } from '../../static/js/components/PgReactTableStyled';
|
||||
import { parseApiError } from '../../static/js/api_instance';
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
height: '100%',
|
||||
|
|
@ -272,9 +273,11 @@ export default function CollectionNodeProperties({
|
|||
setLoaderText('');
|
||||
})
|
||||
.catch((err) => {
|
||||
setData([]);
|
||||
setLoaderText('');
|
||||
pgAdmin.Browser.notifier.alert(
|
||||
gettext('Failed to retrieve data from the server.'),
|
||||
gettext(err.message)
|
||||
parseApiError(err)
|
||||
);
|
||||
});
|
||||
setIsStale(false);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { toPrettySize } from '../../../../static/js/utils';
|
|||
import withStandardTabInfo from '../../../../static/js/helpers/withStandardTabInfo';
|
||||
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
|
||||
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
|
||||
import { parseApiError } from '../../../../static/js/api_instance';
|
||||
|
||||
const Root = styled('div')(({theme}) => ({
|
||||
height : '100%',
|
||||
|
|
@ -193,7 +194,7 @@ function Statistics({ nodeData, nodeItem, node, treeNodeInfo, isActive, isStale,
|
|||
} else {
|
||||
pgAdmin.Browser.notifier.alert(
|
||||
gettext('Failed to retrieve data from the server.'),
|
||||
gettext(err.message)
|
||||
parseApiError(err)
|
||||
);
|
||||
setMsg(gettext('Failed to retrieve data from the server.'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import config
|
|||
#
|
||||
##########################################################################
|
||||
|
||||
SCHEMA_VERSION = 43
|
||||
SCHEMA_VERSION = 44
|
||||
|
||||
##########################################################################
|
||||
#
|
||||
|
|
@ -178,6 +178,7 @@ class Server(db.Model):
|
|||
lazy='joined'
|
||||
)
|
||||
db_res = db.Column(db.Text(), nullable=True)
|
||||
db_res_type = db.Column(db.String(32), default='databases')
|
||||
passexec_cmd = db.Column(db.Text(), nullable=True)
|
||||
passexec_expiration = db.Column(db.Integer(), nullable=True)
|
||||
bgcolor = db.Column(db.String(10), nullable=True)
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ from pgadmin.utils.preferences import Preferences
|
|||
from pgadmin.tools.sqleditor.utils.apply_explain_plan_wrapper import \
|
||||
get_explain_query_length
|
||||
from pgadmin.browser.server_groups.servers.utils import \
|
||||
convert_connection_parameter
|
||||
convert_connection_parameter, get_db_disp_restriction
|
||||
from pgadmin.misc.workspaces import check_and_delete_adhoc_server
|
||||
|
||||
MODULE_NAME = 'sqleditor'
|
||||
|
|
@ -2426,15 +2426,8 @@ def get_new_connection_database(sgid, sid=None):
|
|||
if sid:
|
||||
template_path = 'databases/sql/#{0}#'.format(manager.version)
|
||||
last_system_oid = 0
|
||||
server_node_res = manager
|
||||
|
||||
db_disp_res = None
|
||||
params = None
|
||||
if server_node_res and server_node_res.db_res:
|
||||
db_disp_res = ", ".join(
|
||||
['%s'] * len(server_node_res.db_res.split(','))
|
||||
)
|
||||
params = tuple(server_node_res.db_res.split(','))
|
||||
db_disp_res, params = get_db_disp_restriction(manager)
|
||||
sql = render_template(
|
||||
"/".join([template_path, _NODES_SQL]),
|
||||
last_system_oid=last_system_oid,
|
||||
|
|
|
|||
|
|
@ -160,3 +160,6 @@ DATA_TYPE_WITH_LENGTH = [1560, 'bit', 1561, 'bit[]',
|
|||
1015, 'varchar[]', 'character varying[]',
|
||||
'vector', 'vector[]', 'halfvec', 'halfvec[]',
|
||||
'sparsevec', 'sparsevec[]']
|
||||
|
||||
RESTRICTION_TYPE_DATABASES = 'databases'
|
||||
RESTRICTION_TYPE_SQL = 'sql'
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class ServerManager(object):
|
|||
self.db_info = dict()
|
||||
self.server_types = None
|
||||
self.db_res = server.db_res
|
||||
self.db_res_type = server.db_res_type
|
||||
self.name = server.name
|
||||
self.passexec = \
|
||||
PasswordExec(server.passexec_cmd, server.host, server.port,
|
||||
|
|
|
|||
Loading…
Reference in New Issue