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