summaryrefslogtreecommitdiffstats
path: root/src/ceph-volume/ceph_volume/configuration.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/ceph-volume/ceph_volume/configuration.py232
1 files changed, 232 insertions, 0 deletions
diff --git a/src/ceph-volume/ceph_volume/configuration.py b/src/ceph-volume/ceph_volume/configuration.py
new file mode 100644
index 000000000..e0f7ef1f0
--- /dev/null
+++ b/src/ceph-volume/ceph_volume/configuration.py
@@ -0,0 +1,232 @@
+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.')