1# -*- coding: utf-8 -*- 2# © Copyright EnterpriseDB UK Limited 2011-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 19""" 20This module is responsible for all the things related to 21Barman configuration, such as parsing configuration file. 22""" 23 24import collections 25import datetime 26import inspect 27import logging.handlers 28import os 29import re 30import sys 31from glob import iglob 32 33from barman import output 34 35try: 36 from ConfigParser import ConfigParser, NoOptionError 37except ImportError: 38 from configparser import ConfigParser, NoOptionError 39 40 41# create a namedtuple object called PathConflict with 'label' and 'server' 42PathConflict = collections.namedtuple("PathConflict", "label server") 43 44_logger = logging.getLogger(__name__) 45 46FORBIDDEN_SERVER_NAMES = ["all"] 47 48DEFAULT_USER = "barman" 49DEFAULT_LOG_LEVEL = logging.INFO 50DEFAULT_LOG_FORMAT = "%(asctime)s [%(process)s] %(name)s %(levelname)s: %(message)s" 51 52_TRUE_RE = re.compile(r"""^(true|t|yes|1|on)$""", re.IGNORECASE) 53_FALSE_RE = re.compile(r"""^(false|f|no|0|off)$""", re.IGNORECASE) 54_TIME_INTERVAL_RE = re.compile( 55 r""" 56 ^\s* 57 # N (day|month|week|hour) with optional 's' 58 (\d+)\s+(day|month|week|hour)s? 59 \s*$ 60 """, 61 re.IGNORECASE | re.VERBOSE, 62) 63_SLOT_NAME_RE = re.compile("^[0-9a-z_]+$") 64_SI_SUFFIX_RE = re.compile(r"""(\d+)\s*(k|Ki|M|Mi|G|Gi|T|Ti)?\s*$""") 65 66REUSE_BACKUP_VALUES = ("copy", "link", "off") 67 68# Possible copy methods for backups (must be all lowercase) 69BACKUP_METHOD_VALUES = ["rsync", "postgres", "local-rsync"] 70 71CREATE_SLOT_VALUES = ["manual", "auto"] 72 73 74class CsvOption(set): 75 76 """ 77 Base class for CSV options. 78 79 Given a comma delimited string, this class is a list containing the 80 submitted options. 81 Internally, it uses a set in order to avoid option replication. 82 Allowed values for the CSV option are contained in the 'value_list' 83 attribute. 84 The 'conflicts' attribute specifies for any value, the list of 85 values that are prohibited (and thus generate a conflict). 86 If a conflict is found, raises a ValueError exception. 87 """ 88 89 value_list = [] 90 conflicts = {} 91 92 def __init__(self, value, key, source): 93 # Invoke parent class init and initialize an empty set 94 super(CsvOption, self).__init__() 95 96 # Parse not None values 97 if value is not None: 98 self.parse(value, key, source) 99 100 # Validates the object structure before returning the new instance 101 self.validate(key, source) 102 103 def parse(self, value, key, source): 104 """ 105 Parses a list of values and correctly assign the set of values 106 (removing duplication) and checking for conflicts. 107 """ 108 if not value: 109 return 110 values_list = value.split(",") 111 for val in sorted(values_list): 112 val = val.strip().lower() 113 if val in self.value_list: 114 # check for conflicting values. if a conflict is 115 # found the option is not valid then, raise exception. 116 if val in self.conflicts and self.conflicts[val] in self: 117 raise ValueError( 118 "Invalid configuration value '%s' for " 119 "key %s in %s: cannot contain both " 120 "'%s' and '%s'." 121 "Configuration directive ignored." 122 % (val, key, source, val, self.conflicts[val]) 123 ) 124 else: 125 # otherwise use parsed value 126 self.add(val) 127 else: 128 # not allowed value, reject the configuration 129 raise ValueError( 130 "Invalid configuration value '%s' for " 131 "key %s in %s: Unknown option" % (val, key, source) 132 ) 133 134 def validate(self, key, source): 135 """ 136 Override this method for special validation needs 137 """ 138 139 def to_json(self): 140 """ 141 Output representation of the obj for JSON serialization 142 143 The result is a string which can be parsed by the same class 144 """ 145 return ",".join(self) 146 147 148class BackupOptions(CsvOption): 149 """ 150 Extends CsvOption class providing all the details for the backup_options 151 field 152 """ 153 154 # constants containing labels for allowed values 155 EXCLUSIVE_BACKUP = "exclusive_backup" 156 CONCURRENT_BACKUP = "concurrent_backup" 157 EXTERNAL_CONFIGURATION = "external_configuration" 158 159 # list holding all the allowed values for the BackupOption class 160 value_list = [EXCLUSIVE_BACKUP, CONCURRENT_BACKUP, EXTERNAL_CONFIGURATION] 161 # map holding all the possible conflicts between the allowed values 162 conflicts = { 163 EXCLUSIVE_BACKUP: CONCURRENT_BACKUP, 164 CONCURRENT_BACKUP: EXCLUSIVE_BACKUP, 165 } 166 167 168class RecoveryOptions(CsvOption): 169 """ 170 Extends CsvOption class providing all the details for the recovery_options 171 field 172 """ 173 174 # constants containing labels for allowed values 175 GET_WAL = "get-wal" 176 177 # list holding all the allowed values for the RecoveryOptions class 178 value_list = [GET_WAL] 179 180 181def parse_boolean(value): 182 """ 183 Parse a string to a boolean value 184 185 :param str value: string representing a boolean 186 :raises ValueError: if the string is an invalid boolean representation 187 """ 188 if _TRUE_RE.match(value): 189 return True 190 if _FALSE_RE.match(value): 191 return False 192 raise ValueError("Invalid boolean representation (use 'true' or 'false')") 193 194 195def parse_time_interval(value): 196 """ 197 Parse a string, transforming it in a time interval. 198 Accepted format: N (day|month|week)s 199 200 :param str value: the string to evaluate 201 """ 202 # if empty string or none return none 203 if value is None or value == "": 204 return None 205 result = _TIME_INTERVAL_RE.match(value) 206 # if the string doesn't match, the option is invalid 207 if not result: 208 raise ValueError("Invalid value for a time interval %s" % value) 209 # if the int conversion 210 value = int(result.groups()[0]) 211 unit = result.groups()[1][0].lower() 212 213 # Calculates the time delta 214 if unit == "d": 215 time_delta = datetime.timedelta(days=value) 216 elif unit == "w": 217 time_delta = datetime.timedelta(weeks=value) 218 elif unit == "m": 219 time_delta = datetime.timedelta(days=(31 * value)) 220 elif unit == "h": 221 time_delta = datetime.timedelta(hours=value) 222 else: 223 # This should never happen 224 raise ValueError("Invalid unit time %s" % unit) 225 226 return time_delta 227 228 229def parse_si_suffix(value): 230 """ 231 Parse a string, transforming it into integer and multiplying by 232 the SI or IEC suffix 233 eg a suffix of Ki multiplies the integer value by 1024 234 and returns the new value 235 236 Accepted format: N (k|Ki|M|Mi|G|Gi|T|Ti) 237 238 :param str value: the string to evaluate 239 """ 240 # if empty string or none return none 241 if value is None or value == "": 242 return None 243 result = _SI_SUFFIX_RE.match(value) 244 if not result: 245 raise ValueError("Invalid value for a number %s" % value) 246 # if the int conversion 247 value = int(result.groups()[0]) 248 unit = result.groups()[1] 249 250 # Calculates the value 251 if unit == "k": 252 value *= 1000 253 elif unit == "Ki": 254 value *= 1024 255 elif unit == "M": 256 value *= 1000000 257 elif unit == "Mi": 258 value *= 1048576 259 elif unit == "G": 260 value *= 1000000000 261 elif unit == "Gi": 262 value *= 1073741824 263 elif unit == "T": 264 value *= 1000000000000 265 elif unit == "Ti": 266 value *= 1099511627776 267 268 return value 269 270 271def parse_reuse_backup(value): 272 """ 273 Parse a string to a valid reuse_backup value. 274 275 Valid values are "copy", "link" and "off" 276 277 :param str value: reuse_backup value 278 :raises ValueError: if the value is invalid 279 """ 280 if value is None: 281 return None 282 if value.lower() in REUSE_BACKUP_VALUES: 283 return value.lower() 284 raise ValueError( 285 "Invalid value (use '%s' or '%s')" 286 % ("', '".join(REUSE_BACKUP_VALUES[:-1]), REUSE_BACKUP_VALUES[-1]) 287 ) 288 289 290def parse_backup_method(value): 291 """ 292 Parse a string to a valid backup_method value. 293 294 Valid values are contained in BACKUP_METHOD_VALUES list 295 296 :param str value: backup_method value 297 :raises ValueError: if the value is invalid 298 """ 299 if value is None: 300 return None 301 if value.lower() in BACKUP_METHOD_VALUES: 302 return value.lower() 303 raise ValueError( 304 "Invalid value (must be one in: '%s')" % ("', '".join(BACKUP_METHOD_VALUES)) 305 ) 306 307 308def parse_slot_name(value): 309 """ 310 Replication slot names may only contain lower case letters, numbers, 311 and the underscore character. This function parse a replication slot name 312 313 :param str value: slot_name value 314 :return: 315 """ 316 if value is None: 317 return None 318 319 value = value.lower() 320 if not _SLOT_NAME_RE.match(value): 321 raise ValueError( 322 "Replication slot names may only contain lower case letters, " 323 "numbers, and the underscore character." 324 ) 325 return value 326 327 328def parse_create_slot(value): 329 """ 330 Parse a string to a valid create_slot value. 331 332 Valid values are "manual" and "auto" 333 334 :param str value: create_slot value 335 :raises ValueError: if the value is invalid 336 """ 337 if value is None: 338 return None 339 value = value.lower() 340 if value in CREATE_SLOT_VALUES: 341 return value 342 raise ValueError( 343 "Invalid value (use '%s' or '%s')" 344 % ("', '".join(CREATE_SLOT_VALUES[:-1]), CREATE_SLOT_VALUES[-1]) 345 ) 346 347 348class ServerConfig(object): 349 """ 350 This class represents the configuration for a specific Server instance. 351 """ 352 353 KEYS = [ 354 "active", 355 "archiver", 356 "archiver_batch_size", 357 "backup_directory", 358 "backup_method", 359 "backup_options", 360 "bandwidth_limit", 361 "basebackup_retry_sleep", 362 "basebackup_retry_times", 363 "basebackups_directory", 364 "check_timeout", 365 "compression", 366 "conninfo", 367 "custom_compression_filter", 368 "custom_decompression_filter", 369 "custom_compression_magic", 370 "description", 371 "disabled", 372 "errors_directory", 373 "forward_config_path", 374 "immediate_checkpoint", 375 "incoming_wals_directory", 376 "last_backup_maximum_age", 377 "last_backup_minimum_size", 378 "last_wal_maximum_age", 379 "max_incoming_wals_queue", 380 "minimum_redundancy", 381 "network_compression", 382 "parallel_jobs", 383 "path_prefix", 384 "post_archive_retry_script", 385 "post_archive_script", 386 "post_backup_retry_script", 387 "post_backup_script", 388 "post_delete_script", 389 "post_delete_retry_script", 390 "post_recovery_retry_script", 391 "post_recovery_script", 392 "post_wal_delete_script", 393 "post_wal_delete_retry_script", 394 "pre_archive_retry_script", 395 "pre_archive_script", 396 "pre_backup_retry_script", 397 "pre_backup_script", 398 "pre_delete_script", 399 "pre_delete_retry_script", 400 "pre_recovery_retry_script", 401 "pre_recovery_script", 402 "pre_wal_delete_script", 403 "pre_wal_delete_retry_script", 404 "primary_ssh_command", 405 "recovery_options", 406 "create_slot", 407 "retention_policy", 408 "retention_policy_mode", 409 "reuse_backup", 410 "slot_name", 411 "ssh_command", 412 "streaming_archiver", 413 "streaming_archiver_batch_size", 414 "streaming_archiver_name", 415 "streaming_backup_name", 416 "streaming_conninfo", 417 "streaming_wals_directory", 418 "tablespace_bandwidth_limit", 419 "wal_retention_policy", 420 "wals_directory", 421 ] 422 423 BARMAN_KEYS = [ 424 "archiver", 425 "archiver_batch_size", 426 "backup_method", 427 "backup_options", 428 "bandwidth_limit", 429 "basebackup_retry_sleep", 430 "basebackup_retry_times", 431 "check_timeout", 432 "compression", 433 "configuration_files_directory", 434 "custom_compression_filter", 435 "custom_decompression_filter", 436 "custom_compression_magic", 437 "forward_config_path", 438 "immediate_checkpoint", 439 "last_backup_maximum_age", 440 "last_backup_minimum_size", 441 "last_wal_maximum_age", 442 "max_incoming_wals_queue", 443 "minimum_redundancy", 444 "network_compression", 445 "parallel_jobs", 446 "path_prefix", 447 "post_archive_retry_script", 448 "post_archive_script", 449 "post_backup_retry_script", 450 "post_backup_script", 451 "post_delete_script", 452 "post_delete_retry_script", 453 "post_recovery_retry_script", 454 "post_recovery_script", 455 "post_wal_delete_script", 456 "post_wal_delete_retry_script", 457 "pre_archive_retry_script", 458 "pre_archive_script", 459 "pre_backup_retry_script", 460 "pre_backup_script", 461 "pre_delete_script", 462 "pre_delete_retry_script", 463 "pre_recovery_retry_script", 464 "pre_recovery_script", 465 "pre_wal_delete_script", 466 "pre_wal_delete_retry_script", 467 "primary_ssh_command", 468 "recovery_options", 469 "create_slot", 470 "retention_policy", 471 "retention_policy_mode", 472 "reuse_backup", 473 "slot_name", 474 "streaming_archiver", 475 "streaming_archiver_batch_size", 476 "streaming_archiver_name", 477 "streaming_backup_name", 478 "tablespace_bandwidth_limit", 479 "wal_retention_policy", 480 ] 481 482 DEFAULTS = { 483 "active": "true", 484 "archiver": "off", 485 "archiver_batch_size": "0", 486 "backup_directory": "%(barman_home)s/%(name)s", 487 "backup_method": "rsync", 488 "backup_options": "", 489 "basebackup_retry_sleep": "30", 490 "basebackup_retry_times": "0", 491 "basebackups_directory": "%(backup_directory)s/base", 492 "check_timeout": "30", 493 "disabled": "false", 494 "errors_directory": "%(backup_directory)s/errors", 495 "forward_config_path": "false", 496 "immediate_checkpoint": "false", 497 "incoming_wals_directory": "%(backup_directory)s/incoming", 498 "minimum_redundancy": "0", 499 "network_compression": "false", 500 "parallel_jobs": "1", 501 "recovery_options": "", 502 "create_slot": "manual", 503 "retention_policy_mode": "auto", 504 "streaming_archiver": "off", 505 "streaming_archiver_batch_size": "0", 506 "streaming_archiver_name": "barman_receive_wal", 507 "streaming_backup_name": "barman_streaming_backup", 508 "streaming_conninfo": "%(conninfo)s", 509 "streaming_wals_directory": "%(backup_directory)s/streaming", 510 "wal_retention_policy": "main", 511 "wals_directory": "%(backup_directory)s/wals", 512 } 513 514 FIXED = [ 515 "disabled", 516 ] 517 518 PARSERS = { 519 "active": parse_boolean, 520 "archiver": parse_boolean, 521 "archiver_batch_size": int, 522 "backup_method": parse_backup_method, 523 "backup_options": BackupOptions, 524 "basebackup_retry_sleep": int, 525 "basebackup_retry_times": int, 526 "check_timeout": int, 527 "disabled": parse_boolean, 528 "forward_config_path": parse_boolean, 529 "immediate_checkpoint": parse_boolean, 530 "last_backup_maximum_age": parse_time_interval, 531 "last_backup_minimum_size": parse_si_suffix, 532 "last_wal_maximum_age": parse_time_interval, 533 "max_incoming_wals_queue": int, 534 "network_compression": parse_boolean, 535 "parallel_jobs": int, 536 "recovery_options": RecoveryOptions, 537 "create_slot": parse_create_slot, 538 "reuse_backup": parse_reuse_backup, 539 "streaming_archiver": parse_boolean, 540 "streaming_archiver_batch_size": int, 541 "slot_name": parse_slot_name, 542 } 543 544 def invoke_parser(self, key, source, value, new_value): 545 """ 546 Function used for parsing configuration values. 547 If needed, it uses special parsers from the PARSERS map, 548 and handles parsing exceptions. 549 550 Uses two values (value and new_value) to manage 551 configuration hierarchy (server config overwrites global config). 552 553 :param str key: the name of the configuration option 554 :param str source: the section that contains the configuration option 555 :param value: the old value of the option if present. 556 :param str new_value: the new value that needs to be parsed 557 :return: the parsed value of a configuration option 558 """ 559 # If the new value is None, returns the old value 560 if new_value is None: 561 return value 562 # If we have a parser for the current key, use it to obtain the 563 # actual value. If an exception is thrown, print a warning and 564 # ignore the value. 565 # noinspection PyBroadException 566 if key in self.PARSERS: 567 parser = self.PARSERS[key] 568 try: 569 # If the parser is a subclass of the CsvOption class 570 # we need a different invocation, which passes not only 571 # the value to the parser, but also the key name 572 # and the section that contains the configuration 573 if inspect.isclass(parser) and issubclass(parser, CsvOption): 574 value = parser(new_value, key, source) 575 else: 576 value = parser(new_value) 577 except Exception as e: 578 output.warning( 579 "Ignoring invalid configuration value '%s' for key %s in %s: %s", 580 new_value, 581 key, 582 source, 583 e, 584 ) 585 else: 586 value = new_value 587 return value 588 589 def __init__(self, config, name): 590 self.msg_list = [] 591 self.config = config 592 self.name = name 593 self.barman_home = config.barman_home 594 self.barman_lock_directory = config.barman_lock_directory 595 config.validate_server_config(self.name) 596 for key in ServerConfig.KEYS: 597 value = None 598 # Skip parameters that cannot be configured by users 599 if key not in ServerConfig.FIXED: 600 # Get the setting from the [name] section of config file 601 # A literal None value is converted to an empty string 602 new_value = config.get(name, key, self.__dict__, none_value="") 603 source = "[%s] section" % name 604 value = self.invoke_parser(key, source, value, new_value) 605 # If the setting isn't present in [name] section of config file 606 # check if it has to be inherited from the [barman] section 607 if value is None and key in ServerConfig.BARMAN_KEYS: 608 new_value = config.get("barman", key, self.__dict__, none_value="") 609 source = "[barman] section" 610 value = self.invoke_parser(key, source, value, new_value) 611 # If the setting isn't present in [name] section of config file 612 # and is not inherited from global section use its default 613 # (if present) 614 if value is None and key in ServerConfig.DEFAULTS: 615 new_value = ServerConfig.DEFAULTS[key] % self.__dict__ 616 source = "DEFAULTS" 617 value = self.invoke_parser(key, source, value, new_value) 618 # An empty string is a None value (bypassing inheritance 619 # from global configuration) 620 if value is not None and value == "" or value == "None": 621 value = None 622 setattr(self, key, value) 623 624 def to_json(self): 625 """ 626 Return an equivalent dictionary that can be encoded in json 627 """ 628 json_dict = dict(vars(self)) 629 # remove the reference to main Config object 630 del json_dict["config"] 631 return json_dict 632 633 def get_bwlimit(self, tablespace=None): 634 """ 635 Return the configured bandwidth limit for the provided tablespace 636 637 If tablespace is None, it returns the global bandwidth limit 638 639 :param barman.infofile.Tablespace tablespace: the tablespace to copy 640 :rtype: str 641 """ 642 # Default to global bandwidth limit 643 bwlimit = self.bandwidth_limit 644 645 if tablespace: 646 # A tablespace can be copied using a per-tablespace bwlimit 647 tbl_bw_limit = self.tablespace_bandwidth_limit 648 if tbl_bw_limit and tablespace.name in tbl_bw_limit: 649 bwlimit = tbl_bw_limit[tablespace.name] 650 651 return bwlimit 652 653 654class Config(object): 655 """This class represents the barman configuration. 656 657 Default configuration files are /etc/barman.conf, 658 /etc/barman/barman.conf 659 and ~/.barman.conf for a per-user configuration 660 """ 661 662 CONFIG_FILES = [ 663 "~/.barman.conf", 664 "/usr/local/etc/barman.conf", 665 "/usr/local/etc/barman/barman.conf", 666 ] 667 668 _QUOTE_RE = re.compile(r"""^(["'])(.*)\1$""") 669 670 def __init__(self, filename=None): 671 # In Python 3 ConfigParser has changed to be strict by default. 672 # Barman wants to preserve the Python 2 behavior, so we are 673 # explicitly building it passing strict=False. 674 try: 675 # Python 3.x 676 self._config = ConfigParser(strict=False) 677 except TypeError: 678 # Python 2.x 679 self._config = ConfigParser() 680 if filename: 681 if hasattr(filename, "read"): 682 try: 683 # Python 3.x 684 self._config.read_file(filename) 685 except AttributeError: 686 # Python 2.x 687 self._config.readfp(filename) 688 else: 689 # check for the existence of the user defined file 690 if not os.path.exists(filename): 691 sys.exit("Configuration file '%s' does not exist" % filename) 692 self._config.read(os.path.expanduser(filename)) 693 else: 694 # Check for the presence of configuration files 695 # inside default directories 696 for path in self.CONFIG_FILES: 697 full_path = os.path.expanduser(path) 698 if os.path.exists(full_path) and full_path in self._config.read( 699 full_path 700 ): 701 filename = full_path 702 break 703 else: 704 sys.exit( 705 "Could not find any configuration file at " 706 "default locations.\n" 707 "Check Barman's documentation for more help." 708 ) 709 self.config_file = filename 710 self._servers = None 711 self.servers_msg_list = [] 712 self._parse_global_config() 713 714 def get(self, section, option, defaults=None, none_value=None): 715 """Method to get the value from a given section from 716 Barman configuration 717 """ 718 if not self._config.has_section(section): 719 return None 720 try: 721 value = self._config.get(section, option, raw=False, vars=defaults) 722 if value.lower() == "none": 723 value = none_value 724 if value is not None: 725 value = self._QUOTE_RE.sub(lambda m: m.group(2), value) 726 return value 727 except NoOptionError: 728 return None 729 730 def _parse_global_config(self): 731 """ 732 This method parses the global [barman] section 733 """ 734 self.barman_home = self.get("barman", "barman_home") 735 self.barman_lock_directory = ( 736 self.get("barman", "barman_lock_directory") or self.barman_home 737 ) 738 self.user = self.get("barman", "barman_user") or DEFAULT_USER 739 self.log_file = self.get("barman", "log_file") 740 self.log_format = self.get("barman", "log_format") or DEFAULT_LOG_FORMAT 741 self.log_level = self.get("barman", "log_level") or DEFAULT_LOG_LEVEL 742 # save the raw barman section to be compared later in 743 # _is_global_config_changed() method 744 self._global_config = set(self._config.items("barman")) 745 746 def _is_global_config_changed(self): 747 """Return true if something has changed in global configuration""" 748 return self._global_config != set(self._config.items("barman")) 749 750 def load_configuration_files_directory(self): 751 """ 752 Read the "configuration_files_directory" option and load all the 753 configuration files with the .conf suffix that lie in that folder 754 """ 755 756 config_files_directory = self.get("barman", "configuration_files_directory") 757 758 if not config_files_directory: 759 return 760 761 if not os.path.isdir(os.path.expanduser(config_files_directory)): 762 _logger.warn( 763 'Ignoring the "configuration_files_directory" option as "%s" ' 764 "is not a directory", 765 config_files_directory, 766 ) 767 return 768 769 for cfile in sorted( 770 iglob(os.path.join(os.path.expanduser(config_files_directory), "*.conf")) 771 ): 772 filename = os.path.basename(cfile) 773 if os.path.isfile(cfile): 774 # Load a file 775 _logger.debug("Including configuration file: %s", filename) 776 self._config.read(cfile) 777 if self._is_global_config_changed(): 778 msg = ( 779 "the configuration file %s contains a not empty [" 780 "barman] section" % filename 781 ) 782 _logger.fatal(msg) 783 raise SystemExit("FATAL: %s" % msg) 784 else: 785 # Add an info that a file has been discarded 786 _logger.warn("Discarding configuration file: %s (not a file)", filename) 787 788 def _populate_servers(self): 789 """ 790 Populate server list from configuration file 791 792 Also check for paths errors in configuration. 793 If two or more paths overlap in 794 a single server, that server is disabled. 795 If two or more directory paths overlap between 796 different servers an error is raised. 797 """ 798 799 # Populate servers 800 if self._servers is not None: 801 return 802 self._servers = {} 803 # Cycle all the available configurations sections 804 for section in self._config.sections(): 805 if section == "barman": 806 # skip global settings 807 continue 808 # Exit if the section has a reserved name 809 if section in FORBIDDEN_SERVER_NAMES: 810 msg = ( 811 "the reserved word '%s' is not allowed as server name." 812 "Please rename it." % section 813 ) 814 _logger.fatal(msg) 815 raise SystemExit("FATAL: %s" % msg) 816 # Create a ServerConfig object 817 self._servers[section] = ServerConfig(self, section) 818 819 # Check for conflicting paths in Barman configuration 820 self._check_conflicting_paths() 821 822 def _check_conflicting_paths(self): 823 """ 824 Look for conflicting paths intra-server and inter-server 825 """ 826 827 # All paths in configuration 828 servers_paths = {} 829 # Global errors list 830 self.servers_msg_list = [] 831 832 # Cycle all the available configurations sections 833 for section in sorted(self._config.sections()): 834 if section == "barman": 835 # skip global settings 836 continue 837 838 # Paths map 839 section_conf = self._servers[section] 840 config_paths = { 841 "backup_directory": section_conf.backup_directory, 842 "basebackups_directory": section_conf.basebackups_directory, 843 "errors_directory": section_conf.errors_directory, 844 "incoming_wals_directory": section_conf.incoming_wals_directory, 845 "streaming_wals_directory": section_conf.streaming_wals_directory, 846 "wals_directory": section_conf.wals_directory, 847 } 848 849 # Check for path errors 850 for label, path in sorted(config_paths.items()): 851 # If the path does not conflict with the others, add it to the 852 # paths map 853 real_path = os.path.realpath(path) 854 if real_path not in servers_paths: 855 servers_paths[real_path] = PathConflict(label, section) 856 else: 857 if section == servers_paths[real_path].server: 858 # Internal path error. 859 # Insert the error message into the server.msg_list 860 if real_path == path: 861 self._servers[section].msg_list.append( 862 "Conflicting path: %s=%s conflicts with " 863 "'%s' for server '%s'" 864 % ( 865 label, 866 path, 867 servers_paths[real_path].label, 868 servers_paths[real_path].server, 869 ) 870 ) 871 else: 872 # Symbolic link 873 self._servers[section].msg_list.append( 874 "Conflicting path: %s=%s (symlink to: %s) " 875 "conflicts with '%s' for server '%s'" 876 % ( 877 label, 878 path, 879 real_path, 880 servers_paths[real_path].label, 881 servers_paths[real_path].server, 882 ) 883 ) 884 # Disable the server 885 self._servers[section].disabled = True 886 else: 887 # Global path error. 888 # Insert the error message into the global msg_list 889 if real_path == path: 890 self.servers_msg_list.append( 891 "Conflicting path: " 892 "%s=%s for server '%s' conflicts with " 893 "'%s' for server '%s'" 894 % ( 895 label, 896 path, 897 section, 898 servers_paths[real_path].label, 899 servers_paths[real_path].server, 900 ) 901 ) 902 else: 903 # Symbolic link 904 self.servers_msg_list.append( 905 "Conflicting path: " 906 "%s=%s (symlink to: %s) for server '%s' " 907 "conflicts with '%s' for server '%s'" 908 % ( 909 label, 910 path, 911 real_path, 912 section, 913 servers_paths[real_path].label, 914 servers_paths[real_path].server, 915 ) 916 ) 917 918 def server_names(self): 919 """This method returns a list of server names""" 920 self._populate_servers() 921 return self._servers.keys() 922 923 def servers(self): 924 """This method returns a list of server parameters""" 925 self._populate_servers() 926 return self._servers.values() 927 928 def get_server(self, name): 929 """ 930 Get the configuration of the specified server 931 932 :param str name: the server name 933 """ 934 self._populate_servers() 935 return self._servers.get(name, None) 936 937 def validate_global_config(self): 938 """ 939 Validate global configuration parameters 940 """ 941 # Check for the existence of unexpected parameters in the 942 # global section of the configuration file 943 keys = [ 944 "barman_home", 945 "barman_lock_directory", 946 "barman_user", 947 "log_file", 948 "log_level", 949 "configuration_files_directory", 950 ] 951 keys.extend(ServerConfig.KEYS) 952 self._validate_with_keys(self._global_config, keys, "barman") 953 954 def validate_server_config(self, server): 955 """ 956 Validate configuration parameters for a specified server 957 958 :param str server: the server name 959 """ 960 # Check for the existence of unexpected parameters in the 961 # server section of the configuration file 962 self._validate_with_keys(self._config.items(server), ServerConfig.KEYS, server) 963 964 @staticmethod 965 def _validate_with_keys(config_items, allowed_keys, section): 966 """ 967 Check every config parameter against a list of allowed keys 968 969 :param config_items: list of tuples containing provided parameters 970 along with their values 971 :param allowed_keys: list of allowed keys 972 :param section: source section (for error reporting) 973 """ 974 for parameter in config_items: 975 # if the parameter name is not in the list of allowed values, 976 # then output a warning 977 name = parameter[0] 978 if name not in allowed_keys: 979 output.warning( 980 'Invalid configuration option "%s" in [%s] ' "section.", 981 name, 982 section, 983 ) 984 985 986# easy raw config diagnostic with python -m 987# noinspection PyProtectedMember 988def _main(): 989 print("Active configuration settings:") 990 r = Config() 991 r.load_configuration_files_directory() 992 for section in r._config.sections(): 993 print("Section: %s" % section) 994 for option in r._config.options(section): 995 print("\t%s = %s " % (option, r.get(section, option))) 996 997 998if __name__ == "__main__": 999 _main() 1000