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