1# -*- coding: utf-8 -*-
2# © Copyright EnterpriseDB UK Limited 2014-2021
3#
4# This file is part of Barman.
5#
6# Barman is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# Barman is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Barman.  If not, see <http://www.gnu.org/licenses/>.
18
19import sys
20from datetime import datetime, timedelta
21from shutil import rmtree
22
23import mock
24from dateutil import tz
25
26from barman.backup import BackupManager
27from barman.config import BackupOptions, Config
28from barman.infofile import BackupInfo, LocalBackupInfo, Tablespace, WalFileInfo
29from barman.server import Server
30from barman.utils import mkpath
31from barman.xlog import DEFAULT_XLOG_SEG_SIZE
32
33try:
34    from cStringIO import StringIO
35except ImportError:
36    from io import StringIO
37
38
39def build_test_backup_info(
40    backup_id="1234567890",
41    begin_offset=40,
42    begin_time=None,
43    begin_wal="000000010000000000000002",
44    begin_xlog="0/2000028",
45    config_file="/pgdata/location/postgresql.conf",
46    end_offset=184,
47    end_time=None,
48    end_wal="000000010000000000000002",
49    end_xlog="0/20000B8",
50    error=None,
51    hba_file="/pgdata/location/pg_hba.conf",
52    ident_file="/pgdata/location/pg_ident.conf",
53    mode="default",
54    pgdata="/pgdata/location",
55    server_name="test_server",
56    size=12345,
57    status=BackupInfo.DONE,
58    included_files=None,
59    tablespaces=(
60        ("tbs1", 16387, "/fake/location"),
61        ("tbs2", 16405, "/another/location"),
62    ),
63    timeline=1,
64    version=90302,
65    server=None,
66    copy_stats=None,
67):
68    """
69    Create an 'Ad Hoc' BackupInfo object for testing purposes.
70
71    A BackupInfo object is the barman representation of a physical backup,
72    for testing purposes is necessary to build a BackupInfo avoiding the usage
73    of Mock/MagicMock classes as much as possible.
74
75    :param str backup_id: the id of the backup
76    :param int begin_offset: begin_offset of the backup
77    :param datetime.datetime|None begin_time: begin_time of the backup
78    :param str begin_wal: begin_wal of the backup
79    :param str begin_xlog: begin_xlog of the backup
80    :param str config_file: config file of the backup
81    :param int end_offset: end_offset of the backup
82    :param datetime.datetime|None end_time: end_time of the backup
83    :param str end_wal: begin_xlog of the backup
84    :param str end_xlog: end_xlog of the backup
85    :param str|None error: error message for the backup
86    :param str hba_file: hba_file for the backup
87    :param str ident_file: ident_file for the backup
88    :param str mode: mode of execution of the backup
89    :param str pgdata: pg_data dir of the backup
90    :param str server_name: server name for the backup
91    :param int size: dimension of the backup
92    :param str status: status of the execution of the backup
93    :param list|None included_files: a list of extra configuration files
94    :param list|tuple|None tablespaces: a list of tablespaces for the backup
95    :param int timeline: timeline of the backup
96    :param int version: postgres version of the backup
97    :param barman.server.Server|None server: Server object for the backup
98    :param dict|None: Copy stats dictionary
99    :rtype: barman.infofile.LocalBackupInfo
100    """
101    if begin_time is None:
102        begin_time = datetime.now(tz.tzlocal()) - timedelta(minutes=10)
103    if end_time is None:
104        end_time = datetime.now(tz.tzlocal())
105
106    # Generate a list of tablespace objects (don't use a list comprehension
107    # or in python 2.x the 'item' variable will leak to the main context)
108    if tablespaces is not None:
109        tablespaces = list(Tablespace._make(item) for item in tablespaces)
110
111    # Manage the server for the Backup info: if no server is provided
112    # by the caller use a Mock with a basic configuration
113    if server is None:
114        server = mock.Mock(name=server_name)
115        server.config = build_config_from_dicts().get_server("main")
116        server.passive_node = False
117        server.backup_manager.name = "default"
118
119    backup_info = LocalBackupInfo(**locals())
120    return backup_info
121
122
123def mock_backup_ext_info(
124    backup_info=None,
125    previous_backup_id=None,
126    next_backup_id=None,
127    wal_num=1,
128    wal_size=123456,
129    wal_until_next_num=18,
130    wal_until_next_size=2345678,
131    wals_per_second=0.01,
132    wal_first="000000010000000000000014",
133    wal_first_timestamp=None,
134    wal_last="000000010000000000000014",
135    wal_last_timestamp=None,
136    retention_policy_status=None,
137    wal_compression_ratio=0.0,
138    wal_until_next_compression_ratio=0.0,
139    children_timelines=[],
140    copy_stats={},
141    **kwargs
142):
143
144    # make a dictionary with all the arguments
145    ext_info = dict(locals())
146    del ext_info["backup_info"]
147    if backup_info is None:
148        backup_info = build_test_backup_info(**kwargs)
149
150    # If the status is not DONE, the ext_info is empty
151    if backup_info.status != BackupInfo.DONE:
152        ext_info = {}
153
154    # merge the backup_info values
155    ext_info.update(backup_info.to_dict())
156
157    return ext_info
158
159
160def build_config_from_dicts(
161    global_conf=None, main_conf=None, test_conf=None, config_name=None
162):
163    """
164    Utility method, generate a barman.config.Config object
165
166    It has  a minimal configuration and a single server called "main".
167    All options can be override using the optional arguments
168
169    :param dict[str,str|None]|None global_conf: using this dictionary
170        it is possible to override or add new values to the [barman] section
171    :param dict[str,str|None]|None main_conf: using this dictionary
172        it is possible to override/add new values to the [main] section
173    :return barman.config.Config: a barman configuration object
174    """
175    # base barman section
176    base_barman = {
177        "barman_home": "/some/barman/home",
178        "barman_user": "{USER}",
179        "log_file": "%(barman_home)s/log/barman.log",
180        "archiver": True,
181    }
182    # base main section
183    base_main = {
184        "description": '" Text with quotes "',
185        "ssh_command": 'ssh -c "arcfour" -p 22 postgres@pg01.nowhere',
186        "conninfo": "host=pg01.nowhere user=postgres port=5432",
187    }
188    # base test section
189    base_test = {
190        "description": '" Text with quotes "',
191        "ssh_command": 'ssh -c "arcfour" -p 22 postgres@pg02.nowhere',
192        "conninfo": "host=pg02.nowhere user=postgres port=5433",
193    }
194    # update map values of the two sections
195    if global_conf is not None:
196        base_barman.update(global_conf)
197    if main_conf is not None:
198        base_main.update(main_conf)
199    if test_conf is not None:
200        base_test.update(test_conf)
201
202    # writing the StringIO obj with the barman and main sections
203    config_file = StringIO()
204    config_file.write("\n[barman]\n")
205    for key in base_barman.keys():
206        config_file.write("%s = %s\n" % (key, base_barman[key]))
207
208    config_file.write("[main]\n")
209    for key in base_main.keys():
210        config_file.write("%s = %s\n" % (key, base_main[key]))
211
212    config_file.write("[test]\n")
213    for key in base_test.keys():
214        config_file.write("%s = %s\n" % (key, base_main[key]))
215
216    config_file.seek(0)
217    config = Config(config_file)
218    config.config_file = config_name or "build_config_from_dicts"
219    return config
220
221
222def build_config_dictionary(config_keys=None):
223    """
224    Utility method, generate a dict useful for config comparison
225
226    It has a 'basic' format and every key could be overwritten the
227    config_keys parameter.
228
229    :param dict[str,str|None]|None config_keys: using this dictionary
230        it is possible to override or add new values to the base dictionary.
231    :return dict: a dictionary representing a barman configuration
232    """
233    # Basic dictionary
234    base_config = {
235        "active": True,
236        "archiver": True,
237        "archiver_batch_size": 0,
238        "config": None,
239        "backup_directory": "/some/barman/home/main",
240        "backup_options": BackupOptions("", "", ""),
241        "bandwidth_limit": None,
242        "barman_home": "/some/barman/home",
243        "basebackups_directory": "/some/barman/home/main/base",
244        "barman_lock_directory": "/some/barman/home",
245        "compression": None,
246        "conninfo": "host=pg01.nowhere user=postgres port=5432",
247        "backup_method": "rsync",
248        "check_timeout": 30,
249        "custom_compression_filter": None,
250        "custom_decompression_filter": None,
251        "custom_compression_magic": None,
252        "description": " Text with quotes ",
253        "immediate_checkpoint": False,
254        "incoming_wals_directory": "/some/barman/home/main/incoming",
255        "max_incoming_wals_queue": None,
256        "minimum_redundancy": "0",
257        "name": "main",
258        "network_compression": False,
259        "post_backup_script": None,
260        "pre_backup_script": None,
261        "post_recovery_script": None,
262        "pre_recovery_script": None,
263        "post_recovery_retry_script": None,
264        "pre_recovery_retry_script": None,
265        "slot_name": None,
266        "streaming_archiver_name": "barman_receive_wal",
267        "streaming_archiver_batch_size": 0,
268        "post_backup_retry_script": None,
269        "streaming_backup_name": "barman_streaming_backup",
270        "pre_backup_retry_script": None,
271        "recovery_options": set(),
272        "retention_policy": None,
273        "retention_policy_mode": "auto",
274        "reuse_backup": None,
275        "ssh_command": 'ssh -c "arcfour" -p 22 postgres@pg01.nowhere',
276        "primary_ssh_command": None,
277        "tablespace_bandwidth_limit": None,
278        "wal_retention_policy": "main",
279        "wals_directory": "/some/barman/home/main/wals",
280        "basebackup_retry_sleep": 30,
281        "basebackup_retry_times": 0,
282        "post_archive_script": None,
283        "streaming_conninfo": "host=pg01.nowhere user=postgres port=5432",
284        "pre_archive_script": None,
285        "post_archive_retry_script": None,
286        "pre_archive_retry_script": None,
287        "post_delete_script": None,
288        "pre_delete_script": None,
289        "post_delete_retry_script": None,
290        "pre_delete_retry_script": None,
291        "post_wal_delete_script": None,
292        "pre_wal_delete_script": None,
293        "post_wal_delete_retry_script": None,
294        "pre_wal_delete_retry_script": None,
295        "last_backup_maximum_age": None,
296        "last_backup_minimum_size": None,
297        "last_wal_maximum_age": None,
298        "disabled": False,
299        "msg_list": [],
300        "path_prefix": None,
301        "streaming_archiver": False,
302        "streaming_wals_directory": "/some/barman/home/main/streaming",
303        "errors_directory": "/some/barman/home/main/errors",
304        "parallel_jobs": 1,
305        "create_slot": "manual",
306        "forward_config_path": False,
307    }
308    # Check for overriding keys
309    if config_keys is not None:
310        base_config.update(config_keys)
311    return base_config
312
313
314def build_real_server(global_conf=None, main_conf=None):
315    """
316    Build a real Server object built from a real configuration
317
318    :param dict[str,str|None]|None global_conf: using this dictionary
319        it is possible to override or add new values to the [barman] section
320    :param dict[str,str|None]|None main_conf: using this dictionary
321        it is possible to override/add new values to the [main] section
322    :return barman.server.Server: a barman Server object
323    """
324    return Server(
325        build_config_from_dicts(
326            global_conf=global_conf, main_conf=main_conf
327        ).get_server("main")
328    )
329
330
331def build_mocked_server(name=None, config=None, global_conf=None, main_conf=None):
332    """
333    Build a mock server object
334    :param str name: server name, defaults to 'main'
335    :param barman.config.ServerConfig config: use this object to build the
336        server
337    :param dict[str,str|None]|None global_conf: using this dictionary
338        it is possible to override or add new values to the [barman] section
339    :param dict[str,str|None]|None main_conf: using this dictionary
340        it is possible to override/add new values to the [main] section
341    :rtype: barman.server.Server
342    """
343    # instantiate a retention policy object using mocked parameters
344    server = mock.MagicMock(name="barman.server.Server")
345
346    if not config:
347        server.config = build_config_from_dicts(
348            global_conf=global_conf, main_conf=main_conf
349        ).get_server("main")
350    else:
351        server.config = config
352    server.backup_manager.server = server
353    server.backup_manager.config = server.config
354    server.passive_node = False
355    server.config.name = name or "main"
356    server.postgres.xlog_segment_size = DEFAULT_XLOG_SEG_SIZE
357    server.path = "/test/bin"
358    server.systemid = "6721602258895701769"
359    return server
360
361
362def build_backup_manager(
363    server=None, name=None, config=None, global_conf=None, main_conf=None
364):
365    """
366    Instantiate a BackupManager object using mocked parameters
367
368    The compression_manager member is mocked
369
370    :param barman.server.Server|None server: Optionsl Server object
371    :rtype: barman.backup.BackupManager
372    """
373    if server is None:
374        server = build_mocked_server(name, config, global_conf, main_conf)
375    with mock.patch("barman.backup.CompressionManager"):
376        manager = BackupManager(server=server)
377    manager.compression_manager.unidentified_compression = None
378    manager.compression_manager.get_wal_file_info.side_effect = (
379        lambda filename: WalFileInfo.from_file(filename, manager.compression_manager)
380    )
381    server.backup_manager = manager
382    return manager
383
384
385def caplog_reset(caplog):
386    """
387    Workaround for the fact that caplog doesn't provide a reset method yet
388    """
389    del caplog.handler.records[:]
390    caplog.handler.stream.truncate(0)
391    caplog.handler.stream.seek(0)
392
393
394def build_backup_directories(backup_info):
395    """
396    Create on disk directory structure for a given BackupInfo
397
398    :param LocalBackupInfo backup_info:
399    """
400    rmtree(backup_info.get_basebackup_directory(), ignore_errors=True)
401    mkpath(backup_info.get_data_directory())
402    for tbs in backup_info.tablespaces:
403        mkpath(backup_info.get_data_directory(tbs.oid))
404
405
406def parse_recovery_conf(recovery_conf_file):
407    """
408    Parse a recovery conf file
409    :param file recovery_conf_file: stream reading the recovery conf file
410    :return Dict[str,str]: parsed configuration file
411    """
412    recovery_conf = {}
413
414    for line in recovery_conf_file.readlines():
415        key, value = (s.strip() for s in line.strip().split("=", 1))
416        recovery_conf[key] = value
417
418    return recovery_conf
419
420
421def find_by_attr(iterable, attr, value):
422    """
423    Utility method to find a list member by filtering on attribute content
424
425    :param iterable iterable: An iterable to be inspected
426    :param str attr: The attribute name
427    :param value: The content to match
428    :return:
429    """
430    for element in iterable:
431        if element[attr] == value:
432            return element
433
434
435# The following two functions are useful to create bytes/unicode strings
436# in Python 2 and in Python 3 with the same syntax.
437if sys.version_info[0] >= 3:
438
439    def b(s):
440        """
441        Create a byte string
442        """
443        return s.encode("utf-8")
444
445    def u(s):
446        """
447        Create an unicode string
448        """
449        return s
450
451
452else:
453
454    def b(s):
455        """
456        Create a byte string
457        :param s:
458        :return:
459        """
460        return s
461
462    def u(s):
463        """
464        Create an unicode string
465        :param s:
466        :return:
467        """
468        return unicode(s.replace(r"\\", r"\\\\"), "unicode_escape")  # noqa
469
470
471def interpolate_wals(begin_wal, end_wal):
472    """Helper which generates all WAL names between two WALs (inclusive)"""
473    return ["%024X" % wal for wal in (range(int(begin_wal, 16), int(end_wal, 16) + 1))]
474