]> git.proxmox.com Git - mirror_ifupdown2.git/blame - ifupdown2/ifupdown/utils.py
Fix the return value of utils._execute_subprocess
[mirror_ifupdown2.git] / ifupdown2 / ifupdown / utils.py
CommitLineData
35681c06 1#!/usr/bin/env python3
904908bc 2#
d486dd0d 3# Copyright 2014-2017 Cumulus Networks, Inc. All rights reserved.
904908bc
RP
4# Author: Roopa Prabhu, roopa@cumulusnetworks.com
5#
6# utils --
7# helper class
8#
a193d8d1 9
d40e96ee 10import os
679e6567 11import re
a193d8d1
JF
12import shlex
13import fcntl
a4a53f4b 14import signal
a193d8d1
JF
15import logging
16import subprocess
84c47c4f 17import itertools
a4a53f4b
JF
18
19from functools import partial
66eb9ce3 20from ipaddress import IPv4Address
a4a53f4b 21
d486dd0d
JF
22try:
23 from ifupdown2.ifupdown.iface import *
24
70a6640c 25 import ifupdown2.ifupdown.policymanager as policymanager
d486dd0d 26 import ifupdown2.ifupdown.ifupdownflags as ifupdownflags
bd441a51 27except (ImportError, ModuleNotFoundError):
d486dd0d
JF
28 from ifupdown.iface import *
29
70a6640c 30 import ifupdown.policymanager as policymanager
d486dd0d
JF
31 import ifupdown.ifupdownflags as ifupdownflags
32
22b49c28 33
a4a53f4b
JF
34def signal_handler_f(ps, sig, frame):
35 if ps:
36 ps.send_signal(sig)
37 if sig == signal.SIGINT:
38 raise KeyboardInterrupt
14dc390d 39
40class utils():
a193d8d1
JF
41 logger = logging.getLogger('ifupdown')
42 DEVNULL = open(os.devnull, 'w')
70a6640c 43 vlan_aware_bridge_address_support = None
14dc390d 44
7f0310a7
RP
45 vni_max = 16777215
46
594fb088
JF
47 _string_values = {
48 "on": True,
49 "yes": True,
50 "1": True,
d486dd0d 51 "fast": True,
594fb088
JF
52 "off": False,
53 "no": False,
54 "0": False,
03813675
JF
55 "slow": False,
56 True: True,
57 False: False
594fb088
JF
58 }
59
60 _binary_bool = {
61 True: "1",
62 False: "0",
63 }
64
65 _yesno_bool = {
66 True: 'yes',
67 False: 'no'
68 }
69
70 _onoff_bool = {
71 'yes': 'on',
72 'no': 'off'
73 }
74
d486dd0d
JF
75 _onoff_onezero = {
76 '1' : 'on',
77 '0' : 'off'
78 }
79
80 _yesno_onezero = {
81 '1' : 'yes',
82 '0' : 'no'
83 }
84
85 """
86 Set debian path as default path for all the commands.
87 If command not present in debian path, search for the
88 commands in the other system directories.
89 This search is carried out to handle different locations
90 on different distros.
91 If the command is not found in any of the system
92 directories, command execution will fail because we have
93 set default path same as debian path.
94 """
95 bridge_cmd = '/sbin/bridge'
96 ip_cmd = '/bin/ip'
97 brctl_cmd = '/sbin/brctl'
98 pidof_cmd = '/bin/pidof'
99 service_cmd = '/usr/sbin/service'
100 sysctl_cmd = '/sbin/sysctl'
101 modprobe_cmd = '/sbin/modprobe'
102 pstree_cmd = '/usr/bin/pstree'
103 ss_cmd = '/bin/ss'
104 vrrpd_cmd = '/usr/sbin/vrrpd'
105 ifplugd_cmd = '/usr/sbin/ifplugd'
106 mstpctl_cmd = '/sbin/mstpctl'
107 ethtool_cmd = '/sbin/ethtool'
108 systemctl_cmd = '/bin/systemctl'
109 dpkg_cmd = '/usr/bin/dpkg'
110
223ba5af 111 logger.info("utils init command paths")
d486dd0d
JF
112 for cmd in ['bridge',
113 'ip',
114 'brctl',
115 'pidof',
116 'service',
117 'sysctl',
118 'modprobe',
119 'pstree',
120 'ss',
121 'vrrpd',
122 'ifplugd',
123 'mstpctl',
124 'ethtool',
125 'systemctl',
126 'dpkg'
127 ]:
128 if os.path.exists(vars()[cmd + '_cmd']):
129 continue
130 for path in ['/bin/',
131 '/sbin/',
132 '/usr/bin/',
133 '/usr/sbin/',]:
134 if os.path.exists(path + cmd):
135 vars()[cmd + '_cmd'] = path + cmd
136 else:
137 logger.debug('warning: path %s not found: %s won\'t be usable' % (path + cmd, cmd))
138
3b01ed76 139 mac_translate_tab = str.maketrans(":.-,", " ")
223ba5af
JF
140
141 @classmethod
142 def mac_str_to_int(cls, hw_address):
143 mac = 0
144 if hw_address:
3b01ed76 145 pass
223ba5af
JF
146 for i in hw_address.translate(cls.mac_translate_tab).split():
147 mac = mac << 8
148 mac += int(i, 16)
149 return mac
150
d486dd0d
JF
151 @staticmethod
152 def get_onff_from_onezero(value):
153 if value in utils._onoff_onezero:
154 return utils._onoff_onezero[value]
155 return value
156
157 @staticmethod
158 def get_yesno_from_onezero(value):
159 if value in utils._yesno_onezero:
160 return utils._yesno_onezero[value]
161 return value
162
594fb088
JF
163 @staticmethod
164 def get_onoff_bool(value):
165 if value in utils._onoff_bool:
166 return utils._onoff_bool[value]
167 return value
168
169 @staticmethod
70a6640c
JF
170 def get_boolean_from_string(value, default=False):
171 return utils._string_values.get(value, default)
594fb088
JF
172
173 @staticmethod
174 def get_yesno_boolean(bool):
175 return utils._yesno_bool[bool]
176
177 @staticmethod
178 def boolean_support_binary(value):
179 return utils._binary_bool[utils.get_boolean_from_string(value)]
180
181 @staticmethod
182 def is_binary_bool(value):
183 return value == '0' or value == '1'
184
185 @staticmethod
186 def support_yesno_attrs(attrsdict, attrslist, ifaceobj=None):
187 if ifaceobj:
188 for attr in attrslist:
189 value = ifaceobj.get_attr_value_first(attr)
190 if value and not utils.is_binary_bool(value):
191 if attr in attrsdict:
192 bool = utils.get_boolean_from_string(attrsdict[attr])
193 attrsdict[attr] = utils.get_yesno_boolean(bool)
194 else:
195 for attr in attrslist:
196 if attr in attrsdict:
197 attrsdict[attr] = utils.boolean_support_binary(attrsdict[attr])
198
d486dd0d
JF
199 @staticmethod
200 def get_int_from_boolean_and_string(value):
201 try:
202 return int(value)
3218f49d 203 except Exception:
d486dd0d
JF
204 return int(utils.get_boolean_from_string(value))
205
206 @staticmethod
207 def strip_hwaddress(hwaddress):
208 if hwaddress and hwaddress.startswith("ether"):
209 hwaddress = hwaddress[5:].strip()
210 return hwaddress.lower() if hwaddress else hwaddress
211 # we need to "normalize" the user provided MAC so it can match with
212 # what we have in the cache (data retrieved via a netlink dump by
213 # nlmanager). nlmanager return all macs in lower-case
214
14dc390d 215 @classmethod
216 def importName(cls, modulename, name):
217 """ Import a named object """
218 try:
219 module = __import__(modulename, globals(), locals(), [name])
220 except ImportError:
221 return None
222 return getattr(module, name)
d40e96ee 223
224 @classmethod
225 def lockFile(cls, lockfile):
226 try:
227 fp = os.open(lockfile, os.O_CREAT | os.O_TRUNC | os.O_WRONLY)
228 fcntl.flock(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
a193d8d1 229 fcntl.fcntl(fp, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
d40e96ee 230 except IOError:
231 return False
232 return True
233
679e6567
RP
234 @classmethod
235 def parse_iface_range(cls, name):
eba4da6e
RP
236 # eg: swp1.[2-100]
237 # return (prefix, range-start, range-end)
238 # eg return ("swp1.", 1, 20, ".100")
239 range_match = re.match("^([\w]+)\[([\d]+)-([\d]+)\]([\.\w]+)", name)
679e6567
RP
240 if range_match:
241 range_groups = range_match.groups()
242 if range_groups[1] and range_groups[2]:
243 return (range_groups[0], int(range_groups[1], 10),
eba4da6e
RP
244 int(range_groups[2], 10), range_groups[3])
245 else:
246 # eg: swp[1-20].100
247 # return (prefix, range-start, range-end, suffix)
248 # eg return ("swp", 1, 20, ".100")
249 range_match = re.match("^([\w\.]+)\[([\d]+)-([\d]+)\]", name)
250 if range_match:
251 range_groups = range_match.groups()
252 if range_groups[1] and range_groups[2]:
253 return (range_groups[0], int(range_groups[1], 10),
254 int(range_groups[2], 10))
679e6567
RP
255 return None
256
257 @classmethod
258 def expand_iface_range(cls, name):
259 ifacenames = []
eba4da6e
RP
260 irange = cls.parse_iface_range(name)
261 if irange:
262 if len(irange) == 3:
263 # eg swp1.[2-4], r = "swp1.", 2, 4)
264 for i in range(irange[1], irange[2]):
265 ifacenames.append('%s%d' %(irange[0], i))
266 elif len(irange) == 4:
267 for i in range(irange[1], irange[2]):
268 # eg swp[2-4].100, r = ("swp", 2, 4, ".100")
269 ifacenames.append('%s%d%s' %(irange[0], i, irange[3]))
679e6567
RP
270 return ifacenames
271
eba4da6e
RP
272 @classmethod
273 def is_ifname_range(cls, name):
274 if '[' in name or ']' in name:
275 return True
276 return False
277
d3ad131e
ST
278 @classmethod
279 def check_ifname_size_invalid(cls, name=''):
280 """ IFNAMSIZ in include/linux/if.h is 16 so we check this """
281 IFNAMSIZ = 16
282 if len(name) > IFNAMSIZ - 1:
283 return True
284 else:
285 return False
d40e96ee 286
a4a53f4b
JF
287 @classmethod
288 def enable_subprocess_signal_forwarding(cls, ps, sig):
289 signal.signal(sig, partial(signal_handler_f, ps))
290
291 @classmethod
292 def disable_subprocess_signal_forwarding(cls, sig):
293 signal.signal(sig, signal.SIG_DFL)
294
a193d8d1
JF
295 @classmethod
296 def _log_command_exec(cls, cmd, stdin):
ca45cd9e 297 dry_run = "DRY-RUN: " if ifupdownflags.flags.DRYRUN else ""
a193d8d1 298 if stdin:
ca45cd9e 299 cls.logger.info('%sexecuting %s [%s]' % (dry_run, cmd, stdin))
a193d8d1 300 else:
ca45cd9e 301 cls.logger.info('%sexecuting %s' % (dry_run, cmd))
a193d8d1
JF
302
303 @classmethod
304 def _format_error(cls, cmd, cmd_returncode, cmd_output, stdin):
305 if type(cmd) is list:
306 cmd = ' '.join(cmd)
307 if stdin:
308 cmd = '%s [%s]' % (cmd, stdin)
309 if cmd_output:
310 return 'cmd \'%s\' failed: returned %d (%s)' % \
311 (cmd, cmd_returncode, cmd_output)
312 else:
313 return 'cmd \'%s\' failed: returned %d' % (cmd, cmd_returncode)
314
22b49c28
JF
315 @classmethod
316 def is_addr_ip_allowed_on(cls, ifaceobj, syntax_check=False):
70a6640c
JF
317 if cls.vlan_aware_bridge_address_support is None:
318 cls.vlan_aware_bridge_address_support = utils.get_boolean_from_string(
319 policymanager.policymanager_api.get_module_globals(
320 module_name='address',
321 attr='vlan_aware_bridge_address_support'
322 ),
323 True
324 )
22b49c28
JF
325 msg = ('%s: ignoring ip address. Assigning an IP '
326 'address is not allowed on' % ifaceobj.name)
327 if (ifaceobj.role & ifaceRole.SLAVE
328 and not (ifaceobj.link_privflags & ifaceLinkPrivFlags.VRF_SLAVE)):
329 up = None
330 if ifaceobj.upperifaces:
331 up = ifaceobj.upperifaces[0]
332 msg = ('%s enslaved interfaces. %s'
333 % (msg, ('%s is enslaved to %s'
334 % (ifaceobj.name, up)) if up else '')).strip()
335 if syntax_check:
336 cls.logger.warning(msg)
337 else:
338 cls.logger.info(msg)
339 return False
340 elif (ifaceobj.link_kind & ifaceLinkKind.BRIDGE
70a6640c
JF
341 and ifaceobj.link_privflags & ifaceLinkPrivFlags.BRIDGE_VLAN_AWARE
342 and not cls.vlan_aware_bridge_address_support
343 ):
344 msg = '%s bridge vlan aware interfaces' % msg
22b49c28
JF
345 if syntax_check:
346 cls.logger.warning(msg)
347 else:
348 cls.logger.info(msg)
349 return False
350 return True
351
a193d8d1
JF
352 @classmethod
353 def _execute_subprocess(cls, cmd,
354 env=None,
355 shell=False,
356 close_fds=False,
357 stdout=True,
358 stdin=None,
bdbe0379 359 stderr=subprocess.STDOUT):
a193d8d1
JF
360 """
361 exec's commands using subprocess Popen
362 Args:
363 cmd, should be shlex.split if not shell
364 returns: output
365
366 Note: close_fds=True is affecting performance (2~3 times slower)
367 """
368 if ifupdownflags.flags.DRYRUN:
369 return ''
370
371 cmd_output = None
372 try:
373 ch = subprocess.Popen(cmd,
374 env=env,
375 shell=shell,
376 close_fds=close_fds,
377 stdin=subprocess.PIPE if stdin else None,
bdbe0379 378 stdout=subprocess.PIPE if stdout else cls.DEVNULL,
a193d8d1
JF
379 stderr=stderr)
380 utils.enable_subprocess_signal_forwarding(ch, signal.SIGINT)
381 if stdout or stdin:
6ab519dd 382 cmd_output = ch.communicate(input=stdin.encode() if stdin else stdin)[0]
a193d8d1
JF
383 cmd_returncode = ch.wait()
384 except Exception as e:
bdbe0379 385 raise Exception('cmd \'%s\' failed (%s)' % (' '.join(cmd), str(e)))
a193d8d1
JF
386 finally:
387 utils.disable_subprocess_signal_forwarding(signal.SIGINT)
e36ad206 388
eee38e73 389 cmd_output_string = cmd_output.decode() if cmd_output is not None else cmd_output
e36ad206 390
a193d8d1
JF
391 if cmd_returncode != 0:
392 raise Exception(cls._format_error(cmd,
393 cmd_returncode,
e36ad206 394 cmd_output_string,
a193d8d1 395 stdin))
e36ad206 396 return cmd_output_string
a193d8d1
JF
397
398 @classmethod
ac645a1a 399 def exec_user_command(cls, cmd, env=None, close_fds=False, stdout=True,
a193d8d1
JF
400 stdin=None, stderr=subprocess.STDOUT):
401 cls._log_command_exec(cmd, stdin)
402 return cls._execute_subprocess(cmd,
403 shell=True,
ac645a1a 404 env=env,
a193d8d1
JF
405 close_fds=close_fds,
406 stdout=stdout,
407 stdin=stdin,
bdbe0379 408 stderr=stderr)
a193d8d1
JF
409
410 @classmethod
411 def exec_command(cls, cmd, env=None, close_fds=False, stdout=True,
412 stdin=None, stderr=subprocess.STDOUT):
413 cls._log_command_exec(cmd, stdin)
414 return cls._execute_subprocess(shlex.split(cmd),
415 env=env,
416 close_fds=close_fds,
417 stdout=stdout,
418 stdin=stdin,
419 stderr=stderr)
420
421 @classmethod
422 def exec_commandl(cls, cmdl, env=None, close_fds=False, stdout=True,
423 stdin=None, stderr=subprocess.STDOUT):
424 cls._log_command_exec(' '.join(cmdl), stdin)
425 return cls._execute_subprocess(cmdl,
426 env=env,
427 close_fds=close_fds,
428 stdout=stdout,
429 stdin=stdin,
430 stderr=stderr)
431
84c47c4f
RP
432 @classmethod
433 def ints_to_ranges(cls, ints):
434 for a, b in itertools.groupby(enumerate(ints), lambda x_y: x_y[1] - x_y[0]):
435 b = list(b)
436 yield b[0][1], b[-1][1]
437
438 @classmethod
439 def ranges_to_ints(cls, rangelist):
440 """ returns expanded list of integers given set of string ranges
441 example: ['1', '2-4', '6'] returns [1, 2, 3, 4, 6]
442 """
443 result = []
444 try:
445 for part in rangelist:
446 if '-' in part:
447 a, b = part.split('-')
448 a, b = int(a), int(b)
449 result.extend(list(range(a, b + 1)))
450 else:
451 a = int(part)
452 result.append(a)
453 except Exception:
454 cls.logger.warning('unable to parse vids \'%s\'' %''.join(rangelist))
455 pass
456 return result
457
458 @classmethod
459 def compress_into_ranges(cls, ids_ints):
460 return ['%d' %start if start == end else '%d-%d' %(start, end)
461 for start, end in cls.ints_to_ranges(ids_ints)]
462
66eb9ce3
JF
463 @classmethod
464 def compress_into_ip_ranges(cls, ip_list):
465 return [
466 "%s" % IPv4Address(start) if start == end else "%s-%s" % (IPv4Address(start), IPv4Address(end)) for
467 start, end in cls.ints_to_ranges(map(int, ip_list))
468 ]
469
84c47c4f
RP
470 @classmethod
471 def diff_ids(cls, ids1_ints, ids2_ints):
472 return set(ids2_ints).difference(ids1_ints), set(ids1_ints).difference(ids2_ints)
473
474 @classmethod
475 def compare_ids(cls, ids1, ids2, pvid=None, expand_range=True):
476 """ Returns true if the ids are same else return false """
477
478 if expand_range:
479 ids1_ints = cls.ranges_to_ints(ids1)
480 ids2_ints = cls.ranges_to_ints(ids2)
481 else:
482 ids1_ints = cls.ranges_to_ints(ids1)
483 ids2_ints = ids2
484 set_diff = set(ids1_ints).symmetric_difference(ids2_ints)
485 if pvid and int(pvid) in set_diff:
486 set_diff.remove(int(pvid))
487 if set_diff:
488 return False
489 else:
490 return True
491
7f0310a7
RP
492 @classmethod
493 def get_vlan_vni_in_map_entry(cls, vlan_vni_map_entry):
494 # a good example for map is bridge-vlan-vni-map attribute
495 # format eg: <vlan>=<vni>
496 # 1000-1004=5000-5004
497 # 1000-1004=auto /* here vni = vlan */
498 # 1000-1004=auto-10 /* here vni = vlan - 10 */
499 # 1000-1004=auto+10 /* here vni = vlan + 10 */
500
501 vlan = None
502 vni = None
503 try:
504 (vlan, vni) = vlan_vni_map_entry.split('=', 1)
505 if vni == 'auto':
506 vni = vlan
507 elif vni.startswith('auto'):
508 vnistart = 0
509 vniend = 0
510 if vni.startswith('auto+'):
511 vni = vni.split('+', 1)[1]
512 vint = int(vni)
513 if vint < 0:
514 raise Exception("invalid auto vni suffix %d" % (vint))
515 if '-' in vlan:
516 (vstart, vend) = vlan.split('-', 1)
517 vnistart = int(vstart) + vint
518 vniend = int(vend) + vint
519 else:
520 vnistart = int(vlan) + vint
521 elif vni.startswith('auto-'):
522 vni = vni.split('-', 1)[1]
523 vint = int(vni)
524 if vint < 0:
525 raise Exception("invalid auto vni suffix %d" % (vint))
526 if '-' in vlan:
527 (vstart, vend) = vlan.split('-', 1)
528 vnistart = int(vstart) - vint
529 vniend = int(vend) - vint
530 else:
531 vnistart = int(vlan) - vint
532 if (vnistart <= 0 or (vniend > 0 and (vniend < vnistart)) or
533 (vnistart > cls.vni_max) or (vniend > cls.vni_max)):
534 raise Exception("invalid vni - unable to derive auto vni %s" % (vni))
535 if vniend > 0:
536 vni = '%d-%d' % (vnistart, vniend)
537 else:
538 vni = '%d' % (vnistart)
539 except Exception as e:
540 raise Exception(str(e))
541 return
542 return (vlan, vni)
543
544 @classmethod
545 def get_vlan_vnis_in_map(cls, vlan_vni_map):
546 # a good example for map is bridge-vlan-vni-map attribute
547 # format eg: <vlan>=<vni>
548 # 1000-1004=5000-5004
549 # 1000-1004=auto /* here vni = vlan */
550 # 1000-1004=auto-10 /* here vni = vlan - 10 */
551 # 1000-1004=auto+10 /* here vni = vlan + 10 */
552 vnis = []
553 vlans = []
554 for ventry in vlan_vni_map.split():
555 try:
556 (vlan, vni) = cls.get_vlan_vni_in_map_entry(ventry)
557 except Exception as e:
558 cls.logger.error("invalid vlan vni map entry - %s (%s)" % (ventry, str(e)))
559 raise
560 vlans.extend([vlan])
561 vnis.extend([vni])
562 return (vlans, vnis)
563
af8d5db2
RP
564 @classmethod
565 def get_vni_mcastgrp_in_map(cls, vni_mcastgrp_map):
566 vnid = {}
567 for ventry in vni_mcastgrp_map.split():
568 try:
569 (vnis, mcastgrp) = ventry.split('=', 1)
570 vnis_int = utils.ranges_to_ints([vnis])
571 for v in vnis_int:
572 vnid[v] = mcastgrp
573 except Exception as e:
574 cls.logger.error("invalid vlan mcast grp map entry - %s (%s)" % (ventry, str(e)))
575 raise
576 return vnid
577
a193d8d1 578fcntl.fcntl(utils.DEVNULL, fcntl.F_SETFD, fcntl.FD_CLOEXEC)