]> git.proxmox.com Git - mirror_ifupdown2.git/blame - ifupdown/networkinterfaces.py
Change "source" inclusion errors to warns.
[mirror_ifupdown2.git] / ifupdown / networkinterfaces.py
CommitLineData
a6f80f0e 1#!/usr/bin/python
3e8ee54f 2#
904908bc 3# Copyright 2014 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
10import collections
a6f80f0e 11import logging
12import glob
739f665b 13import re
14dc390d 14import os
679e6567
RP
15import copy
16from utils import utils
3e8ee54f 17from iface import *
14dc390d 18from template import templateEngine
a6f80f0e 19
c0071225
RP
20whitespaces = '\n\t\r '
21
a6f80f0e 22class networkInterfaces():
2c0ad8b3 23 """ debian ifupdown /etc/network/interfaces file parser """
a6f80f0e 24
25 hotplugs = {}
26 auto_ifaces = []
27 callbacks = {}
679e6567 28 auto_all = False
a6f80f0e 29
d913472d 30 _addrfams = {'inet' : ['static', 'manual', 'loopback', 'dhcp', 'dhcp6'],
31 'inet6' : ['static', 'manual', 'loopback', 'dhcp', 'dhcp6']}
32
14dc390d 33 def __init__(self, interfacesfile='/etc/network/interfaces',
3dcc1d0e 34 interfacesfileiobuf=None, interfacesfileformat='native',
35 template_engine=None, template_lookuppath=None):
2c0ad8b3
RP
36 """This member function initializes the networkinterfaces parser object.
37
38 Kwargs:
904908bc
RP
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
2c0ad8b3
RP
48
49 Raises:
50 AttributeError, KeyError """
51
a6f80f0e 52 self.logger = logging.getLogger('ifupdown.' +
53 self.__class__.__name__)
d08d5f54 54 self.callbacks = {'iface_found' : None,
3dcc1d0e 55 'validateifaceattr' : None,
56 'validateifaceobj' : None}
a6f80f0e 57 self.allow_classes = {}
14dc390d 58 self.interfacesfile = interfacesfile
3dcc1d0e 59 self.interfacesfileiobuf = interfacesfileiobuf
60 self.interfacesfileformat = interfacesfileformat
14dc390d 61 self._filestack = [self.interfacesfile]
62 self._template_engine = templateEngine(template_engine,
63 template_lookuppath)
d40e96ee 64 self._currentfile_has_template = False
f102ef63 65 self._ws_split_regex = re.compile(r'[\s\t]\s*')
a6f80f0e 66
9dce3561 67 @property
68 def _currentfile(self):
69 try:
70 return self._filestack[-1]
71 except:
14dc390d 72 return self.interfacesfile
9dce3561 73
74 def _parse_error(self, filename, lineno, msg):
d40e96ee 75 if lineno == -1 or self._currentfile_has_template:
9dce3561 76 self.logger.error('%s: %s' %(filename, msg))
77 else:
78 self.logger.error('%s: line%d: %s' %(filename, lineno, msg))
a6f80f0e 79
aa5751ba
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
3dcc1d0e 86 def _validate_addr_family(self, ifaceobj, lineno=-1):
d913472d 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
a6f80f0e 104 def subscribe(self, callback_name, callback_func):
904908bc 105 """This member function registers callback functions.
2c0ad8b3
RP
106
107 Args:
904908bc
RP
108 **callback_name** (str): callback function name (supported names: 'iface_found', 'validateifaceattr', 'validateifaceobj')
109
110 **callback_func** (function pointer): callback function pointer
2c0ad8b3
RP
111
112 Warns on error
113 """
114
a6f80f0e 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
a6f80f0e 121 def ignore_line(self, line):
c0071225 122 l = line.strip(whitespaces)
fe0a57d3 123 if not l or l[0] == '#':
a6f80f0e 124 return 1
a6f80f0e 125 return 0
126
127 def process_allow(self, lines, cur_idx, lineno):
3e8ee54f 128 allow_line = lines[cur_idx]
a6f80f0e 129
f102ef63 130 words = re.split(self._ws_split_regex, allow_line)
a6f80f0e 131 if len(words) <= 1:
be0b20f2 132 raise Exception('invalid allow line \'%s\' at line %d'
133 %(allow_line, lineno))
a6f80f0e 134
135 allow_class = words[0].split('-')[1]
136 ifacenames = words[1:]
137
62ddec8b 138 if self.allow_classes.get(allow_class):
a6f80f0e 139 for i in ifacenames:
140 self.allow_classes[allow_class].append(i)
141 else:
142 self.allow_classes[allow_class] = ifacenames
a6f80f0e 143 return 0
144
a6f80f0e 145 def process_source(self, lines, cur_idx, lineno):
146 # Support regex
37c0543d 147 self.logger.debug('processing sourced line ..\'%s\'' %lines[cur_idx])
f102ef63 148 sourced_file = re.split(self._ws_split_regex, lines[cur_idx], 2)[1]
62ddec8b 149 if sourced_file:
2b5635d4
RP
150 filenames = glob.glob(sourced_file)
151 if not filenames:
aa5751ba 152 self._parse_warn(self._currentfile, lineno,
2b5635d4
RP
153 'cannot find source file %s' %sourced_file)
154 return 0
155 for f in filenames:
eab25b7c 156 self.read_file(f)
a6f80f0e 157 else:
9dce3561 158 self._parse_error(self._currentfile, lineno,
159 'unable to read source line')
a6f80f0e 160 return 0
161
162 def process_auto(self, lines, cur_idx, lineno):
f102ef63 163 auto_ifaces = re.split(self._ws_split_regex, lines[cur_idx])[1:]
9dce3561 164 if not auto_ifaces:
d913472d 165 self._parse_error(self._currentfile, lineno,
9dce3561 166 'invalid auto line \'%s\''%lines[cur_idx])
167 return 0
679e6567
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)
a6f80f0e 177 return 0
178
14dc390d 179 def _add_to_iface_config(self, ifacename, iface_config, attrname,
180 attrval, lineno):
7a51240e 181 newattrname = attrname.replace("_", "-")
7949b8a5 182 try:
9e012f9e
RP
183 if not self.callbacks.get('validateifaceattr')(newattrname,
184 attrval):
7949b8a5 185 self._parse_error(self._currentfile, lineno,
14dc390d 186 'iface %s: unsupported keyword (%s)'
187 %(ifacename, attrname))
7949b8a5 188 return
189 except:
190 pass
7a51240e 191 attrvallist = iface_config.get(newattrname, [])
192 if newattrname in ['scope', 'netmask', 'broadcast', 'preferred-lifetime']:
be0b20f2 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)
7a51240e 208 iface_config[newattrname] = attrvallist
be0b20f2 209 elif not attrvallist:
7a51240e 210 iface_config[newattrname] = [attrval]
be0b20f2 211 else:
7a51240e 212 iface_config[newattrname].append(attrval)
a6f80f0e 213
679e6567 214 def parse_iface(self, lines, cur_idx, lineno, ifaceobj):
a6f80f0e 215 lines_consumed = 0
579b3f25 216 line_idx = cur_idx
a6f80f0e 217
c0071225 218 iface_line = lines[cur_idx].strip(whitespaces)
f102ef63 219 iface_attrs = re.split(self._ws_split_regex, iface_line)
739f665b 220 ifacename = iface_attrs[1]
a6f80f0e 221
62ddec8b 222 ifaceobj.raw_config.append(iface_line)
a6f80f0e 223
224 iface_config = collections.OrderedDict()
225 for line_idx in range(cur_idx + 1, len(lines)):
c0071225 226 l = lines[line_idx].strip(whitespaces)
a6f80f0e 227 if self.ignore_line(l) == 1:
228 continue
f102ef63
RP
229 attrs = re.split(self._ws_split_regex, l, 1)
230 if self._is_keyword(attrs[0]):
a6f80f0e 231 line_idx -= 1
232 break
f102ef63 233 # if not a keyword, every line must have at least a key and value
eab25b7c 234 if len(attrs) < 2:
9dce3561 235 self._parse_error(self._currentfile, line_idx,
14dc390d 236 'iface %s: invalid syntax \'%s\'' %(ifacename, l))
eab25b7c 237 continue
f102ef63 238 ifaceobj.raw_config.append(l)
d08d5f54 239 attrname = attrs[0]
f102ef63
RP
240 # preprocess vars (XXX: only preprocesses $IFACE for now)
241 attrval = re.sub(r'\$IFACE', ifacename, attrs[1])
14dc390d 242 self._add_to_iface_config(ifacename, iface_config, attrname,
243 attrval, line_idx+1)
a6f80f0e 244 lines_consumed = line_idx - cur_idx
245
246 # Create iface object
37c0543d 247 if ifacename.find(':') != -1:
62ddec8b 248 ifaceobj.name = ifacename.split(':')[0]
37c0543d 249 else:
62ddec8b 250 ifaceobj.name = ifacename
37c0543d 251
62ddec8b 252 ifaceobj.config = iface_config
a6f80f0e 253 ifaceobj.generate_env()
d913472d 254
255 try:
62ddec8b 256 ifaceobj.addr_family = iface_attrs[2]
257 ifaceobj.addr_method = iface_attrs[3]
d913472d 258 except IndexError:
259 # ignore
260 pass
261 self._validate_addr_family(ifaceobj, lineno)
a6f80f0e 262
679e6567 263 if self.auto_all or (ifaceobj.name in self.auto_ifaces):
62ddec8b 264 ifaceobj.auto = True
a6f80f0e 265
62ddec8b 266 classes = self.get_allow_classes_for_iface(ifaceobj.name)
fe0a57d3 267 if classes:
62ddec8b 268 [ifaceobj.set_class(c) for c in classes]
679e6567 269
a6f80f0e 270 return lines_consumed # Return next index
271
679e6567
RP
272 def process_iface(self, lines, cur_idx, lineno):
273 ifaceobj = iface()
274 lines_consumed = self.parse_iface(lines, cur_idx, lineno, ifaceobj)
275
276 range_val = utils.parse_iface_range(ifaceobj.name)
277 if range_val:
278 for v in range(range_val[1], range_val[2]):
279 ifaceobj_new = copy.deepcopy(ifaceobj)
2da58137 280 ifaceobj_new.realname = ifaceobj.name
84ca006f 281 ifaceobj_new.name = "%s%d" %(range_val[0], v)
2da58137
RP
282 ifaceobj_new.flags |= iface.IFACERANGE_ENTRY
283 if v == range_val[1]:
284 ifaceobj_new.flags |= iface.IFACERANGE_START
679e6567
RP
285 self.callbacks.get('iface_found')(ifaceobj_new)
286 else:
287 self.callbacks.get('iface_found')(ifaceobj)
288
289 return lines_consumed # Return next index
a6f80f0e 290
84ca006f
RP
291 def process_vlan(self, lines, cur_idx, lineno):
292 ifaceobj = iface()
293 lines_consumed = self.parse_iface(lines, cur_idx, lineno, ifaceobj)
294
295 range_val = utils.parse_iface_range(ifaceobj.name)
296 if range_val:
297 for v in range(range_val[1], range_val[2]):
298 ifaceobj_new = copy.deepcopy(ifaceobj)
2da58137 299 ifaceobj_new.realname = ifaceobj.name
84ca006f
RP
300 ifaceobj_new.name = "%s%d" %(range_val[0], v)
301 ifaceobj_new.type = ifaceType.BRIDGE_VLAN
2da58137
RP
302 ifaceobj_new.flags |= iface.IFACERANGE_ENTRY
303 if v == range_val[1]:
304 ifaceobj_new.flags |= iface.IFACERANGE_START
84ca006f
RP
305 self.callbacks.get('iface_found')(ifaceobj_new)
306 else:
307 ifaceobj.type = ifaceType.BRIDGE_VLAN
308 self.callbacks.get('iface_found')(ifaceobj)
309
310 return lines_consumed # Return next index
311
a6f80f0e 312 network_elems = { 'source' : process_source,
313 'allow' : process_allow,
314 'auto' : process_auto,
84ca006f
RP
315 'iface' : process_iface,
316 'vlan' : process_vlan}
a6f80f0e 317
9dce3561 318 def _is_keyword(self, str):
a6f80f0e 319 # The additional split here is for allow- keyword
320 tmp_str = str.split('-')[0]
321 if tmp_str in self.network_elems.keys():
322 return 1
a6f80f0e 323 return 0
324
9dce3561 325 def _get_keyword_func(self, str):
a6f80f0e 326 tmp_str = str.split('-')[0]
a6f80f0e 327 return self.network_elems.get(tmp_str)
328
329 def get_allow_classes_for_iface(self, ifacename):
330 classes = []
331 for class_name, ifacenames in self.allow_classes.items():
332 if ifacename in ifacenames:
333 classes.append(class_name)
a6f80f0e 334 return classes
335
3dcc1d0e 336 def process_interfaces(self, filedata):
a6f80f0e 337 line_idx = 0
338 lines_consumed = 0
62ddec8b 339 raw_config = filedata.split('\n')
c0071225 340 lines = [l.strip(whitespaces) for l in raw_config]
579b3f25 341 while (line_idx < len(lines)):
579b3f25 342 if self.ignore_line(lines[line_idx]):
343 line_idx += 1
344 continue
f102ef63 345 words = re.split(self._ws_split_regex, lines[line_idx])
c0071225
RP
346 if not words:
347 line_idx += 1
348 continue
579b3f25 349 # Check if first element is a supported keyword
9dce3561 350 if self._is_keyword(words[0]):
351 keyword_func = self._get_keyword_func(words[0])
d913472d 352 lines_consumed = keyword_func(self, lines, line_idx, line_idx+1)
579b3f25 353 line_idx += lines_consumed
354 else:
9dce3561 355 self._parse_error(self._currentfile, line_idx + 1,
356 'error processing line \'%s\'' %lines[line_idx])
579b3f25 357 line_idx += 1
579b3f25 358 return 0
a6f80f0e 359
3dcc1d0e 360 def read_filedata(self, filedata):
d40e96ee 361 self._currentfile_has_template = False
579b3f25 362 # process line continuations
363 filedata = ' '.join(d.strip() for d in filedata.split('\\'))
579b3f25 364 # run through template engine
9dce3561 365 try:
14dc390d 366 rendered_filedata = self._template_engine.render(filedata)
d40e96ee 367 if rendered_filedata is filedata:
368 self._currentfile_has_template = True
369 else:
370 self._currentfile_has_template = False
9dce3561 371 except Exception, e:
372 self._parse_error(self._currentfile, -1,
14dc390d 373 'failed to render template (%s). ' %str(e) +
9dce3561 374 'Continue without template rendering ...')
375 rendered_filedata = None
376 pass
9dce3561 377 if rendered_filedata:
3dcc1d0e 378 self.process_interfaces(rendered_filedata)
9dce3561 379 else:
3dcc1d0e 380 self.process_interfaces(filedata)
381
382 def read_file(self, filename, fileiobuf=None):
383 if fileiobuf:
384 self.read_filedata(fileiobuf)
385 return
386 self._filestack.append(filename)
387 self.logger.info('processing interfaces file %s' %filename)
388 f = open(filename)
389 filedata = f.read()
390 f.close()
391 self.read_filedata(filedata)
9dce3561 392 self._filestack.pop()
a6f80f0e 393
3dcc1d0e 394 def read_file_json(self, filename, fileiobuf=None):
395 if fileiobuf:
396 ifacedicts = json.loads(fileiobuf, encoding="utf-8")
397 #object_hook=ifaceJsonDecoder.json_object_hook)
398 elif filename:
399 self.logger.info('processing interfaces file %s' %filename)
400 fp = open(filename)
401 ifacedicts = json.load(fp)
402 #object_hook=ifaceJsonDecoder.json_object_hook)
403 for ifacedict in ifacedicts:
404 ifaceobj = ifaceJsonDecoder.json_to_ifaceobj(ifacedict)
405 if ifaceobj:
406 self._validate_addr_family(ifaceobj)
407 self.callbacks.get('validateifaceobj')(ifaceobj)
408 self.callbacks.get('iface_found')(ifaceobj)
409
410 def load(self):
2c0ad8b3
RP
411 """ This member function loads the networkinterfaces file.
412
413 Assumes networkinterfaces parser object is initialized with the
414 parser arguments
415 """
3dcc1d0e 416 if self.interfacesfileformat == 'json':
417 return self.read_file_json(self.interfacesfile,
418 self.interfacesfileiobuf)
419 return self.read_file(self.interfacesfile,
420 self.interfacesfileiobuf)