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/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)