]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/configuration.py
update ceph source to reef 18.2.0
[ceph.git] / ceph / src / ceph-volume / ceph_volume / configuration.py
1 import contextlib
2 import logging
3 import os
4 import re
5 from ceph_volume import terminal, conf
6 from ceph_volume import exceptions
7 from sys import version_info as sys_version_info
8
9 if sys_version_info.major >= 3:
10 import configparser
11 conf_parentclass = configparser.ConfigParser
12 elif sys_version_info.major < 3:
13 import ConfigParser as configparser
14 conf_parentclass = configparser.SafeConfigParser
15 else:
16 raise RuntimeError('Not expecting python version > 3 yet.')
17
18
19 logger = logging.getLogger(__name__)
20
21
22 class _TrimIndentFile(object):
23 """
24 This is used to take a file-like object and removes any
25 leading tabs from each line when it's read. This is important
26 because some ceph configuration files include tabs which break
27 ConfigParser.
28 """
29 def __init__(self, fp):
30 self.fp = fp
31
32 def readline(self):
33 line = self.fp.readline()
34 return line.lstrip(' \t')
35
36 def __iter__(self):
37 return iter(self.readline, '')
38
39
40 def load_ceph_conf_path(cluster_name='ceph'):
41 abspath = '/etc/ceph/%s.conf' % cluster_name
42 conf.path = os.getenv('CEPH_CONF', abspath)
43 conf.cluster = cluster_name
44
45
46 def load(abspath=None):
47 if abspath is None:
48 abspath = conf.path
49
50 if not os.path.exists(abspath):
51 raise exceptions.ConfigurationError(abspath=abspath)
52
53 parser = Conf()
54
55 try:
56 ceph_file = open(abspath)
57 trimmed_conf = _TrimIndentFile(ceph_file)
58 with contextlib.closing(ceph_file):
59 parser.read_conf(trimmed_conf)
60 conf.ceph = parser
61 return parser
62 except configparser.ParsingError as error:
63 logger.exception('Unable to parse INI-style file: %s' % abspath)
64 terminal.error(str(error))
65 raise RuntimeError('Unable to read configuration file: %s' % abspath)
66
67
68 class Conf(conf_parentclass):
69 """
70 Subclasses from ConfigParser to give a few helpers for Ceph
71 configuration.
72 """
73
74 def read_path(self, path):
75 self.path = path
76 return self.read(path)
77
78 def is_valid(self):
79 try:
80 self.get('global', 'fsid')
81 except (configparser.NoSectionError, configparser.NoOptionError):
82 raise exceptions.ConfigurationKeyError('global', 'fsid')
83
84 def optionxform(self, s):
85 s = s.replace('_', ' ')
86 s = '_'.join(s.split())
87 return s
88
89 def get_safe(self, section, key, default=None, check_valid=True):
90 """
91 Attempt to get a configuration value from a certain section
92 in a ``cfg`` object but returning None if not found. Avoids the need
93 to be doing try/except {ConfigParser Exceptions} every time.
94 """
95 if check_valid:
96 self.is_valid()
97 try:
98 return self.get(section, key)
99 except (configparser.NoSectionError, configparser.NoOptionError):
100 return default
101
102 def get_list(self, section, key, default=None, split=','):
103 """
104 Assumes that the value for a given key is going to be a list separated
105 by commas. It gets rid of trailing comments. If just one item is
106 present it returns a list with a single item, if no key is found an
107 empty list is returned.
108
109 Optionally split on other characters besides ',' and return a fallback
110 value if no items are found.
111 """
112 self.is_valid()
113 value = self.get_safe(section, key, [])
114 if value == []:
115 if default is not None:
116 return default
117 return value
118
119 # strip comments
120 value = re.split(r'\s+#', value)[0]
121
122 # split on commas
123 value = value.split(split)
124
125 # strip spaces
126 return [x.strip() for x in value]
127
128 # XXX Almost all of it lifted from the original ConfigParser._read method,
129 # except for the parsing of '#' in lines. This is only a problem in Python 2.7, and can be removed
130 # once tooling is Python3 only with `Conf(inline_comment_prefixes=('#',';'))`
131 def _read(self, fp, fpname):
132 """Parse a sectioned setup file.
133
134 The sections in setup file contains a title line at the top,
135 indicated by a name in square brackets (`[]'), plus key/value
136 options lines, indicated by `name: value' format lines.
137 Continuations are represented by an embedded newline then
138 leading whitespace. Blank lines, lines beginning with a '#',
139 and just about everything else are ignored.
140 """
141 cursect = None # None, or a dictionary
142 optname = None
143 lineno = 0
144 e = None # None, or an exception
145 while True:
146 line = fp.readline()
147 if not line:
148 break
149 lineno = lineno + 1
150 # comment or blank line?
151 if line.strip() == '' or line[0] in '#;':
152 continue
153 if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
154 # no leading whitespace
155 continue
156 # continuation line?
157 if line[0].isspace() and cursect is not None and optname:
158 value = line.strip()
159 if value:
160 cursect[optname].append(value)
161 # a section header or option header?
162 else:
163 # is it a section header?
164 mo = self.SECTCRE.match(line)
165 if mo:
166 sectname = mo.group('header')
167 if sectname in self._sections:
168 cursect = self._sections[sectname]
169 elif sectname == 'DEFAULT':
170 cursect = self._defaults
171 else:
172 cursect = self._dict()
173 cursect['__name__'] = sectname
174 self._sections[sectname] = cursect
175 # So sections can't start with a continuation line
176 optname = None
177 # no section header in the file?
178 elif cursect is None:
179 raise configparser.MissingSectionHeaderError(fpname, lineno, line)
180 # an option line?
181 else:
182 mo = self._optcre.match(line)
183 if mo:
184 optname, vi, optval = mo.group('option', 'vi', 'value')
185 optname = self.optionxform(optname.rstrip())
186 # This check is fine because the OPTCRE cannot
187 # match if it would set optval to None
188 if optval is not None:
189 # XXX Added support for '#' inline comments
190 if vi in ('=', ':') and (';' in optval or '#' in optval):
191 # strip comments
192 optval = re.split(r'\s+(;|#)', optval)[0]
193 # if what is left is comment as a value, fallback to an empty string
194 # that is: `foo = ;` would mean `foo` is '', which brings parity with
195 # what ceph-conf tool does
196 if optval in [';','#']:
197 optval = ''
198 optval = optval.strip()
199 # allow empty values
200 if optval == '""':
201 optval = ''
202 cursect[optname] = [optval]
203 else:
204 # valueless option handling
205 cursect[optname] = optval
206 else:
207 # a non-fatal parsing error occurred. set up the
208 # exception but keep going. the exception will be
209 # raised at the end of the file and will contain a
210 # list of all bogus lines
211 if not e:
212 e = configparser.ParsingError(fpname)
213 e.append(lineno, repr(line))
214 # if any parsing errors occurred, raise an exception
215 if e:
216 raise e
217
218 # join the multi-line values collected while reading
219 all_sections = [self._defaults]
220 all_sections.extend(self._sections.values())
221 for options in all_sections:
222 for name, val in options.items():
223 if isinstance(val, list):
224 options[name] = '\n'.join(val)
225
226 def read_conf(self, conffile):
227 if sys_version_info.major >= 3:
228 self.read_file(conffile)
229 elif sys_version_info.major < 3:
230 self.readfp(conffile)
231 else:
232 raise RuntimeError('Not expecting python version > 3 yet.')