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