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