Skip to content

Commit 6cb3a80

Browse files
authored
Add work with remote host (#78)
1 parent 09e9f01 commit 6cb3a80

19 files changed

+2321
-244
lines changed

Diff for: README.md

+27
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,33 @@ with testgres.get_new_node().init() as master:
173173
Note that `default_conf()` is called by `init()` function; both of them overwrite
174174
the configuration file, which means that they should be called before `append_conf()`.
175175

176+
### Remote mode
177+
Testgres supports the creation of PostgreSQL nodes on a remote host. This is useful when you want to run distributed tests involving multiple nodes spread across different machines.
178+
179+
To use this feature, you need to use the RemoteOperations class.
180+
Here is an example of how you might set this up:
181+
182+
```python
183+
from testgres import ConnectionParams, RemoteOperations, TestgresConfig, get_remote_node
184+
185+
# Set up connection params
186+
conn_params = ConnectionParams(
187+
host='your_host', # replace with your host
188+
username='user_name', # replace with your username
189+
ssh_key='path_to_ssh_key' # replace with your SSH key path
190+
)
191+
os_ops = RemoteOperations(conn_params)
192+
193+
# Add remote testgres config before test
194+
TestgresConfig.set_os_ops(os_ops=os_ops)
195+
196+
# Proceed with your test
197+
def test_basic_query(self):
198+
with get_remote_node(conn_params=conn_params) as node:
199+
node.init().start()
200+
res = node.execute('SELECT 1')
201+
self.assertEqual(res, [(1,)])
202+
```
176203

177204
## Authors
178205

Diff for: setup.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"six>=1.9.0",
1313
"psutil",
1414
"packaging",
15+
"paramiko",
16+
"fabric",
17+
"sshtunnel"
1518
]
1619

1720
# Add compatibility enum class
@@ -27,9 +30,9 @@
2730
readme = f.read()
2831

2932
setup(
30-
version='1.8.9',
33+
version='1.9.0',
3134
name='testgres',
32-
packages=['testgres'],
35+
packages=['testgres', 'testgres.operations'],
3336
description='Testing utility for PostgreSQL and its extensions',
3437
url='https://github.com/postgrespro/testgres',
3538
long_description=readme,

Diff for: testgres/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .api import get_new_node
1+
from .api import get_new_node, get_remote_node
22
from .backup import NodeBackup
33

44
from .config import \
@@ -46,8 +46,13 @@
4646
First, \
4747
Any
4848

49+
from .operations.os_ops import OsOperations, ConnectionParams
50+
from .operations.local_ops import LocalOperations
51+
from .operations.remote_ops import RemoteOperations
52+
4953
__all__ = [
5054
"get_new_node",
55+
"get_remote_node",
5156
"NodeBackup",
5257
"TestgresConfig", "configure_testgres", "scoped_config", "push_config", "pop_config",
5358
"NodeConnection", "DatabaseError", "InternalError", "ProgrammingError", "OperationalError",
@@ -56,4 +61,5 @@
5661
"PostgresNode", "NodeApp",
5762
"reserve_port", "release_port", "bound_ports", "get_bin_path", "get_pg_config", "get_pg_version",
5863
"First", "Any",
64+
"OsOperations", "LocalOperations", "RemoteOperations", "ConnectionParams"
5965
]

Diff for: testgres/api.py

+12
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,15 @@ def get_new_node(name=None, base_dir=None, **kwargs):
4040
"""
4141
# NOTE: leave explicit 'name' and 'base_dir' for compatibility
4242
return PostgresNode(name=name, base_dir=base_dir, **kwargs)
43+
44+
45+
def get_remote_node(name=None, conn_params=None):
46+
"""
47+
Simply a wrapper around :class:`.PostgresNode` constructor for remote node.
48+
See :meth:`.PostgresNode.__init__` for details.
49+
For remote connection you can add the next parameter:
50+
conn_params = ConnectionParams(host='127.0.0.1',
51+
ssh_key=None,
52+
username=default_username())
53+
"""
54+
return get_new_node(name=name, conn_params=conn_params)

Diff for: testgres/backup.py

+7-11
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
import os
44

5-
from shutil import rmtree, copytree
65
from six import raise_from
7-
from tempfile import mkdtemp
86

97
from .enums import XLogMethod
108

@@ -15,8 +13,6 @@
1513
PG_CONF_FILE, \
1614
BACKUP_LOG_FILE
1715

18-
from .defaults import default_username
19-
2016
from .exceptions import BackupException
2117

2218
from .utils import \
@@ -47,7 +43,7 @@ def __init__(self,
4743
username: database user name.
4844
xlog_method: none | fetch | stream (see docs)
4945
"""
50-
46+
self.os_ops = node.os_ops
5147
if not node.status():
5248
raise BackupException('Node must be running')
5349

@@ -60,8 +56,8 @@ def __init__(self,
6056
raise BackupException(msg)
6157

6258
# Set default arguments
63-
username = username or default_username()
64-
base_dir = base_dir or mkdtemp(prefix=TMP_BACKUP)
59+
username = username or self.os_ops.get_user()
60+
base_dir = base_dir or self.os_ops.mkdtemp(prefix=TMP_BACKUP)
6561

6662
# public
6763
self.original_node = node
@@ -107,14 +103,14 @@ def _prepare_dir(self, destroy):
107103
available = not destroy
108104

109105
if available:
110-
dest_base_dir = mkdtemp(prefix=TMP_NODE)
106+
dest_base_dir = self.os_ops.mkdtemp(prefix=TMP_NODE)
111107

112108
data1 = os.path.join(self.base_dir, DATA_DIR)
113109
data2 = os.path.join(dest_base_dir, DATA_DIR)
114110

115111
try:
116112
# Copy backup to new data dir
117-
copytree(data1, data2)
113+
self.os_ops.copytree(data1, data2)
118114
except Exception as e:
119115
raise_from(BackupException('Failed to copy files'), e)
120116
else:
@@ -143,7 +139,7 @@ def spawn_primary(self, name=None, destroy=True):
143139

144140
# Build a new PostgresNode
145141
NodeClass = self.original_node.__class__
146-
with clean_on_error(NodeClass(name=name, base_dir=base_dir)) as node:
142+
with clean_on_error(NodeClass(name=name, base_dir=base_dir, conn_params=self.original_node.os_ops.conn_params)) as node:
147143

148144
# New nodes should always remove dir tree
149145
node._should_rm_dirs = True
@@ -185,4 +181,4 @@ def cleanup(self):
185181

186182
if self._available:
187183
self._available = False
188-
rmtree(self.base_dir, ignore_errors=True)
184+
self.os_ops.rmdirs(self.base_dir, ignore_errors=True)

Diff for: testgres/cache.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
# coding: utf-8
22

3-
import io
43
import os
54

6-
from shutil import copytree
75
from six import raise_from
86

97
from .config import testgres_config
@@ -20,12 +18,16 @@
2018
get_bin_path, \
2119
execute_utility
2220

21+
from .operations.local_ops import LocalOperations
22+
from .operations.os_ops import OsOperations
2323

24-
def cached_initdb(data_dir, logfile=None, params=None):
24+
25+
def cached_initdb(data_dir, logfile=None, params=None, os_ops: OsOperations = LocalOperations()):
2526
"""
2627
Perform initdb or use cached node files.
2728
"""
28-
def call_initdb(initdb_dir, log=None):
29+
30+
def call_initdb(initdb_dir, log=logfile):
2931
try:
3032
_params = [get_bin_path("initdb"), "-D", initdb_dir, "-N"]
3133
execute_utility(_params + (params or []), log)
@@ -39,22 +41,23 @@ def call_initdb(initdb_dir, log=None):
3941
cached_data_dir = testgres_config.cached_initdb_dir
4042

4143
# Initialize cached initdb
42-
if not os.path.exists(cached_data_dir) or \
43-
not os.listdir(cached_data_dir):
44+
45+
if not os_ops.path_exists(cached_data_dir) or \
46+
not os_ops.listdir(cached_data_dir):
4447
call_initdb(cached_data_dir)
4548

4649
try:
4750
# Copy cached initdb to current data dir
48-
copytree(cached_data_dir, data_dir)
51+
os_ops.copytree(cached_data_dir, data_dir)
4952

5053
# Assign this node a unique system id if asked to
5154
if testgres_config.cached_initdb_unique:
5255
# XXX: write new unique system id to control file
5356
# Some users might rely upon unique system ids, but
5457
# our initdb caching mechanism breaks this contract.
5558
pg_control = os.path.join(data_dir, XLOG_CONTROL_FILE)
56-
with io.open(pg_control, "r+b") as f:
57-
f.write(generate_system_id()) # overwrite id
59+
system_id = generate_system_id()
60+
os_ops.write(pg_control, system_id, truncate=True, binary=True, read_and_write=True)
5861

5962
# XXX: build new WAL segment with our system id
6063
_params = [get_bin_path("pg_resetwal"), "-D", data_dir, "-f"]

Diff for: testgres/config.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
import tempfile
66

77
from contextlib import contextmanager
8-
from shutil import rmtree
9-
from tempfile import mkdtemp
108

119
from .consts import TMP_CACHE
10+
from .operations.os_ops import OsOperations
11+
from .operations.local_ops import LocalOperations
1212

1313

1414
class GlobalConfig(object):
@@ -43,6 +43,9 @@ class GlobalConfig(object):
4343

4444
_cached_initdb_dir = None
4545
""" underlying class attribute for cached_initdb_dir property """
46+
47+
os_ops = LocalOperations()
48+
""" OsOperation object that allows work on remote host """
4649
@property
4750
def cached_initdb_dir(self):
4851
""" path to a temp directory for cached initdb. """
@@ -54,6 +57,7 @@ def cached_initdb_dir(self, value):
5457

5558
if value:
5659
cached_initdb_dirs.add(value)
60+
return testgres_config.cached_initdb_dir
5761

5862
@property
5963
def temp_dir(self):
@@ -118,6 +122,11 @@ def copy(self):
118122

119123
return copy.copy(self)
120124

125+
@staticmethod
126+
def set_os_ops(os_ops: OsOperations):
127+
testgres_config.os_ops = os_ops
128+
testgres_config.cached_initdb_dir = os_ops.mkdtemp(prefix=TMP_CACHE)
129+
121130

122131
# cached dirs to be removed
123132
cached_initdb_dirs = set()
@@ -135,7 +144,7 @@ def copy(self):
135144
@atexit.register
136145
def _rm_cached_initdb_dirs():
137146
for d in cached_initdb_dirs:
138-
rmtree(d, ignore_errors=True)
147+
testgres_config.os_ops.rmdirs(d, ignore_errors=True)
139148

140149

141150
def push_config(**options):
@@ -198,4 +207,4 @@ def configure_testgres(**options):
198207

199208

200209
# NOTE: assign initial cached dir for initdb
201-
testgres_config.cached_initdb_dir = mkdtemp(prefix=TMP_CACHE)
210+
testgres_config.cached_initdb_dir = testgres_config.os_ops.mkdtemp(prefix=TMP_CACHE)

Diff for: testgres/connection.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ def __init__(self,
4141

4242
self._node = node
4343

44-
self._connection = pglib.connect(database=dbname,
45-
user=username,
46-
password=password,
47-
host=node.host,
48-
port=node.port)
44+
self._connection = node.os_ops.db_connect(dbname=dbname,
45+
user=username,
46+
password=password,
47+
host=node.host,
48+
port=node.port)
4949

5050
self._connection.autocommit = autocommit
5151
self._cursor = self.connection.cursor()
@@ -103,16 +103,15 @@ def rollback(self):
103103

104104
def execute(self, query, *args):
105105
self.cursor.execute(query, args)
106-
107106
try:
108107
res = self.cursor.fetchall()
109-
110108
# pg8000 might return tuples
111109
if isinstance(res, tuple):
112110
res = [tuple(t) for t in res]
113111

114112
return res
115-
except Exception:
113+
except Exception as e:
114+
print("Error executing query: {}".format(e))
116115
return None
117116

118117
def close(self):

Diff for: testgres/defaults.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import datetime
2-
import getpass
3-
import os
42
import struct
53
import uuid
64

5+
from .config import testgres_config as tconf
6+
77

88
def default_dbname():
99
"""
@@ -17,8 +17,7 @@ def default_username():
1717
"""
1818
Return default username (current user).
1919
"""
20-
21-
return getpass.getuser()
20+
return tconf.os_ops.get_user()
2221

2322

2423
def generate_app_name():
@@ -44,7 +43,7 @@ def generate_system_id():
4443
system_id = 0
4544
system_id |= (secs << 32)
4645
system_id |= (usecs << 12)
47-
system_id |= (os.getpid() & 0xFFF)
46+
system_id |= (tconf.os_ops.get_pid() & 0xFFF)
4847

4948
# pack ULL in native byte order
5049
return struct.pack('=Q', system_id)

0 commit comments

Comments
 (0)