]> git.proxmox.com Git - mirror_ifupdown2.git/blame - ifupdown2/ifupdown/networkinterfaces.py
addons: addressvirtual: keep macvlan down if link-down specified on lower device
[mirror_ifupdown2.git] / ifupdown2 / ifupdown / networkinterfaces.py
CommitLineData
a6f80f0e 1#!/usr/bin/python
3e8ee54f 2#
d486dd0d 3# Copyright 2014-2017 Cumulus Networks, Inc. All rights reserved.
3e8ee54f 4# Author: Roopa Prabhu, roopa@cumulusnetworks.com
5#
6# networkInterfaces --
7# ifupdown network interfaces file parser
8#
a6f80f0e 9
739f665b 10import re
679e6567 11import copy
d486dd0d
JF
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
a6f80f0e 25
c0071225
RP
26whitespaces = '\n\t\r '
27
a6f80f0e 28class networkInterfaces():
2c0ad8b3 29 """ debian ifupdown /etc/network/interfaces file parser """
a6f80f0e 30
77054f7f
SA
31 _addrfams = {'inet' : ['static', 'manual', 'loopback', 'dhcp', 'dhcp6', 'ppp', 'tunnel'],
32 'inet6' : ['static', 'manual', 'loopback', 'dhcp', 'dhcp6', 'ppp', 'tunnel']}
d913472d 33
14dc390d 34 def __init__(self, interfacesfile='/etc/network/interfaces',
3dcc1d0e 35 interfacesfileiobuf=None, interfacesfileformat='native',
f27710fe
RP
36 template_enable='0', template_engine=None,
37 template_lookuppath=None):
2c0ad8b3
RP
38 """This member function initializes the networkinterfaces parser object.
39
40 Kwargs:
904908bc
RP
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
2c0ad8b3
RP
50
51 Raises:
52 AttributeError, KeyError """
53
d486dd0d
JF
54 self.auto_ifaces = []
55 self.callbacks = {}
56 self.auto_all = False
57
a6f80f0e 58 self.logger = logging.getLogger('ifupdown.' +
59 self.__class__.__name__)
d08d5f54 60 self.callbacks = {'iface_found' : None,
3dcc1d0e 61 'validateifaceattr' : None,
62 'validateifaceobj' : None}
a6f80f0e 63 self.allow_classes = {}
14dc390d 64 self.interfacesfile = interfacesfile
3dcc1d0e 65 self.interfacesfileiobuf = interfacesfileiobuf
66 self.interfacesfileformat = interfacesfileformat
14dc390d 67 self._filestack = [self.interfacesfile]
e272efd9
JF
68
69 self._template_engine = None
1f5db4a8 70 self._template_enable = template_enable
e272efd9
JF
71 self._template_engine_name = template_engine
72 self._template_engine_path = template_lookuppath
73
d40e96ee 74 self._currentfile_has_template = False
f102ef63 75 self._ws_split_regex = re.compile(r'[\s\t]\s*')
a6f80f0e 76
cfa06db6
RP
77 self.errors = 0
78 self.warns = 0
79
9dce3561 80 @property
81 def _currentfile(self):
82 try:
83 return self._filestack[-1]
84 except:
14dc390d 85 return self.interfacesfile
9dce3561 86
87 def _parse_error(self, filename, lineno, msg):
d40e96ee 88 if lineno == -1 or self._currentfile_has_template:
9dce3561 89 self.logger.error('%s: %s' %(filename, msg))
90 else:
91 self.logger.error('%s: line%d: %s' %(filename, lineno, msg))
cfa06db6 92 self.errors += 1
a6f80f0e 93
aa5751ba
RP
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))
cfa06db6 99 self.warns += 1
aa5751ba 100
3dcc1d0e 101 def _validate_addr_family(self, ifaceobj, lineno=-1):
004d1e65
JF
102 for family in ifaceobj.addr_family:
103 if not self._addrfams.get(family):
d913472d 104 self._parse_error(self._currentfile, lineno,
004d1e65
JF
105 'iface %s: unsupported address family \'%s\''
106 % (ifaceobj.name, family))
107 ifaceobj.addr_family = []
d913472d 108 ifaceobj.addr_method = None
109 return
110 if ifaceobj.addr_method:
004d1e65 111 if ifaceobj.addr_method not in self._addrfams.get(family):
d913472d 112 self._parse_error(self._currentfile, lineno,
004d1e65
JF
113 'iface %s: unsupported '
114 'address method \'%s\''
115 % (ifaceobj.name, ifaceobj.addr_method))
d913472d 116 else:
117 ifaceobj.addr_method = 'static'
118
a6f80f0e 119 def subscribe(self, callback_name, callback_func):
904908bc 120 """This member function registers callback functions.
2c0ad8b3
RP
121
122 Args:
904908bc
RP
123 **callback_name** (str): callback function name (supported names: 'iface_found', 'validateifaceattr', 'validateifaceobj')
124
125 **callback_func** (function pointer): callback function pointer
2c0ad8b3
RP
126
127 Warns on error
128 """
129
a6f80f0e 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
a6f80f0e 136 def ignore_line(self, line):
c0071225 137 l = line.strip(whitespaces)
fe0a57d3 138 if not l or l[0] == '#':
a6f80f0e 139 return 1
a6f80f0e 140 return 0
141
142 def process_allow(self, lines, cur_idx, lineno):
3e8ee54f 143 allow_line = lines[cur_idx]
a6f80f0e 144
f102ef63 145 words = re.split(self._ws_split_regex, allow_line)
a6f80f0e 146 if len(words) <= 1:
be0b20f2 147 raise Exception('invalid allow line \'%s\' at line %d'
148 %(allow_line, lineno))
a6f80f0e 149
150 allow_class = words[0].split('-')[1]
151 ifacenames = words[1:]
152
62ddec8b 153 if self.allow_classes.get(allow_class):
a6f80f0e 154 for i in ifacenames:
155 self.allow_classes[allow_class].append(i)
156 else:
157 self.allow_classes[allow_class] = ifacenames
a6f80f0e 158 return 0
159
a6f80f0e 160 def process_source(self, lines, cur_idx, lineno):
161 # Support regex
37c0543d 162 self.logger.debug('processing sourced line ..\'%s\'' %lines[cur_idx])
f102ef63 163 sourced_file = re.split(self._ws_split_regex, lines[cur_idx], 2)[1]
62ddec8b 164 if sourced_file:
2b5635d4
RP
165 filenames = glob.glob(sourced_file)
166 if not filenames:
3cdb1619
RP
167 if '*' not in sourced_file:
168 self._parse_warn(self._currentfile, lineno,
2b5635d4
RP
169 'cannot find source file %s' %sourced_file)
170 return 0
171 for f in filenames:
eab25b7c 172 self.read_file(f)
a6f80f0e 173 else:
9dce3561 174 self._parse_error(self._currentfile, lineno,
175 'unable to read source line')
a6f80f0e 176 return 0
177
178 def process_auto(self, lines, cur_idx, lineno):
f102ef63 179 auto_ifaces = re.split(self._ws_split_regex, lines[cur_idx])[1:]
9dce3561 180 if not auto_ifaces:
d913472d 181 self._parse_error(self._currentfile, lineno,
9dce3561 182 'invalid auto line \'%s\''%lines[cur_idx])
183 return 0
679e6567
RP
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:
eba4da6e
RP
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]))
679e6567 198 self.auto_ifaces.append(a)
a6f80f0e 199 return 0
200
14dc390d 201 def _add_to_iface_config(self, ifacename, iface_config, attrname,
202 attrval, lineno):
7a51240e 203 newattrname = attrname.replace("_", "-")
7949b8a5 204 try:
9e012f9e
RP
205 if not self.callbacks.get('validateifaceattr')(newattrname,
206 attrval):
7949b8a5 207 self._parse_error(self._currentfile, lineno,
14dc390d 208 'iface %s: unsupported keyword (%s)'
209 %(ifacename, attrname))
7949b8a5 210 return
211 except:
212 pass
7a51240e 213 attrvallist = iface_config.get(newattrname, [])
214 if newattrname in ['scope', 'netmask', 'broadcast', 'preferred-lifetime']:
be0b20f2 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
d486dd0d 218 # attributes. since the related attributes can be optional,
be0b20f2 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)
7a51240e 230 iface_config[newattrname] = attrvallist
be0b20f2 231 elif not attrvallist:
7a51240e 232 iface_config[newattrname] = [attrval]
be0b20f2 233 else:
7a51240e 234 iface_config[newattrname].append(attrval)
a6f80f0e 235
679e6567 236 def parse_iface(self, lines, cur_idx, lineno, ifaceobj):
a6f80f0e 237 lines_consumed = 0
579b3f25 238 line_idx = cur_idx
a6f80f0e 239
c0071225 240 iface_line = lines[cur_idx].strip(whitespaces)
f102ef63 241 iface_attrs = re.split(self._ws_split_regex, iface_line)
739f665b 242 ifacename = iface_attrs[1]
a6f80f0e 243
eba4da6e
RP
244 if (not utils.is_ifname_range(ifacename) and
245 utils.check_ifname_size_invalid(ifacename)):
9432c671
RP
246 self._parse_warn(self._currentfile, lineno,
247 '%s: interface name too long' %ifacename)
248
ef892ccc
RP
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
62ddec8b 260 ifaceobj.raw_config.append(iface_line)
a6f80f0e 261 iface_config = collections.OrderedDict()
262 for line_idx in range(cur_idx + 1, len(lines)):
c0071225 263 l = lines[line_idx].strip(whitespaces)
a6f80f0e 264 if self.ignore_line(l) == 1:
d486dd0d 265 ifaceobj.raw_config.append(l)
a6f80f0e 266 continue
f102ef63
RP
267 attrs = re.split(self._ws_split_regex, l, 1)
268 if self._is_keyword(attrs[0]):
a6f80f0e 269 line_idx -= 1
270 break
f102ef63 271 # if not a keyword, every line must have at least a key and value
eab25b7c 272 if len(attrs) < 2:
9dce3561 273 self._parse_error(self._currentfile, line_idx,
14dc390d 274 'iface %s: invalid syntax \'%s\'' %(ifacename, l))
eab25b7c 275 continue
f102ef63 276 ifaceobj.raw_config.append(l)
d08d5f54 277 attrname = attrs[0]
f102ef63
RP
278 # preprocess vars (XXX: only preprocesses $IFACE for now)
279 attrval = re.sub(r'\$IFACE', ifacename, attrs[1])
14dc390d 280 self._add_to_iface_config(ifacename, iface_config, attrname,
281 attrval, line_idx+1)
a6f80f0e 282 lines_consumed = line_idx - cur_idx
283
284 # Create iface object
37c0543d 285 if ifacename.find(':') != -1:
62ddec8b 286 ifaceobj.name = ifacename.split(':')[0]
37c0543d 287 else:
62ddec8b 288 ifaceobj.name = ifacename
37c0543d 289
62ddec8b 290 ifaceobj.config = iface_config
a6f80f0e 291 ifaceobj.generate_env()
d913472d 292
293 try:
004d1e65
JF
294 if iface_attrs[2]:
295 ifaceobj.addr_family.append(iface_attrs[2])
62ddec8b 296 ifaceobj.addr_method = iface_attrs[3]
d913472d 297 except IndexError:
298 # ignore
299 pass
300 self._validate_addr_family(ifaceobj, lineno)
a6f80f0e 301
679e6567 302 if self.auto_all or (ifaceobj.name in self.auto_ifaces):
62ddec8b 303 ifaceobj.auto = True
a6f80f0e 304
62ddec8b 305 classes = self.get_allow_classes_for_iface(ifaceobj.name)
fe0a57d3 306 if classes:
62ddec8b 307 [ifaceobj.set_class(c) for c in classes]
d486dd0d 308
a6f80f0e 309 return lines_consumed # Return next index
310
eba4da6e
RP
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
679e6567
RP
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:
eba4da6e
RP
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)
679e6567
RP
351 else:
352 self.callbacks.get('iface_found')(ifaceobj)
353
354 return lines_consumed # Return next index
a6f80f0e 355
84ca006f
RP
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:
eba4da6e
RP
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)
84ca006f
RP
381 else:
382 ifaceobj.type = ifaceType.BRIDGE_VLAN
383 self.callbacks.get('iface_found')(ifaceobj)
384
385 return lines_consumed # Return next index
386
a6f80f0e 387 network_elems = { 'source' : process_source,
388 'allow' : process_allow,
389 'auto' : process_auto,
84ca006f
RP
390 'iface' : process_iface,
391 'vlan' : process_vlan}
a6f80f0e 392
9dce3561 393 def _is_keyword(self, str):
a6f80f0e 394 # The additional split here is for allow- keyword
3d58f6af
RP
395 if (str in self.network_elems.keys() or
396 str.split('-')[0] == 'allow'):
a6f80f0e 397 return 1
a6f80f0e 398 return 0
399
9dce3561 400 def _get_keyword_func(self, str):
a6f80f0e 401 tmp_str = str.split('-')[0]
a6f80f0e 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)
a6f80f0e 409 return classes
410
3dcc1d0e 411 def process_interfaces(self, filedata):
5b65654f
RP
412
413 # process line continuations
414 filedata = ' '.join(d.strip() for d in filedata.split('\\'))
415
a6f80f0e 416 line_idx = 0
417 lines_consumed = 0
62ddec8b 418 raw_config = filedata.split('\n')
c0071225 419 lines = [l.strip(whitespaces) for l in raw_config]
579b3f25 420 while (line_idx < len(lines)):
579b3f25 421 if self.ignore_line(lines[line_idx]):
422 line_idx += 1
423 continue
f102ef63 424 words = re.split(self._ws_split_regex, lines[line_idx])
c0071225
RP
425 if not words:
426 line_idx += 1
427 continue
579b3f25 428 # Check if first element is a supported keyword
9dce3561 429 if self._is_keyword(words[0]):
430 keyword_func = self._get_keyword_func(words[0])
d913472d 431 lines_consumed = keyword_func(self, lines, line_idx, line_idx+1)
579b3f25 432 line_idx += lines_consumed
433 else:
9dce3561 434 self._parse_error(self._currentfile, line_idx + 1,
435 'error processing line \'%s\'' %lines[line_idx])
579b3f25 436 line_idx += 1
579b3f25 437 return 0
a6f80f0e 438
3dcc1d0e 439 def read_filedata(self, filedata):
d40e96ee 440 self._currentfile_has_template = False
579b3f25 441 # run through template engine
e272efd9
JF
442 if filedata and '%' in filedata:
443 try:
800417ae
JF
444 if not self._template_engine:
445 self._template_engine = templateEngine(
1f5db4a8
MG
446 template_engine=self._template_engine_name,
447 template_enable=self._template_enable,
448 template_lookuppath=self._template_engine_path)
e272efd9
JF
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)
3dcc1d0e 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)
38510743 470 try:
a193d8d1
JF
471 with open(filename) as f:
472 filedata = f.read()
38510743
RP
473 except Exception, e:
474 self.logger.warn('error processing file %s (%s)',
475 filename, str(e))
476 return
3dcc1d0e 477 self.read_filedata(filedata)
9dce3561 478 self._filestack.pop()
a6f80f0e 479
3dcc1d0e 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)
a193d8d1
JF
486 with open(filename) as fp:
487 ifacedicts = json.load(fp)
3dcc1d0e 488 #object_hook=ifaceJsonDecoder.json_object_hook)
b6c1f551
ST
489
490 # we need to handle both lists and non lists formats (e.g. {{}})
491 if not isinstance(ifacedicts,list):
492 ifacedicts = [ifacedicts]
493
cfa06db6 494 errors = 0
3dcc1d0e 495 for ifacedict in ifacedicts:
496 ifaceobj = ifaceJsonDecoder.json_to_ifaceobj(ifacedict)
497 if ifaceobj:
498 self._validate_addr_family(ifaceobj)
cfa06db6
RP
499 if not self.callbacks.get('validateifaceobj')(ifaceobj):
500 errors += 1
3dcc1d0e 501 self.callbacks.get('iface_found')(ifaceobj)
cfa06db6 502 self.errors += errors
d486dd0d 503
3dcc1d0e 504 def load(self):
2c0ad8b3
RP
505 """ This member function loads the networkinterfaces file.
506
507 Assumes networkinterfaces parser object is initialized with the
508 parser arguments
509 """
c28fc55e
ST
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.')
cfa06db6 513 self.warns += 1
1e6d7bd7
ST
514 return
515
3dcc1d0e 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)