testgres-pg-probackup2 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pg_probackup2/__init__.py +0 -0
- pg_probackup2/app.py +762 -0
- pg_probackup2/gdb.py +346 -0
- pg_probackup2/init_helpers.py +207 -0
- pg_probackup2/storage/__init__.py +0 -0
- pg_probackup2/storage/fs_backup.py +101 -0
- pg_probackup2/storage/s3_backup.py +134 -0
- testgres_pg_probackup2-0.0.1.dist-info/METADATA +15 -0
- testgres_pg_probackup2-0.0.1.dist-info/RECORD +11 -0
- testgres_pg_probackup2-0.0.1.dist-info/WHEEL +5 -0
- testgres_pg_probackup2-0.0.1.dist-info/top_level.txt +1 -0
pg_probackup2/app.py
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import importlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import unittest
|
|
11
|
+
|
|
12
|
+
import testgres
|
|
13
|
+
|
|
14
|
+
from .storage.fs_backup import TestBackupDir, FSTestBackupDir
|
|
15
|
+
from .gdb import GDBobj
|
|
16
|
+
from .init_helpers import init_params
|
|
17
|
+
|
|
18
|
+
warning = """
|
|
19
|
+
Wrong splint in show_pb
|
|
20
|
+
Original Header:f
|
|
21
|
+
{header}
|
|
22
|
+
Original Body:
|
|
23
|
+
{body}
|
|
24
|
+
Splitted Header
|
|
25
|
+
{header_split}
|
|
26
|
+
Splitted Body
|
|
27
|
+
{body_split}
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProbackupException(Exception):
|
|
32
|
+
def __init__(self, message, cmd):
|
|
33
|
+
self.message = message
|
|
34
|
+
self.cmd = cmd
|
|
35
|
+
|
|
36
|
+
def __str__(self):
|
|
37
|
+
return '\n ERROR: {0}\n CMD: {1}'.format(repr(self.message), self.cmd)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ProbackupApp:
|
|
41
|
+
|
|
42
|
+
def __init__(self, test_class: unittest.TestCase,
|
|
43
|
+
pg_node, pb_log_path, test_env, auto_compress_alg, backup_dir):
|
|
44
|
+
self.test_class = test_class
|
|
45
|
+
self.pg_node = pg_node
|
|
46
|
+
self.pb_log_path = pb_log_path
|
|
47
|
+
self.test_env = test_env
|
|
48
|
+
self.auto_compress_alg = auto_compress_alg
|
|
49
|
+
self.backup_dir = backup_dir
|
|
50
|
+
self.probackup_path = init_params.probackup_path
|
|
51
|
+
self.probackup_old_path = init_params.probackup_old_path
|
|
52
|
+
self.remote = init_params.remote
|
|
53
|
+
self.verbose = init_params.verbose
|
|
54
|
+
self.archive_compress = init_params.archive_compress
|
|
55
|
+
self.test_class.output = None
|
|
56
|
+
|
|
57
|
+
def run(self, command, gdb=False, old_binary=False, return_id=True, env=None,
|
|
58
|
+
skip_log_directory=False, expect_error=False, use_backup_dir=True):
|
|
59
|
+
"""
|
|
60
|
+
Run pg_probackup
|
|
61
|
+
backup_dir: target directory for making backup
|
|
62
|
+
command: commandline options
|
|
63
|
+
expect_error: option for ignoring errors and getting error message as a result of running the function
|
|
64
|
+
gdb: when True it returns GDBObj(), when tuple('suspend', port) it runs probackup
|
|
65
|
+
in suspended gdb mode with attachable gdb port, for local debugging
|
|
66
|
+
"""
|
|
67
|
+
if isinstance(use_backup_dir, TestBackupDir):
|
|
68
|
+
command = [command[0], *use_backup_dir.pb_args, *command[1:]]
|
|
69
|
+
elif use_backup_dir:
|
|
70
|
+
command = [command[0], *self.backup_dir.pb_args, *command[1:]]
|
|
71
|
+
|
|
72
|
+
if not self.probackup_old_path and old_binary:
|
|
73
|
+
print('PGPROBACKUPBIN_OLD is not set')
|
|
74
|
+
exit(1)
|
|
75
|
+
|
|
76
|
+
if old_binary:
|
|
77
|
+
binary_path = self.probackup_old_path
|
|
78
|
+
else:
|
|
79
|
+
binary_path = self.probackup_path
|
|
80
|
+
|
|
81
|
+
if not env:
|
|
82
|
+
env = self.test_env
|
|
83
|
+
|
|
84
|
+
strcommand = ' '.join(str(p) for p in command)
|
|
85
|
+
if '--log-level-file' in strcommand and \
|
|
86
|
+
'--log-directory' not in strcommand and \
|
|
87
|
+
not skip_log_directory:
|
|
88
|
+
command += ['--log-directory=' + self.pb_log_path]
|
|
89
|
+
strcommand += ' ' + command[-1]
|
|
90
|
+
|
|
91
|
+
if 'pglz' in strcommand and \
|
|
92
|
+
' -j' not in strcommand and '--thread' not in strcommand:
|
|
93
|
+
command += ['-j', '1']
|
|
94
|
+
strcommand += ' -j 1'
|
|
95
|
+
|
|
96
|
+
self.test_class.cmd = binary_path + ' ' + strcommand
|
|
97
|
+
if self.verbose:
|
|
98
|
+
print(self.test_class.cmd)
|
|
99
|
+
|
|
100
|
+
cmdline = [binary_path, *command]
|
|
101
|
+
if gdb is True:
|
|
102
|
+
# general test flow for using GDBObj
|
|
103
|
+
return GDBobj(cmdline, self.test_class)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
result = None
|
|
107
|
+
if type(gdb) is tuple and gdb[0] == 'suspend':
|
|
108
|
+
# special test flow for manually debug probackup
|
|
109
|
+
gdb_port = gdb[1]
|
|
110
|
+
cmdline = ['gdbserver'] + ['localhost:' + str(gdb_port)] + cmdline
|
|
111
|
+
print("pg_probackup gdb suspended, waiting gdb connection on localhost:{0}".format(gdb_port))
|
|
112
|
+
|
|
113
|
+
self.test_class.output = subprocess.check_output(
|
|
114
|
+
cmdline,
|
|
115
|
+
stderr=subprocess.STDOUT,
|
|
116
|
+
env=env
|
|
117
|
+
).decode('utf-8', errors='replace')
|
|
118
|
+
if command[0] == 'backup' and return_id:
|
|
119
|
+
# return backup ID
|
|
120
|
+
for line in self.test_class.output.splitlines():
|
|
121
|
+
if 'INFO: Backup' and 'completed' in line:
|
|
122
|
+
result = line.split()[2]
|
|
123
|
+
else:
|
|
124
|
+
result = self.test_class.output
|
|
125
|
+
if expect_error is True:
|
|
126
|
+
assert False, f"Exception was expected, but run finished successful with result: `{result}`\n" \
|
|
127
|
+
f"CMD: {self.test_class.cmd}"
|
|
128
|
+
elif expect_error:
|
|
129
|
+
assert False, f"Exception was expected {expect_error}, but run finished successful with result: `{result}`\n" \
|
|
130
|
+
f"CMD: {self.test_class.cmd}"
|
|
131
|
+
return result
|
|
132
|
+
except subprocess.CalledProcessError as e:
|
|
133
|
+
self.test_class.output = e.output.decode('utf-8').replace("\r", "")
|
|
134
|
+
if expect_error:
|
|
135
|
+
return self.test_class.output
|
|
136
|
+
else:
|
|
137
|
+
raise ProbackupException(self.test_class.output, self.test_class.cmd)
|
|
138
|
+
|
|
139
|
+
def init(self, options=None, old_binary=False, skip_log_directory=False, expect_error=False, use_backup_dir=True):
|
|
140
|
+
if options is None:
|
|
141
|
+
options = []
|
|
142
|
+
return self.run(['init'] + options,
|
|
143
|
+
old_binary=old_binary,
|
|
144
|
+
skip_log_directory=skip_log_directory,
|
|
145
|
+
expect_error=expect_error,
|
|
146
|
+
use_backup_dir=use_backup_dir
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def add_instance(self, instance, node, old_binary=False, options=None, expect_error=False):
|
|
150
|
+
if options is None:
|
|
151
|
+
options = []
|
|
152
|
+
cmd = [
|
|
153
|
+
'add-instance',
|
|
154
|
+
'--instance={0}'.format(instance),
|
|
155
|
+
'-D', node.data_dir
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
# don`t forget to kill old_binary after remote ssh release
|
|
159
|
+
if self.remote and not old_binary:
|
|
160
|
+
options = options + [
|
|
161
|
+
'--remote-proto=ssh',
|
|
162
|
+
'--remote-host=localhost']
|
|
163
|
+
|
|
164
|
+
return self.run(cmd + options, old_binary=old_binary, expect_error=expect_error)
|
|
165
|
+
|
|
166
|
+
def set_config(self, instance, old_binary=False, options=None, expect_error=False):
|
|
167
|
+
if options is None:
|
|
168
|
+
options = []
|
|
169
|
+
cmd = [
|
|
170
|
+
'set-config',
|
|
171
|
+
'--instance={0}'.format(instance),
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
return self.run(cmd + options, old_binary=old_binary, expect_error=expect_error)
|
|
175
|
+
|
|
176
|
+
def set_backup(self, instance, backup_id=False,
|
|
177
|
+
old_binary=False, options=None, expect_error=False):
|
|
178
|
+
if options is None:
|
|
179
|
+
options = []
|
|
180
|
+
cmd = [
|
|
181
|
+
'set-backup',
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
if instance:
|
|
185
|
+
cmd = cmd + ['--instance={0}'.format(instance)]
|
|
186
|
+
|
|
187
|
+
if backup_id:
|
|
188
|
+
cmd = cmd + ['-i', backup_id]
|
|
189
|
+
|
|
190
|
+
return self.run(cmd + options, old_binary=old_binary, expect_error=expect_error)
|
|
191
|
+
|
|
192
|
+
def del_instance(self, instance, old_binary=False, expect_error=False):
|
|
193
|
+
|
|
194
|
+
return self.run([
|
|
195
|
+
'del-instance',
|
|
196
|
+
'--instance={0}'.format(instance),
|
|
197
|
+
],
|
|
198
|
+
old_binary=old_binary,
|
|
199
|
+
expect_error=expect_error
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def backup_node(
|
|
203
|
+
self, instance, node, data_dir=False,
|
|
204
|
+
backup_type='full', datname=False, options=None,
|
|
205
|
+
gdb=False,
|
|
206
|
+
old_binary=False, return_id=True, no_remote=False,
|
|
207
|
+
env=None,
|
|
208
|
+
expect_error=False,
|
|
209
|
+
sync=False
|
|
210
|
+
):
|
|
211
|
+
if options is None:
|
|
212
|
+
options = []
|
|
213
|
+
if not node and not data_dir:
|
|
214
|
+
print('You must provide ether node or data_dir for backup')
|
|
215
|
+
exit(1)
|
|
216
|
+
|
|
217
|
+
if not datname:
|
|
218
|
+
datname = 'postgres'
|
|
219
|
+
|
|
220
|
+
cmd_list = [
|
|
221
|
+
'backup',
|
|
222
|
+
'--instance={0}'.format(instance),
|
|
223
|
+
# "-D", pgdata,
|
|
224
|
+
'-p', '%i' % node.port,
|
|
225
|
+
'-d', datname
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
if data_dir:
|
|
229
|
+
cmd_list += ['-D', self._node_dir(data_dir)]
|
|
230
|
+
|
|
231
|
+
# don`t forget to kill old_binary after remote ssh release
|
|
232
|
+
if self.remote and not old_binary and not no_remote:
|
|
233
|
+
options = options + [
|
|
234
|
+
'--remote-proto=ssh',
|
|
235
|
+
'--remote-host=localhost']
|
|
236
|
+
|
|
237
|
+
if self.auto_compress_alg and '--compress' in options and \
|
|
238
|
+
self.archive_compress and self.archive_compress != 'zlib':
|
|
239
|
+
options = [o if o != '--compress' else f'--compress-algorithm={self.archive_compress}'
|
|
240
|
+
for o in options]
|
|
241
|
+
|
|
242
|
+
if backup_type:
|
|
243
|
+
cmd_list += ['-b', backup_type]
|
|
244
|
+
|
|
245
|
+
if not (old_binary or sync):
|
|
246
|
+
cmd_list += ['--no-sync']
|
|
247
|
+
|
|
248
|
+
return self.run(cmd_list + options, gdb, old_binary, return_id, env=env,
|
|
249
|
+
expect_error=expect_error)
|
|
250
|
+
|
|
251
|
+
def backup_replica_node(self, instance, node, data_dir=False, *,
|
|
252
|
+
master, backup_type='full', datname=False,
|
|
253
|
+
options=None, env=None):
|
|
254
|
+
"""
|
|
255
|
+
Try to reliably run backup on replica by switching wal at master
|
|
256
|
+
at the moment pg_probackup is waiting for archived wal segment
|
|
257
|
+
"""
|
|
258
|
+
if options is None:
|
|
259
|
+
options = []
|
|
260
|
+
assert '--stream' not in options or backup_type == 'page', \
|
|
261
|
+
"backup_replica_node should be used with one of archive-mode or " \
|
|
262
|
+
"page-stream mode"
|
|
263
|
+
|
|
264
|
+
options = options.copy()
|
|
265
|
+
if not any('--log-level-file' in x for x in options):
|
|
266
|
+
options.append('--log-level-file=INFO')
|
|
267
|
+
|
|
268
|
+
gdb = self.backup_node(
|
|
269
|
+
instance, node, data_dir,
|
|
270
|
+
backup_type=backup_type,
|
|
271
|
+
datname=datname,
|
|
272
|
+
options=options,
|
|
273
|
+
env=env,
|
|
274
|
+
gdb=True)
|
|
275
|
+
gdb.set_breakpoint('wait_wal_lsn')
|
|
276
|
+
# we need to break on wait_wal_lsn in pg_stop_backup
|
|
277
|
+
gdb.run_until_break()
|
|
278
|
+
if backup_type == 'page':
|
|
279
|
+
self.switch_wal_segment(master)
|
|
280
|
+
if '--stream' not in options:
|
|
281
|
+
gdb.continue_execution_until_break()
|
|
282
|
+
self.switch_wal_segment(master)
|
|
283
|
+
gdb.continue_execution_until_exit()
|
|
284
|
+
|
|
285
|
+
output = self.read_pb_log()
|
|
286
|
+
self.unlink_pg_log()
|
|
287
|
+
parsed_output = re.compile(r'Backup \S+ completed').search(output)
|
|
288
|
+
assert parsed_output, f"Expected: `Backup 'backup_id' completed`, but found `{output}`"
|
|
289
|
+
backup_id = parsed_output[0].split(' ')[1]
|
|
290
|
+
return (backup_id, output)
|
|
291
|
+
|
|
292
|
+
def checkdb_node(
|
|
293
|
+
self, use_backup_dir=False, instance=False, data_dir=False,
|
|
294
|
+
options=None, gdb=False, old_binary=False,
|
|
295
|
+
skip_log_directory=False,
|
|
296
|
+
expect_error=False
|
|
297
|
+
):
|
|
298
|
+
if options is None:
|
|
299
|
+
options = []
|
|
300
|
+
cmd_list = ["checkdb"]
|
|
301
|
+
|
|
302
|
+
if instance:
|
|
303
|
+
cmd_list += ["--instance={0}".format(instance)]
|
|
304
|
+
|
|
305
|
+
if data_dir:
|
|
306
|
+
cmd_list += ["-D", self._node_dir(data_dir)]
|
|
307
|
+
|
|
308
|
+
return self.run(cmd_list + options, gdb, old_binary,
|
|
309
|
+
skip_log_directory=skip_log_directory, expect_error=expect_error,
|
|
310
|
+
use_backup_dir=use_backup_dir)
|
|
311
|
+
|
|
312
|
+
def merge_backup(
|
|
313
|
+
self, instance, backup_id,
|
|
314
|
+
gdb=False, old_binary=False, options=None, expect_error=False):
|
|
315
|
+
if options is None:
|
|
316
|
+
options = []
|
|
317
|
+
cmd_list = [
|
|
318
|
+
'merge',
|
|
319
|
+
'--instance={0}'.format(instance),
|
|
320
|
+
'-i', backup_id
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
return self.run(cmd_list + options, gdb, old_binary, expect_error=expect_error)
|
|
324
|
+
|
|
325
|
+
def restore_node(
|
|
326
|
+
self, instance, node=None, restore_dir=None,
|
|
327
|
+
backup_id=None, old_binary=False, options=None,
|
|
328
|
+
gdb=False,
|
|
329
|
+
expect_error=False,
|
|
330
|
+
sync=False
|
|
331
|
+
):
|
|
332
|
+
if options is None:
|
|
333
|
+
options = []
|
|
334
|
+
if node:
|
|
335
|
+
if isinstance(node, str):
|
|
336
|
+
data_dir = node
|
|
337
|
+
else:
|
|
338
|
+
data_dir = node.data_dir
|
|
339
|
+
elif restore_dir:
|
|
340
|
+
data_dir = self._node_dir(restore_dir)
|
|
341
|
+
else:
|
|
342
|
+
raise ValueError("You must provide ether node or base_dir for backup")
|
|
343
|
+
|
|
344
|
+
cmd_list = [
|
|
345
|
+
'restore',
|
|
346
|
+
'-D', data_dir,
|
|
347
|
+
'--instance={0}'.format(instance)
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
# don`t forget to kill old_binary after remote ssh release
|
|
351
|
+
if self.remote and not old_binary:
|
|
352
|
+
options = options + [
|
|
353
|
+
'--remote-proto=ssh',
|
|
354
|
+
'--remote-host=localhost']
|
|
355
|
+
|
|
356
|
+
if backup_id:
|
|
357
|
+
cmd_list += ['-i', backup_id]
|
|
358
|
+
|
|
359
|
+
if not (old_binary or sync):
|
|
360
|
+
cmd_list += ['--no-sync']
|
|
361
|
+
|
|
362
|
+
return self.run(cmd_list + options, gdb=gdb, old_binary=old_binary, expect_error=expect_error)
|
|
363
|
+
|
|
364
|
+
def catchup_node(
|
|
365
|
+
self,
|
|
366
|
+
backup_mode, source_pgdata, destination_node,
|
|
367
|
+
options=None,
|
|
368
|
+
remote_host='localhost',
|
|
369
|
+
expect_error=False,
|
|
370
|
+
gdb=False
|
|
371
|
+
):
|
|
372
|
+
|
|
373
|
+
if options is None:
|
|
374
|
+
options = []
|
|
375
|
+
cmd_list = [
|
|
376
|
+
'catchup',
|
|
377
|
+
'--backup-mode={0}'.format(backup_mode),
|
|
378
|
+
'--source-pgdata={0}'.format(source_pgdata),
|
|
379
|
+
'--destination-pgdata={0}'.format(destination_node.data_dir)
|
|
380
|
+
]
|
|
381
|
+
if self.remote:
|
|
382
|
+
cmd_list += ['--remote-proto=ssh', '--remote-host=%s' % remote_host]
|
|
383
|
+
if self.verbose:
|
|
384
|
+
cmd_list += [
|
|
385
|
+
'--log-level-file=VERBOSE',
|
|
386
|
+
'--log-directory={0}'.format(destination_node.logs_dir)
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
return self.run(cmd_list + options, gdb=gdb, expect_error=expect_error, use_backup_dir=False)
|
|
390
|
+
|
|
391
|
+
def show(
|
|
392
|
+
self, instance=None, backup_id=None,
|
|
393
|
+
options=None, as_text=False, as_json=True, old_binary=False,
|
|
394
|
+
env=None,
|
|
395
|
+
expect_error=False,
|
|
396
|
+
gdb=False
|
|
397
|
+
):
|
|
398
|
+
|
|
399
|
+
if options is None:
|
|
400
|
+
options = []
|
|
401
|
+
backup_list = []
|
|
402
|
+
specific_record = {}
|
|
403
|
+
cmd_list = [
|
|
404
|
+
'show',
|
|
405
|
+
]
|
|
406
|
+
if instance:
|
|
407
|
+
cmd_list += ['--instance={0}'.format(instance)]
|
|
408
|
+
|
|
409
|
+
if backup_id:
|
|
410
|
+
cmd_list += ['-i', backup_id]
|
|
411
|
+
|
|
412
|
+
# AHTUNG, WARNING will break json parsing
|
|
413
|
+
if as_json:
|
|
414
|
+
cmd_list += ['--format=json', '--log-level-console=error']
|
|
415
|
+
|
|
416
|
+
if as_text:
|
|
417
|
+
# You should print it when calling as_text=true
|
|
418
|
+
return self.run(cmd_list + options, old_binary=old_binary, env=env,
|
|
419
|
+
expect_error=expect_error, gdb=gdb)
|
|
420
|
+
|
|
421
|
+
# get show result as list of lines
|
|
422
|
+
if as_json:
|
|
423
|
+
text_json = str(self.run(cmd_list + options, old_binary=old_binary, env=env,
|
|
424
|
+
expect_error=expect_error, gdb=gdb))
|
|
425
|
+
try:
|
|
426
|
+
if expect_error:
|
|
427
|
+
return text_json
|
|
428
|
+
data = json.loads(text_json)
|
|
429
|
+
except ValueError:
|
|
430
|
+
assert False, f"Couldn't parse {text_json} as json. " \
|
|
431
|
+
f"Check that you don't have additional messages inside the log or use 'as_text=True'"
|
|
432
|
+
|
|
433
|
+
for instance_data in data:
|
|
434
|
+
# find specific instance if requested
|
|
435
|
+
if instance and instance_data['instance'] != instance:
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
for backup in reversed(instance_data['backups']):
|
|
439
|
+
# find specific backup if requested
|
|
440
|
+
if backup_id:
|
|
441
|
+
if backup['id'] == backup_id:
|
|
442
|
+
return backup
|
|
443
|
+
else:
|
|
444
|
+
backup_list.append(backup)
|
|
445
|
+
|
|
446
|
+
if backup_id is not None:
|
|
447
|
+
assert False, "Failed to find backup with ID: {0}".format(backup_id)
|
|
448
|
+
|
|
449
|
+
return backup_list
|
|
450
|
+
else:
|
|
451
|
+
show_splitted = self.run(cmd_list + options, old_binary=old_binary, env=env,
|
|
452
|
+
expect_error=expect_error).splitlines()
|
|
453
|
+
if instance is not None and backup_id is None:
|
|
454
|
+
# cut header(ID, Mode, etc) from show as single string
|
|
455
|
+
header = show_splitted[1:2][0]
|
|
456
|
+
# cut backup records from show as single list
|
|
457
|
+
# with string for every backup record
|
|
458
|
+
body = show_splitted[3:]
|
|
459
|
+
# inverse list so oldest record come first
|
|
460
|
+
body = body[::-1]
|
|
461
|
+
# split string in list with string for every header element
|
|
462
|
+
header_split = re.split(' +', header)
|
|
463
|
+
# Remove empty items
|
|
464
|
+
for i in header_split:
|
|
465
|
+
if i == '':
|
|
466
|
+
header_split.remove(i)
|
|
467
|
+
continue
|
|
468
|
+
header_split = [
|
|
469
|
+
header_element.rstrip() for header_element in header_split
|
|
470
|
+
]
|
|
471
|
+
for backup_record in body:
|
|
472
|
+
backup_record = backup_record.rstrip()
|
|
473
|
+
# split list with str for every backup record element
|
|
474
|
+
backup_record_split = re.split(' +', backup_record)
|
|
475
|
+
# Remove empty items
|
|
476
|
+
for i in backup_record_split:
|
|
477
|
+
if i == '':
|
|
478
|
+
backup_record_split.remove(i)
|
|
479
|
+
if len(header_split) != len(backup_record_split):
|
|
480
|
+
print(warning.format(
|
|
481
|
+
header=header, body=body,
|
|
482
|
+
header_split=header_split,
|
|
483
|
+
body_split=backup_record_split)
|
|
484
|
+
)
|
|
485
|
+
exit(1)
|
|
486
|
+
new_dict = dict(zip(header_split, backup_record_split))
|
|
487
|
+
backup_list.append(new_dict)
|
|
488
|
+
return backup_list
|
|
489
|
+
else:
|
|
490
|
+
# cut out empty lines and lines started with #
|
|
491
|
+
# and other garbage then reconstruct it as dictionary
|
|
492
|
+
# print show_splitted
|
|
493
|
+
sanitized_show = [item for item in show_splitted if item]
|
|
494
|
+
sanitized_show = [
|
|
495
|
+
item for item in sanitized_show if not item.startswith('#')
|
|
496
|
+
]
|
|
497
|
+
# print sanitized_show
|
|
498
|
+
for line in sanitized_show:
|
|
499
|
+
name, var = line.partition(' = ')[::2]
|
|
500
|
+
var = var.strip('"')
|
|
501
|
+
var = var.strip("'")
|
|
502
|
+
specific_record[name.strip()] = var
|
|
503
|
+
|
|
504
|
+
if not specific_record:
|
|
505
|
+
assert False, "Failed to find backup with ID: {0}".format(backup_id)
|
|
506
|
+
|
|
507
|
+
return specific_record
|
|
508
|
+
|
|
509
|
+
def show_archive(
|
|
510
|
+
self, instance=None, options=None,
|
|
511
|
+
as_text=False, as_json=True, old_binary=False,
|
|
512
|
+
tli=0,
|
|
513
|
+
expect_error=False
|
|
514
|
+
):
|
|
515
|
+
if options is None:
|
|
516
|
+
options = []
|
|
517
|
+
cmd_list = [
|
|
518
|
+
'show',
|
|
519
|
+
'--archive',
|
|
520
|
+
]
|
|
521
|
+
if instance:
|
|
522
|
+
cmd_list += ['--instance={0}'.format(instance)]
|
|
523
|
+
|
|
524
|
+
# AHTUNG, WARNING will break json parsing
|
|
525
|
+
if as_json:
|
|
526
|
+
cmd_list += ['--format=json', '--log-level-console=error']
|
|
527
|
+
|
|
528
|
+
if as_text:
|
|
529
|
+
# You should print it when calling as_text=true
|
|
530
|
+
return self.run(cmd_list + options, old_binary=old_binary, expect_error=expect_error)
|
|
531
|
+
|
|
532
|
+
if as_json:
|
|
533
|
+
if as_text:
|
|
534
|
+
data = self.run(cmd_list + options, old_binary=old_binary, expect_error=expect_error)
|
|
535
|
+
else:
|
|
536
|
+
data = json.loads(self.run(cmd_list + options, old_binary=old_binary, expect_error=expect_error))
|
|
537
|
+
|
|
538
|
+
if instance:
|
|
539
|
+
instance_timelines = None
|
|
540
|
+
for instance_name in data:
|
|
541
|
+
if instance_name['instance'] == instance:
|
|
542
|
+
instance_timelines = instance_name['timelines']
|
|
543
|
+
break
|
|
544
|
+
|
|
545
|
+
if tli > 0:
|
|
546
|
+
for timeline in instance_timelines:
|
|
547
|
+
if timeline['tli'] == tli:
|
|
548
|
+
return timeline
|
|
549
|
+
|
|
550
|
+
return {}
|
|
551
|
+
|
|
552
|
+
if instance_timelines:
|
|
553
|
+
return instance_timelines
|
|
554
|
+
|
|
555
|
+
return data
|
|
556
|
+
else:
|
|
557
|
+
show_splitted = self.run(cmd_list + options, old_binary=old_binary,
|
|
558
|
+
expect_error=expect_error).splitlines()
|
|
559
|
+
print(show_splitted)
|
|
560
|
+
exit(1)
|
|
561
|
+
|
|
562
|
+
def validate(
|
|
563
|
+
self, instance=None, backup_id=None,
|
|
564
|
+
options=None, old_binary=False, gdb=False, expect_error=False
|
|
565
|
+
):
|
|
566
|
+
if options is None:
|
|
567
|
+
options = []
|
|
568
|
+
cmd_list = [
|
|
569
|
+
'validate',
|
|
570
|
+
]
|
|
571
|
+
if instance:
|
|
572
|
+
cmd_list += ['--instance={0}'.format(instance)]
|
|
573
|
+
if backup_id:
|
|
574
|
+
cmd_list += ['-i', backup_id]
|
|
575
|
+
|
|
576
|
+
return self.run(cmd_list + options, old_binary=old_binary, gdb=gdb,
|
|
577
|
+
expect_error=expect_error)
|
|
578
|
+
|
|
579
|
+
def delete(
|
|
580
|
+
self, instance, backup_id=None,
|
|
581
|
+
options=None, old_binary=False, gdb=False, expect_error=False):
|
|
582
|
+
if options is None:
|
|
583
|
+
options = []
|
|
584
|
+
cmd_list = [
|
|
585
|
+
'delete',
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
cmd_list += ['--instance={0}'.format(instance)]
|
|
589
|
+
if backup_id:
|
|
590
|
+
cmd_list += ['-i', backup_id]
|
|
591
|
+
|
|
592
|
+
return self.run(cmd_list + options, old_binary=old_binary, gdb=gdb,
|
|
593
|
+
expect_error=expect_error)
|
|
594
|
+
|
|
595
|
+
def delete_expired(
|
|
596
|
+
self, instance, options=None, old_binary=False, expect_error=False):
|
|
597
|
+
if options is None:
|
|
598
|
+
options = []
|
|
599
|
+
cmd_list = [
|
|
600
|
+
'delete',
|
|
601
|
+
'--instance={0}'.format(instance)
|
|
602
|
+
]
|
|
603
|
+
return self.run(cmd_list + options, old_binary=old_binary, expect_error=expect_error)
|
|
604
|
+
|
|
605
|
+
def show_config(self, instance, old_binary=False, expect_error=False, gdb=False):
|
|
606
|
+
out_dict = {}
|
|
607
|
+
cmd_list = [
|
|
608
|
+
'show-config',
|
|
609
|
+
'--instance={0}'.format(instance)
|
|
610
|
+
]
|
|
611
|
+
|
|
612
|
+
res = self.run(cmd_list, old_binary=old_binary, expect_error=expect_error, gdb=gdb).splitlines()
|
|
613
|
+
for line in res:
|
|
614
|
+
if not line.startswith('#'):
|
|
615
|
+
name, var = line.partition(' = ')[::2]
|
|
616
|
+
out_dict[name] = var
|
|
617
|
+
return out_dict
|
|
618
|
+
|
|
619
|
+
def run_binary(self, command, asynchronous=False, env=None):
|
|
620
|
+
|
|
621
|
+
if not env:
|
|
622
|
+
env = self.test_env
|
|
623
|
+
|
|
624
|
+
if self.verbose:
|
|
625
|
+
print([' '.join(map(str, command))])
|
|
626
|
+
try:
|
|
627
|
+
if asynchronous:
|
|
628
|
+
return subprocess.Popen(
|
|
629
|
+
command,
|
|
630
|
+
stdin=subprocess.PIPE,
|
|
631
|
+
stdout=subprocess.PIPE,
|
|
632
|
+
stderr=subprocess.PIPE,
|
|
633
|
+
env=env
|
|
634
|
+
)
|
|
635
|
+
else:
|
|
636
|
+
self.test_class.output = subprocess.check_output(
|
|
637
|
+
command,
|
|
638
|
+
stderr=subprocess.STDOUT,
|
|
639
|
+
env=env
|
|
640
|
+
).decode('utf-8')
|
|
641
|
+
return self.test_class.output
|
|
642
|
+
except subprocess.CalledProcessError as e:
|
|
643
|
+
raise ProbackupException(e.output.decode('utf-8'), command)
|
|
644
|
+
|
|
645
|
+
def _node_dir(self, base_dir):
|
|
646
|
+
return os.path.join(self.pg_node.test_path, base_dir)
|
|
647
|
+
|
|
648
|
+
def set_archiving(
|
|
649
|
+
self, instance, node, replica=False,
|
|
650
|
+
overwrite=False, compress=True, old_binary=False,
|
|
651
|
+
log_level=False, archive_timeout=False,
|
|
652
|
+
custom_archive_command=None):
|
|
653
|
+
|
|
654
|
+
# parse postgresql.auto.conf
|
|
655
|
+
options = {}
|
|
656
|
+
if replica:
|
|
657
|
+
options['archive_mode'] = 'always'
|
|
658
|
+
options['hot_standby'] = 'on'
|
|
659
|
+
else:
|
|
660
|
+
options['archive_mode'] = 'on'
|
|
661
|
+
|
|
662
|
+
if custom_archive_command is None:
|
|
663
|
+
archive_command = " ".join([f'"{init_params.probackup_path}"',
|
|
664
|
+
'archive-push', *self.backup_dir.pb_args])
|
|
665
|
+
if os.name == "nt":
|
|
666
|
+
archive_command = archive_command.replace("\\", "\\\\")
|
|
667
|
+
archive_command += f' --instance={instance}'
|
|
668
|
+
|
|
669
|
+
# don`t forget to kill old_binary after remote ssh release
|
|
670
|
+
if init_params.remote and not old_binary:
|
|
671
|
+
archive_command += ' --remote-proto=ssh --remote-host=localhost'
|
|
672
|
+
|
|
673
|
+
if init_params.archive_compress and compress:
|
|
674
|
+
archive_command += ' --compress-algorithm=' + init_params.archive_compress
|
|
675
|
+
|
|
676
|
+
if overwrite:
|
|
677
|
+
archive_command += ' --overwrite'
|
|
678
|
+
|
|
679
|
+
archive_command += ' --log-level-console=VERBOSE'
|
|
680
|
+
archive_command += ' -j 5'
|
|
681
|
+
archive_command += ' --batch-size 10'
|
|
682
|
+
archive_command += ' --no-sync'
|
|
683
|
+
|
|
684
|
+
if archive_timeout:
|
|
685
|
+
archive_command += f' --archive-timeout={archive_timeout}'
|
|
686
|
+
|
|
687
|
+
if os.name == 'posix':
|
|
688
|
+
archive_command += ' --wal-file-path=%p --wal-file-name=%f'
|
|
689
|
+
|
|
690
|
+
elif os.name == 'nt':
|
|
691
|
+
archive_command += ' --wal-file-path="%p" --wal-file-name="%f"'
|
|
692
|
+
|
|
693
|
+
if log_level:
|
|
694
|
+
archive_command += f' --log-level-console={log_level}'
|
|
695
|
+
else: # custom_archive_command is not None
|
|
696
|
+
archive_command = custom_archive_command
|
|
697
|
+
options['archive_command'] = archive_command
|
|
698
|
+
|
|
699
|
+
node.set_auto_conf(options)
|
|
700
|
+
|
|
701
|
+
def switch_wal_segment(self, node, sleep_seconds=1, and_tx=False):
|
|
702
|
+
"""
|
|
703
|
+
Execute pg_switch_wal() in given node
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
node: an instance of PostgresNode or NodeConnection class
|
|
707
|
+
"""
|
|
708
|
+
if isinstance(node, testgres.PostgresNode):
|
|
709
|
+
with node.connect('postgres') as con:
|
|
710
|
+
if and_tx:
|
|
711
|
+
con.execute('select txid_current()')
|
|
712
|
+
lsn = con.execute('select pg_switch_wal()')[0][0]
|
|
713
|
+
else:
|
|
714
|
+
lsn = node.execute('select pg_switch_wal()')[0][0]
|
|
715
|
+
|
|
716
|
+
if sleep_seconds > 0:
|
|
717
|
+
time.sleep(sleep_seconds)
|
|
718
|
+
return lsn
|
|
719
|
+
|
|
720
|
+
@contextlib.contextmanager
|
|
721
|
+
def switch_wal_after(self, node, seconds, and_tx=True):
|
|
722
|
+
tm = threading.Timer(seconds, self.switch_wal_segment, [node, 0, and_tx])
|
|
723
|
+
tm.start()
|
|
724
|
+
try:
|
|
725
|
+
yield
|
|
726
|
+
finally:
|
|
727
|
+
tm.cancel()
|
|
728
|
+
tm.join()
|
|
729
|
+
|
|
730
|
+
def read_pb_log(self):
|
|
731
|
+
with open(os.path.join(self.pb_log_path, 'pg_probackup.log')) as fl:
|
|
732
|
+
return fl.read()
|
|
733
|
+
|
|
734
|
+
def unlink_pg_log(self):
|
|
735
|
+
os.unlink(os.path.join(self.pb_log_path, 'pg_probackup.log'))
|
|
736
|
+
|
|
737
|
+
def load_backup_class(fs_type):
|
|
738
|
+
fs_type = os.environ.get('PROBACKUP_FS_TYPE')
|
|
739
|
+
implementation = f"{__package__}.fs_backup.FSTestBackupDir"
|
|
740
|
+
if fs_type:
|
|
741
|
+
implementation = fs_type
|
|
742
|
+
|
|
743
|
+
print("Using ", implementation)
|
|
744
|
+
module_name, class_name = implementation.rsplit(sep='.', maxsplit=1)
|
|
745
|
+
|
|
746
|
+
module = importlib.import_module(module_name)
|
|
747
|
+
|
|
748
|
+
return getattr(module, class_name)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
# Local or S3 backup
|
|
752
|
+
fs_backup_class = FSTestBackupDir
|
|
753
|
+
if os.environ.get('PG_PROBACKUP_S3_TEST', os.environ.get('PROBACKUP_S3_TYPE_FULL_TEST')):
|
|
754
|
+
root = os.path.realpath(os.path.join(os.path.dirname(__file__), '../..'))
|
|
755
|
+
if root not in sys.path:
|
|
756
|
+
sys.path.append(root)
|
|
757
|
+
from pg_probackup2.storage.s3_backup import S3TestBackupDir
|
|
758
|
+
|
|
759
|
+
fs_backup_class = S3TestBackupDir
|
|
760
|
+
|
|
761
|
+
def build_backup_dir(self, backup='backup'):
|
|
762
|
+
return fs_backup_class(rel_path=self.rel_path, backup=backup)
|