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