import contextlib import logging import os import re from ceph_volume import terminal, conf from ceph_volume import exceptions from sys import version_info as sys_version_info if sys_version_info.major >= 3: import configparser conf_parentclass = configparser.ConfigParser elif sys_version_info.major < 3: import ConfigParser as configparser conf_parentclass = configparser.SafeConfigParser else: raise RuntimeError('Not expecting python version > 3 yet.') logger = logging.getLogger(__name__) class _TrimIndentFile(object): """ This is used to take a file-like object and removes any leading tabs from each line when it's read. This is important because some ceph configuration files include tabs which break ConfigParser. """ def __init__(self, fp): self.fp = fp def readline(self): line = self.fp.readline() return line.lstrip(' \t') def __iter__(self): return iter(self.readline, '') def load_ceph_conf_path(cluster_name='ceph'): abspath = '/etc/ceph/%s.conf' % cluster_name conf.path = os.getenv('CEPH_CONF', abspath) conf.cluster = cluster_name def load(abspath=None): if abspath is None: abspath = conf.path if not os.path.exists(abspath): raise exceptions.ConfigurationError(abspath=abspath) parser = Conf() try: ceph_file = open(abspath) trimmed_conf = _TrimIndentFile(ceph_file) with contextlib.closing(ceph_file): parser.read_conf(trimmed_conf) conf.ceph = parser return parser except configparser.ParsingError as error: logger.exception('Unable to parse INI-style file: %s' % abspath) terminal.error(str(error)) raise RuntimeError('Unable to read configuration file: %s' % abspath) class Conf(conf_parentclass): """ Subclasses from ConfigParser to give a few helpers for Ceph configuration. """ def read_path(self, path): self.path = path return self.read(path) def is_valid(self): try: self.get('global', 'fsid') except (configparser.NoSectionError, configparser.NoOptionError): raise exceptions.ConfigurationKeyError('global', 'fsid') def optionxform(self, s): s = s.replace('_', ' ') s = '_'.join(s.split()) return s def get_safe(self, section, key, default=None, check_valid=True): """ Attempt to get a configuration value from a certain section in a ``cfg`` object but returning None if not found. Avoids the need to be doing try/except {ConfigParser Exceptions} every time. """ if check_valid: self.is_valid() try: return self.get(section, key) except (configparser.NoSectionError, configparser.NoOptionError): return default def get_list(self, section, key, default=None, split=','): """ Assumes that the value for a given key is going to be a list separated by commas. It gets rid of trailing comments. If just one item is present it returns a list with a single item, if no key is found an empty list is returned. Optionally split on other characters besides ',' and return a fallback value if no items are found. """ self.is_valid() value = self.get_safe(section, key, []) if value == []: if default is not None: return default return value # strip comments value = re.split(r'\s+#', value)[0] # split on commas value = value.split(split) # strip spaces return [x.strip() for x in value] # XXX Almost all of it lifted from the original ConfigParser._read method, # except for the parsing of '#' in lines. This is only a problem in Python 2.7, and can be removed # once tooling is Python3 only with `Conf(inline_comment_prefixes=('#',';'))` def _read(self, fp, fpname): """Parse a sectioned setup file. The sections in setup file contains a title line at the top, indicated by a name in square brackets (`[]'), plus key/value options lines, indicated by `name: value' format lines. Continuations are represented by an embedded newline then leading whitespace. Blank lines, lines beginning with a '#', and just about everything else are ignored. """ cursect = None # None, or a dictionary optname = None lineno = 0 e = None # None, or an exception while True: line = fp.readline() if not line: break lineno = lineno + 1 # comment or blank line? if line.strip() == '' or line[0] in '#;': continue if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR": # no leading whitespace continue # continuation line? if line[0].isspace() and cursect is not None and optname: value = line.strip() if value: cursect[optname].append(value) # a section header or option header? else: # is it a section header? mo = self.SECTCRE.match(line) if mo: sectname = mo.group('header') if sectname in self._sections: cursect = self._sections[sectname] elif sectname == 'DEFAULT': cursect = self._defaults else: cursect = self._dict() cursect['__name__'] = sectname self._sections[sectname] = cursect # So sections can't start with a continuation line optname = None # no section header in the file? elif cursect is None: raise configparser.MissingSectionHeaderError(fpname, lineno, line) # an option line? else: mo = self._optcre.match(line) if mo: optname, vi, optval = mo.group('option', 'vi', 'value') optname = self.optionxform(optname.rstrip()) # This check is fine because the OPTCRE cannot # match if it would set optval to None if optval is not None: # XXX Added support for '#' inline comments if vi in ('=', ':') and (';' in optval or '#' in optval): # strip comments optval = re.split(r'\s+(;|#)', optval)[0] # if what is left is comment as a value, fallback to an empty string # that is: `foo = ;` would mean `foo` is '', which brings parity with # what ceph-conf tool does if optval in [';','#']: optval = '' optval = optval.strip() # allow empty values if optval == '""': optval = '' cursect[optname] = [optval] else: # valueless option handling cursect[optname] = optval else: # a non-fatal parsing error occurred. set up the # exception but keep going. the exception will be # raised at the end of the file and will contain a # list of all bogus lines if not e: e = configparser.ParsingError(fpname) e.append(lineno, repr(line)) # if any parsing errors occurred, raise an exception if e: raise e # join the multi-line values collected while reading all_sections = [self._defaults] all_sections.extend(self._sections.values()) for options in all_sections: for name, val in options.items(): if isinstance(val, list): options[name] = '\n'.join(val) def read_conf(self, conffile): if sys_version_info.major >= 3: self.read_file(conffile) elif sys_version_info.major < 3: self.readfp(conffile) else: raise RuntimeError('Not expecting python version > 3 yet.')