]>
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 | ||
2a845540 | 89 | def get_safe(self, section, key, default=None, check_valid=True): |
d2e6a577 FG |
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 | """ | |
2a845540 TL |
95 | if check_valid: |
96 | self.is_valid() | |
d2e6a577 FG |
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] | |
b32b8144 FG |
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) | |
eafe8130 TL |
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.') |