]>
Commit | Line | Data |
---|---|---|
2c8c4ce7 RP |
1 | #!/usr/bin/python |
2 | # | |
3 | # Copyright 2014 Cumulus Networks, Inc. All rights reserved. | |
4 | # Author: Roopa Prabhu, roopa@cumulusnetworks.com | |
5 | # | |
6 | # networkInterfaces -- | |
7 | # ifupdown network interfaces file parser | |
8 | # | |
9 | ||
10 | import collections | |
11 | import logging | |
12 | import glob | |
13 | import re | |
14 | import os | |
f82758bf RP |
15 | import copy |
16 | from utils import utils | |
2c8c4ce7 RP |
17 | from iface import * |
18 | from template import templateEngine | |
19 | ||
20 | whitespaces = '\n\t\r ' | |
21 | ||
22 | class networkInterfaces(): | |
23 | """ debian ifupdown /etc/network/interfaces file parser """ | |
24 | ||
25 | hotplugs = {} | |
26 | auto_ifaces = [] | |
27 | callbacks = {} | |
f82758bf | 28 | auto_all = False |
2c8c4ce7 RP |
29 | |
30 | _addrfams = {'inet' : ['static', 'manual', 'loopback', 'dhcp', 'dhcp6'], | |
31 | 'inet6' : ['static', 'manual', 'loopback', 'dhcp', 'dhcp6']} | |
32 | ||
33 | def __init__(self, interfacesfile='/etc/network/interfaces', | |
34 | interfacesfileiobuf=None, interfacesfileformat='native', | |
35 | template_engine=None, template_lookuppath=None): | |
36 | """This member function initializes the networkinterfaces parser object. | |
37 | ||
38 | Kwargs: | |
39 | **interfacesfile** (str): path to the interfaces file (default is /etc/network/interfaces) | |
40 | ||
41 | **interfacesfileiobuf** (object): interfaces file io stream | |
42 | ||
43 | **interfacesfileformat** (str): format of interfaces file (choices are 'native' and 'json'. 'native' being the default) | |
44 | ||
45 | **template_engine** (str): template engine name | |
46 | ||
47 | **template_lookuppath** (str): template lookup path | |
48 | ||
49 | Raises: | |
50 | AttributeError, KeyError """ | |
51 | ||
52 | self.logger = logging.getLogger('ifupdown.' + | |
53 | self.__class__.__name__) | |
54 | self.callbacks = {'iface_found' : None, | |
55 | 'validateifaceattr' : None, | |
56 | 'validateifaceobj' : None} | |
57 | self.allow_classes = {} | |
58 | self.interfacesfile = interfacesfile | |
59 | self.interfacesfileiobuf = interfacesfileiobuf | |
60 | self.interfacesfileformat = interfacesfileformat | |
61 | self._filestack = [self.interfacesfile] | |
62 | self._template_engine = templateEngine(template_engine, | |
63 | template_lookuppath) | |
64 | self._currentfile_has_template = False | |
65 | self._ws_split_regex = re.compile(r'[\s\t]\s*') | |
66 | ||
67 | @property | |
68 | def _currentfile(self): | |
69 | try: | |
70 | return self._filestack[-1] | |
71 | except: | |
72 | return self.interfacesfile | |
73 | ||
74 | def _parse_error(self, filename, lineno, msg): | |
75 | if lineno == -1 or self._currentfile_has_template: | |
76 | self.logger.error('%s: %s' %(filename, msg)) | |
77 | else: | |
78 | self.logger.error('%s: line%d: %s' %(filename, lineno, msg)) | |
79 | ||
f82758bf RP |
80 | def _parse_warn(self, filename, lineno, msg): |
81 | if lineno == -1 or self._currentfile_has_template: | |
82 | self.logger.warn('%s: %s' %(filename, msg)) | |
83 | else: | |
84 | self.logger.warn('%s: line%d: %s' %(filename, lineno, msg)) | |
85 | ||
2c8c4ce7 RP |
86 | def _validate_addr_family(self, ifaceobj, lineno=-1): |
87 | if ifaceobj.addr_family: | |
88 | if not self._addrfams.get(ifaceobj.addr_family): | |
89 | self._parse_error(self._currentfile, lineno, | |
90 | 'iface %s: unsupported address family \'%s\'' | |
91 | %(ifaceobj.name, ifaceobj.addr_family)) | |
92 | ifaceobj.addr_family = None | |
93 | ifaceobj.addr_method = None | |
94 | return | |
95 | if ifaceobj.addr_method: | |
96 | if (ifaceobj.addr_method not in | |
97 | self._addrfams.get(ifaceobj.addr_family)): | |
98 | self._parse_error(self._currentfile, lineno, | |
99 | 'iface %s: unsupported address method \'%s\'' | |
100 | %(ifaceobj.name, ifaceobj.addr_method)) | |
101 | else: | |
102 | ifaceobj.addr_method = 'static' | |
103 | ||
104 | def subscribe(self, callback_name, callback_func): | |
105 | """This member function registers callback functions. | |
106 | ||
107 | Args: | |
108 | **callback_name** (str): callback function name (supported names: 'iface_found', 'validateifaceattr', 'validateifaceobj') | |
109 | ||
110 | **callback_func** (function pointer): callback function pointer | |
111 | ||
112 | Warns on error | |
113 | """ | |
114 | ||
115 | if callback_name not in self.callbacks.keys(): | |
116 | print 'warning: invalid callback ' + callback_name | |
117 | return -1 | |
118 | ||
119 | self.callbacks[callback_name] = callback_func | |
120 | ||
121 | def ignore_line(self, line): | |
122 | l = line.strip(whitespaces) | |
123 | if not l or l[0] == '#': | |
124 | return 1 | |
125 | return 0 | |
126 | ||
127 | def process_allow(self, lines, cur_idx, lineno): | |
128 | allow_line = lines[cur_idx] | |
129 | ||
130 | words = re.split(self._ws_split_regex, allow_line) | |
131 | if len(words) <= 1: | |
132 | raise Exception('invalid allow line \'%s\' at line %d' | |
133 | %(allow_line, lineno)) | |
134 | ||
135 | allow_class = words[0].split('-')[1] | |
136 | ifacenames = words[1:] | |
137 | ||
138 | if self.allow_classes.get(allow_class): | |
139 | for i in ifacenames: | |
140 | self.allow_classes[allow_class].append(i) | |
141 | else: | |
142 | self.allow_classes[allow_class] = ifacenames | |
143 | return 0 | |
144 | ||
145 | def process_source(self, lines, cur_idx, lineno): | |
146 | # Support regex | |
147 | self.logger.debug('processing sourced line ..\'%s\'' %lines[cur_idx]) | |
148 | sourced_file = re.split(self._ws_split_regex, lines[cur_idx], 2)[1] | |
149 | if sourced_file: | |
f82758bf RP |
150 | filenames = glob.glob(sourced_file) |
151 | if not filenames: | |
152 | self._parse_warn(self._currentfile, lineno, | |
153 | 'cannot find source file %s' %sourced_file) | |
154 | return 0 | |
155 | for f in filenames: | |
2c8c4ce7 RP |
156 | self.read_file(f) |
157 | else: | |
158 | self._parse_error(self._currentfile, lineno, | |
159 | 'unable to read source line') | |
160 | return 0 | |
161 | ||
162 | def process_auto(self, lines, cur_idx, lineno): | |
163 | auto_ifaces = re.split(self._ws_split_regex, lines[cur_idx])[1:] | |
164 | if not auto_ifaces: | |
165 | self._parse_error(self._currentfile, lineno, | |
166 | 'invalid auto line \'%s\''%lines[cur_idx]) | |
167 | return 0 | |
f82758bf RP |
168 | for a in auto_ifaces: |
169 | if a == 'all': | |
170 | self.auto_all = True | |
171 | break | |
172 | r = utils.parse_iface_range(a) | |
173 | if r: | |
174 | for i in range(r[1], r[2]): | |
175 | self.auto_ifaces.append('%s-%d' %(r[0], i)) | |
176 | self.auto_ifaces.append(a) | |
2c8c4ce7 RP |
177 | return 0 |
178 | ||
179 | def _add_to_iface_config(self, ifacename, iface_config, attrname, | |
180 | attrval, lineno): | |
181 | newattrname = attrname.replace("_", "-") | |
182 | try: | |
f82758bf RP |
183 | if not self.callbacks.get('validateifaceattr')(newattrname, |
184 | attrval): | |
2c8c4ce7 RP |
185 | self._parse_error(self._currentfile, lineno, |
186 | 'iface %s: unsupported keyword (%s)' | |
187 | %(ifacename, attrname)) | |
188 | return | |
189 | except: | |
190 | pass | |
191 | attrvallist = iface_config.get(newattrname, []) | |
192 | if newattrname in ['scope', 'netmask', 'broadcast', 'preferred-lifetime']: | |
193 | # For attributes that are related and that can have multiple | |
194 | # entries, store them at the same index as their parent attribute. | |
195 | # The example of such attributes is 'address' and its related | |
196 | # attributes. since the related attributes can be optional, | |
197 | # we add null string '' in places where they are optional. | |
198 | # XXX: this introduces awareness of attribute names in | |
199 | # this class which is a violation. | |
200 | ||
201 | # get the index corresponding to the 'address' | |
202 | addrlist = iface_config.get('address') | |
203 | if addrlist: | |
204 | # find the index of last address element | |
205 | for i in range(0, len(addrlist) - len(attrvallist) -1): | |
206 | attrvallist.append('') | |
207 | attrvallist.append(attrval) | |
208 | iface_config[newattrname] = attrvallist | |
209 | elif not attrvallist: | |
210 | iface_config[newattrname] = [attrval] | |
211 | else: | |
212 | iface_config[newattrname].append(attrval) | |
213 | ||
f82758bf | 214 | def parse_iface(self, lines, cur_idx, lineno, ifaceobj): |
2c8c4ce7 RP |
215 | lines_consumed = 0 |
216 | line_idx = cur_idx | |
217 | ||
2c8c4ce7 RP |
218 | iface_line = lines[cur_idx].strip(whitespaces) |
219 | iface_attrs = re.split(self._ws_split_regex, iface_line) | |
220 | ifacename = iface_attrs[1] | |
221 | ||
f82758bf RP |
222 | # in cases where mako is unable to render the template |
223 | # or incorrectly renders it due to user template | |
224 | # errors, we maybe left with interface names with | |
225 | # mako variables in them. There is no easy way to | |
226 | # recognize and warn about these. In the below check | |
227 | # we try to warn the user of such cases by looking for | |
228 | # variable patterns ('$') in interface names. | |
229 | if '$' in ifacename: | |
230 | self._parse_warn(self._currentfile, lineno, | |
231 | '%s: unexpected characters in interface name' %ifacename) | |
232 | ||
2c8c4ce7 | 233 | ifaceobj.raw_config.append(iface_line) |
2c8c4ce7 RP |
234 | iface_config = collections.OrderedDict() |
235 | for line_idx in range(cur_idx + 1, len(lines)): | |
236 | l = lines[line_idx].strip(whitespaces) | |
237 | if self.ignore_line(l) == 1: | |
238 | continue | |
239 | attrs = re.split(self._ws_split_regex, l, 1) | |
240 | if self._is_keyword(attrs[0]): | |
241 | line_idx -= 1 | |
242 | break | |
243 | # if not a keyword, every line must have at least a key and value | |
244 | if len(attrs) < 2: | |
245 | self._parse_error(self._currentfile, line_idx, | |
246 | 'iface %s: invalid syntax \'%s\'' %(ifacename, l)) | |
247 | continue | |
248 | ifaceobj.raw_config.append(l) | |
249 | attrname = attrs[0] | |
250 | # preprocess vars (XXX: only preprocesses $IFACE for now) | |
251 | attrval = re.sub(r'\$IFACE', ifacename, attrs[1]) | |
252 | self._add_to_iface_config(ifacename, iface_config, attrname, | |
253 | attrval, line_idx+1) | |
254 | lines_consumed = line_idx - cur_idx | |
255 | ||
256 | # Create iface object | |
257 | if ifacename.find(':') != -1: | |
258 | ifaceobj.name = ifacename.split(':')[0] | |
259 | else: | |
260 | ifaceobj.name = ifacename | |
261 | ||
262 | ifaceobj.config = iface_config | |
263 | ifaceobj.generate_env() | |
264 | ||
265 | try: | |
266 | ifaceobj.addr_family = iface_attrs[2] | |
267 | ifaceobj.addr_method = iface_attrs[3] | |
268 | except IndexError: | |
269 | # ignore | |
270 | pass | |
271 | self._validate_addr_family(ifaceobj, lineno) | |
272 | ||
f82758bf | 273 | if self.auto_all or (ifaceobj.name in self.auto_ifaces): |
2c8c4ce7 RP |
274 | ifaceobj.auto = True |
275 | ||
276 | classes = self.get_allow_classes_for_iface(ifaceobj.name) | |
277 | if classes: | |
278 | [ifaceobj.set_class(c) for c in classes] | |
f82758bf RP |
279 | |
280 | return lines_consumed # Return next index | |
281 | ||
282 | def process_iface(self, lines, cur_idx, lineno): | |
283 | ifaceobj = iface() | |
284 | lines_consumed = self.parse_iface(lines, cur_idx, lineno, ifaceobj) | |
285 | ||
286 | range_val = utils.parse_iface_range(ifaceobj.name) | |
287 | if range_val: | |
288 | for v in range(range_val[1], range_val[2]): | |
289 | ifaceobj_new = copy.deepcopy(ifaceobj) | |
290 | ifaceobj_new.realname = '%s' %ifaceobj.name | |
291 | ifaceobj_new.name = '%s%d' %(range_val[0], v) | |
292 | ifaceobj_new.flags = iface.IFACERANGE_ENTRY | |
293 | if v == range_val[1]: | |
294 | ifaceobj_new.flags |= iface.IFACERANGE_START | |
295 | self.callbacks.get('iface_found')(ifaceobj_new) | |
296 | else: | |
297 | self.callbacks.get('iface_found')(ifaceobj) | |
2c8c4ce7 | 298 | |
2c8c4ce7 RP |
299 | return lines_consumed # Return next index |
300 | ||
f82758bf RP |
301 | def process_vlan(self, lines, cur_idx, lineno): |
302 | ifaceobj = iface() | |
303 | lines_consumed = self.parse_iface(lines, cur_idx, lineno, ifaceobj) | |
304 | ||
305 | range_val = utils.parse_iface_range(ifaceobj.name) | |
306 | if range_val: | |
307 | for v in range(range_val[1], range_val[2]): | |
308 | ifaceobj_new = copy.deepcopy(ifaceobj) | |
309 | ifaceobj_new.realname = '%s' %ifaceobj.name | |
310 | ifaceobj_new.name = '%s%d' %(range_val[0], v) | |
311 | ifaceobj_new.type = ifaceType.BRIDGE_VLAN | |
312 | ifaceobj_new.flags = iface.IFACERANGE_ENTRY | |
313 | if v == range_val[1]: | |
314 | ifaceobj_new.flags |= iface.IFACERANGE_START | |
315 | self.callbacks.get('iface_found')(ifaceobj_new) | |
316 | else: | |
317 | ifaceobj.type = ifaceType.BRIDGE_VLAN | |
318 | self.callbacks.get('iface_found')(ifaceobj) | |
319 | ||
320 | return lines_consumed # Return next index | |
2c8c4ce7 RP |
321 | |
322 | network_elems = { 'source' : process_source, | |
323 | 'allow' : process_allow, | |
324 | 'auto' : process_auto, | |
f82758bf RP |
325 | 'iface' : process_iface, |
326 | 'vlan' : process_vlan} | |
2c8c4ce7 RP |
327 | |
328 | def _is_keyword(self, str): | |
329 | # The additional split here is for allow- keyword | |
330 | tmp_str = str.split('-')[0] | |
331 | if tmp_str in self.network_elems.keys(): | |
332 | return 1 | |
333 | return 0 | |
334 | ||
335 | def _get_keyword_func(self, str): | |
336 | tmp_str = str.split('-')[0] | |
337 | return self.network_elems.get(tmp_str) | |
338 | ||
339 | def get_allow_classes_for_iface(self, ifacename): | |
340 | classes = [] | |
341 | for class_name, ifacenames in self.allow_classes.items(): | |
342 | if ifacename in ifacenames: | |
343 | classes.append(class_name) | |
344 | return classes | |
345 | ||
346 | def process_interfaces(self, filedata): | |
f82758bf RP |
347 | |
348 | # process line continuations | |
349 | filedata = ' '.join(d.strip() for d in filedata.split('\\')) | |
350 | ||
2c8c4ce7 RP |
351 | line_idx = 0 |
352 | lines_consumed = 0 | |
353 | raw_config = filedata.split('\n') | |
354 | lines = [l.strip(whitespaces) for l in raw_config] | |
355 | while (line_idx < len(lines)): | |
356 | if self.ignore_line(lines[line_idx]): | |
357 | line_idx += 1 | |
358 | continue | |
359 | words = re.split(self._ws_split_regex, lines[line_idx]) | |
360 | if not words: | |
361 | line_idx += 1 | |
362 | continue | |
363 | # Check if first element is a supported keyword | |
364 | if self._is_keyword(words[0]): | |
365 | keyword_func = self._get_keyword_func(words[0]) | |
366 | lines_consumed = keyword_func(self, lines, line_idx, line_idx+1) | |
367 | line_idx += lines_consumed | |
368 | else: | |
369 | self._parse_error(self._currentfile, line_idx + 1, | |
370 | 'error processing line \'%s\'' %lines[line_idx]) | |
371 | line_idx += 1 | |
372 | return 0 | |
373 | ||
374 | def read_filedata(self, filedata): | |
375 | self._currentfile_has_template = False | |
2c8c4ce7 RP |
376 | # run through template engine |
377 | try: | |
378 | rendered_filedata = self._template_engine.render(filedata) | |
379 | if rendered_filedata is filedata: | |
2c8c4ce7 | 380 | self._currentfile_has_template = False |
f82758bf RP |
381 | else: |
382 | self._currentfile_has_template = True | |
2c8c4ce7 RP |
383 | except Exception, e: |
384 | self._parse_error(self._currentfile, -1, | |
385 | 'failed to render template (%s). ' %str(e) + | |
386 | 'Continue without template rendering ...') | |
387 | rendered_filedata = None | |
388 | pass | |
389 | if rendered_filedata: | |
390 | self.process_interfaces(rendered_filedata) | |
391 | else: | |
392 | self.process_interfaces(filedata) | |
393 | ||
394 | def read_file(self, filename, fileiobuf=None): | |
395 | if fileiobuf: | |
396 | self.read_filedata(fileiobuf) | |
397 | return | |
398 | self._filestack.append(filename) | |
399 | self.logger.info('processing interfaces file %s' %filename) | |
400 | f = open(filename) | |
401 | filedata = f.read() | |
402 | f.close() | |
403 | self.read_filedata(filedata) | |
404 | self._filestack.pop() | |
405 | ||
406 | def read_file_json(self, filename, fileiobuf=None): | |
407 | if fileiobuf: | |
408 | ifacedicts = json.loads(fileiobuf, encoding="utf-8") | |
409 | #object_hook=ifaceJsonDecoder.json_object_hook) | |
410 | elif filename: | |
411 | self.logger.info('processing interfaces file %s' %filename) | |
412 | fp = open(filename) | |
413 | ifacedicts = json.load(fp) | |
414 | #object_hook=ifaceJsonDecoder.json_object_hook) | |
f82758bf RP |
415 | |
416 | # we need to handle both lists and non lists formats (e.g. {{}}) | |
417 | if not isinstance(ifacedicts,list): | |
418 | ifacedicts = [ifacedicts] | |
419 | ||
2c8c4ce7 RP |
420 | for ifacedict in ifacedicts: |
421 | ifaceobj = ifaceJsonDecoder.json_to_ifaceobj(ifacedict) | |
422 | if ifaceobj: | |
423 | self._validate_addr_family(ifaceobj) | |
424 | self.callbacks.get('validateifaceobj')(ifaceobj) | |
425 | self.callbacks.get('iface_found')(ifaceobj) | |
426 | ||
427 | def load(self): | |
428 | """ This member function loads the networkinterfaces file. | |
429 | ||
430 | Assumes networkinterfaces parser object is initialized with the | |
431 | parser arguments | |
432 | """ | |
433 | if self.interfacesfileformat == 'json': | |
434 | return self.read_file_json(self.interfacesfile, | |
435 | self.interfacesfileiobuf) | |
436 | return self.read_file(self.interfacesfile, | |
437 | self.interfacesfileiobuf) |