]>
Commit | Line | Data |
---|---|---|
b32b8144 | 1 | import contextlib |
d2e6a577 FG |
2 | import logging |
3 | import os | |
4 | import re | |
91327a77 | 5 | from ceph_volume import terminal, conf |
d2e6a577 | 6 | from ceph_volume import exceptions |
eafe8130 TL |
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.') | |
d2e6a577 FG |
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 | ||
91327a77 AA |
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 | ||
d2e6a577 | 46 | def load(abspath=None): |
91327a77 AA |
47 | if abspath is None: |
48 | abspath = conf.path | |
49 | ||
b32b8144 FG |
50 | if not os.path.exists(abspath): |
51 | raise exceptions.ConfigurationError(abspath=abspath) | |
52 | ||
d2e6a577 | 53 | parser = Conf() |
b32b8144 | 54 | |
d2e6a577 | 55 | try: |
b32b8144 FG |
56 | ceph_file = open(abspath) |
57 | trimmed_conf = _TrimIndentFile(ceph_file) | |
58 | with contextlib.closing(ceph_file): | |
eafe8130 | 59 | parser.read_conf(trimmed_conf) |
91327a77 | 60 | conf.ceph = parser |
b32b8144 | 61 | return parser |
d2e6a577 | 62 | except configparser.ParsingError as error: |
d2e6a577 | 63 | logger.exception('Unable to parse INI-style file: %s' % abspath) |
b32b8144 FG |
64 | terminal.error(str(error)) |
65 | raise RuntimeError('Unable to read configuration file: %s' % abspath) | |
d2e6a577 FG |
66 | |
67 | ||
eafe8130 | 68 | class Conf(conf_parentclass): |
d2e6a577 | 69 | """ |
eafe8130 | 70 | Subclasses from ConfigParser to give a few helpers for Ceph |
d2e6a577 FG |
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): | |
d2e6a577 FG |
79 | try: |
80 | self.get('global', 'fsid') | |
81 | except (configparser.NoSectionError, configparser.NoOptionError): | |
82 | raise exceptions.ConfigurationKeyError('global', 'fsid') | |
83 | ||
b32b8144 FG |
84 | def optionxform(self, s): |
85 | s = s.replace('_', ' ') | |
86 | s = '_'.join(s.split()) | |
87 | return s | |
88 | ||
d2e6a577 FG |
89 | def get_safe(self, section, key, default=None): |
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 | self.is_valid() | |
96 | try: | |
97 | return self.get(section, key) | |
98 | except (configparser.NoSectionError, configparser.NoOptionError): | |
99 | return default | |
100 | ||
101 | def get_list(self, section, key, default=None, split=','): | |
102 | """ | |
103 | Assumes that the value for a given key is going to be a list separated | |
104 | by commas. It gets rid of trailing comments. If just one item is | |
105 | present it returns a list with a single item, if no key is found an | |
106 | empty list is returned. | |
107 | ||
108 | Optionally split on other characters besides ',' and return a fallback | |
109 | value if no items are found. | |
110 | """ | |
111 | self.is_valid() | |
112 | value = self.get_safe(section, key, []) | |
113 | if value == []: | |
114 | if default is not None: | |
115 | return default | |
116 | return value | |
117 | ||
118 | # strip comments | |
119 | value = re.split(r'\s+#', value)[0] | |
120 | ||
121 | # split on commas | |
122 | value = value.split(split) | |
123 | ||
124 | # strip spaces | |
125 | return [x.strip() for x in value] | |
b32b8144 FG |
126 | |
127 | # XXX Almost all of it lifted from the original ConfigParser._read method, | |
128 | # except for the parsing of '#' in lines. This is only a problem in Python 2.7, and can be removed | |
129 | # once tooling is Python3 only with `Conf(inline_comment_prefixes=('#',';'))` | |
130 | def _read(self, fp, fpname): | |
131 | """Parse a sectioned setup file. | |
132 | ||
133 | The sections in setup file contains a title line at the top, | |
134 | indicated by a name in square brackets (`[]'), plus key/value | |
135 | options lines, indicated by `name: value' format lines. | |
136 | Continuations are represented by an embedded newline then | |
137 | leading whitespace. Blank lines, lines beginning with a '#', | |
138 | and just about everything else are ignored. | |
139 | """ | |
140 | cursect = None # None, or a dictionary | |
141 | optname = None | |
142 | lineno = 0 | |
143 | e = None # None, or an exception | |
144 | while True: | |
145 | line = fp.readline() | |
146 | if not line: | |
147 | break | |
148 | lineno = lineno + 1 | |
149 | # comment or blank line? | |
150 | if line.strip() == '' or line[0] in '#;': | |
151 | continue | |
152 | if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR": | |
153 | # no leading whitespace | |
154 | continue | |
155 | # continuation line? | |
156 | if line[0].isspace() and cursect is not None and optname: | |
157 | value = line.strip() | |
158 | if value: | |
159 | cursect[optname].append(value) | |
160 | # a section header or option header? | |
161 | else: | |
162 | # is it a section header? | |
163 | mo = self.SECTCRE.match(line) | |
164 | if mo: | |
165 | sectname = mo.group('header') | |
166 | if sectname in self._sections: | |
167 | cursect = self._sections[sectname] | |
168 | elif sectname == 'DEFAULT': | |
169 | cursect = self._defaults | |
170 | else: | |
171 | cursect = self._dict() | |
172 | cursect['__name__'] = sectname | |
173 | self._sections[sectname] = cursect | |
174 | # So sections can't start with a continuation line | |
175 | optname = None | |
176 | # no section header in the file? | |
177 | elif cursect is None: | |
178 | raise configparser.MissingSectionHeaderError(fpname, lineno, line) | |
179 | # an option line? | |
180 | else: | |
181 | mo = self._optcre.match(line) | |
182 | if mo: | |
183 | optname, vi, optval = mo.group('option', 'vi', 'value') | |
184 | optname = self.optionxform(optname.rstrip()) | |
185 | # This check is fine because the OPTCRE cannot | |
186 | # match if it would set optval to None | |
187 | if optval is not None: | |
188 | # XXX Added support for '#' inline comments | |
189 | if vi in ('=', ':') and (';' in optval or '#' in optval): | |
190 | # strip comments | |
191 | optval = re.split(r'\s+(;|#)', optval)[0] | |
192 | # if what is left is comment as a value, fallback to an empty string | |
193 | # that is: `foo = ;` would mean `foo` is '', which brings parity with | |
194 | # what ceph-conf tool does | |
195 | if optval in [';','#']: | |
196 | optval = '' | |
197 | optval = optval.strip() | |
198 | # allow empty values | |
199 | if optval == '""': | |
200 | optval = '' | |
201 | cursect[optname] = [optval] | |
202 | else: | |
203 | # valueless option handling | |
204 | cursect[optname] = optval | |
205 | else: | |
206 | # a non-fatal parsing error occurred. set up the | |
207 | # exception but keep going. the exception will be | |
208 | # raised at the end of the file and will contain a | |
209 | # list of all bogus lines | |
210 | if not e: | |
211 | e = configparser.ParsingError(fpname) | |
212 | e.append(lineno, repr(line)) | |
213 | # if any parsing errors occurred, raise an exception | |
214 | if e: | |
215 | raise e | |
216 | ||
217 | # join the multi-line values collected while reading | |
218 | all_sections = [self._defaults] | |
219 | all_sections.extend(self._sections.values()) | |
220 | for options in all_sections: | |
221 | for name, val in options.items(): | |
222 | if isinstance(val, list): | |
223 | options[name] = '\n'.join(val) | |
eafe8130 TL |
224 | |
225 | def read_conf(self, conffile): | |
226 | if sys_version_info.major >= 3: | |
227 | self.read_file(conffile) | |
228 | elif sys_version_info.major < 3: | |
229 | self.readfp(conffile) | |
230 | else: | |
231 | raise RuntimeError('Not expecting python version > 3 yet.') |