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