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