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