github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/provider/maas/add-juju-bridge.py (about) 1 #!/usr/bin/env python 2 3 # Copyright 2015 Canonical Ltd. 4 # Licensed under the AGPLv3, see LICENCE file for details. 5 6 # 7 # This file has been and should be formatted using pyfmt(1). 8 # 9 10 from __future__ import print_function 11 import argparse 12 import os 13 import re 14 import shutil 15 import subprocess 16 import sys 17 18 # These options are to be removed from a sub-interface and applied to 19 # the new bridged interface. 20 21 BRIDGE_ONLY_OPTIONS = {'address', 'gateway', 'netmask', 'dns-nameservers', 'dns-search', 'dns-sortlist'} 22 23 24 class SeekableIterator(object): 25 """An iterator that supports relative seeking.""" 26 27 def __init__(self, iterable): 28 self.iterable = iterable 29 self.index = 0 30 31 def __iter__(self): 32 return self 33 34 def next(self): # Python 2 35 try: 36 value = self.iterable[self.index] 37 self.index += 1 38 return value 39 except IndexError: 40 raise StopIteration 41 42 def __next__(self): # Python 3 43 return self.next() 44 45 def seek(self, n, relative=False): 46 if relative: 47 self.index += n 48 else: 49 self.index = n 50 if self.index < 0 or self.index >= len(self.iterable): 51 raise IndexError 52 53 54 class PhysicalInterface(object): 55 """Represents a physical ('auto') interface.""" 56 57 def __init__(self, definition): 58 self.name = definition.split()[1] 59 60 def __str__(self): 61 return self.name 62 63 64 class LogicalInterface(object): 65 """Represents a logical ('iface') interface.""" 66 67 def __init__(self, definition, options=None): 68 if not options: 69 options = [] 70 _, self.name, self.family, self.method = definition.split() 71 self.options = options 72 self.is_loopback = self.method == 'loopback' 73 self.is_bonded = [x for x in self.options if "bond-" in x] 74 self.has_bond_master_option, self.bond_master_options = self.has_option(['bond-master']) 75 self.is_alias = ":" in self.name 76 self.is_vlan = [x for x in self.options if x.startswith("vlan-raw-device")] 77 self.is_bridged, self.bridge_ports = self.has_option(['bridge_ports']) 78 self.has_auto_stanza = None 79 self.parent = None 80 81 def __str__(self): 82 return self.name 83 84 def has_option(self, options): 85 for o in self.options: 86 words = o.split() 87 ident = words[0] 88 if ident in options: 89 return True, words[1:] 90 return False, [] 91 92 @classmethod 93 def prune_options(cls, options, invalid_options): 94 result = [] 95 for o in options: 96 words = o.split() 97 if words[0] not in invalid_options: 98 result.append(o) 99 return result 100 101 # Returns an ordered set of stanzas to bridge this interface. 102 def _bridge(self, prefix, bridge_name): 103 if bridge_name is None: 104 bridge_name = prefix + self.name 105 # Note: the testing order here is significant. 106 if self.is_loopback or self.is_bridged or self.has_bond_master_option: 107 return self._bridge_unchanged() 108 elif self.is_alias: 109 if self.parent and self.parent.iface and self.parent.iface.is_bridged: 110 # if we didn't change the parent interface 111 # then we don't change the aliases neither. 112 return self._bridge_unchanged() 113 else: 114 return self._bridge_alias(bridge_name) 115 elif self.is_vlan: 116 return self._bridge_vlan(bridge_name) 117 elif self.is_bonded: 118 return self._bridge_bond(bridge_name) 119 else: 120 return self._bridge_device(bridge_name) 121 122 def _bridge_device(self, bridge_name): 123 stanzas = [] 124 if self.has_auto_stanza: 125 stanzas.append(AutoStanza(self.name)) 126 options = self.prune_options(self.options, BRIDGE_ONLY_OPTIONS) 127 stanzas.append(IfaceStanza(self.name, self.family, "manual", options)) 128 stanzas.append(AutoStanza(bridge_name)) 129 options = list(self.options) 130 options.append("bridge_ports {}".format(self.name)) 131 options = self.prune_options(options, ['mtu']) 132 stanzas.append(IfaceStanza(bridge_name, self.family, self.method, options)) 133 return stanzas 134 135 def _bridge_vlan(self, bridge_name): 136 stanzas = [] 137 if self.has_auto_stanza: 138 stanzas.append(AutoStanza(self.name)) 139 options = self.prune_options(self.options, BRIDGE_ONLY_OPTIONS) 140 stanzas.append(IfaceStanza(self.name, self.family, "manual", options)) 141 stanzas.append(AutoStanza(bridge_name)) 142 options = list(self.options) 143 options.append("bridge_ports {}".format(self.name)) 144 options = self.prune_options(options, ['mtu', 'vlan_id', 'vlan-raw-device']) 145 stanzas.append(IfaceStanza(bridge_name, self.family, self.method, options)) 146 return stanzas 147 148 def _bridge_alias(self, bridge_name): 149 stanzas = [] 150 if self.has_auto_stanza: 151 stanzas.append(AutoStanza(bridge_name)) 152 stanzas.append(IfaceStanza(bridge_name, self.family, self.method, list(self.options))) 153 return stanzas 154 155 def _bridge_bond(self, bridge_name): 156 stanzas = [] 157 if self.has_auto_stanza: 158 stanzas.append(AutoStanza(self.name)) 159 options = self.prune_options(self.options, BRIDGE_ONLY_OPTIONS) 160 stanzas.append(IfaceStanza(self.name, self.family, "manual", options)) 161 stanzas.append(AutoStanza(bridge_name)) 162 options = [x for x in self.options if not x.startswith("bond")] 163 options = self.prune_options(options, ['mtu']) 164 options.append("bridge_ports {}".format(self.name)) 165 stanzas.append(IfaceStanza(bridge_name, self.family, self.method, options)) 166 return stanzas 167 168 def _bridge_unchanged(self): 169 stanzas = [] 170 if self.has_auto_stanza: 171 stanzas.append(AutoStanza(self.name)) 172 stanzas.append(IfaceStanza(self.name, self.family, self.method, list(self.options))) 173 return stanzas 174 175 176 class Stanza(object): 177 """Represents one stanza together with all of its options.""" 178 179 def __init__(self, definition, options=None): 180 if not options: 181 options = [] 182 self.definition = definition 183 self.options = options 184 self.is_logical_interface = definition.startswith('iface ') 185 self.is_physical_interface = definition.startswith('auto ') 186 self.iface = None 187 self.phy = None 188 if self.is_logical_interface: 189 self.iface = LogicalInterface(definition, self.options) 190 if self.is_physical_interface: 191 self.phy = PhysicalInterface(definition) 192 193 def __str__(self): 194 return self.definition 195 196 197 class NetworkInterfaceParser(object): 198 """Parse a network interface file into a set of stanzas.""" 199 200 @classmethod 201 def is_stanza(cls, s): 202 return re.match(r'^(iface|mapping|auto|allow-|source)', s) 203 204 def __init__(self, filename): 205 self._stanzas = [] 206 with open(filename, 'r') as f: 207 lines = f.readlines() 208 line_iterator = SeekableIterator(lines) 209 for line in line_iterator: 210 if self.is_stanza(line): 211 stanza = self._parse_stanza(line, line_iterator) 212 self._stanzas.append(stanza) 213 physical_interfaces = self._physical_interfaces() 214 for s in self._stanzas: 215 if not s.is_logical_interface: 216 continue 217 s.iface.has_auto_stanza = s.iface.name in physical_interfaces 218 219 self._connect_aliases() 220 self._bridged_interfaces = self._find_bridged_ifaces() 221 222 def _parse_stanza(self, stanza_line, iterable): 223 stanza_options = [] 224 for line in iterable: 225 line = line.strip() 226 if line.startswith('#') or line == "": 227 continue 228 if self.is_stanza(line): 229 iterable.seek(-1, True) 230 break 231 stanza_options.append(line) 232 return Stanza(stanza_line.strip(), stanza_options) 233 234 def stanzas(self): 235 return [x for x in self._stanzas] 236 237 def _connect_aliases(self): 238 """Set a reference in the alias interfaces to its related interface""" 239 ifaces = {} 240 aliases = [] 241 for stanza in self._stanzas: 242 if stanza.iface is None: 243 continue 244 245 if stanza.iface.is_alias: 246 aliases.append(stanza) 247 else: 248 ifaces[stanza.iface.name] = stanza 249 250 for alias in aliases: 251 parent_name = alias.iface.name.split(':')[0] 252 if parent_name in ifaces: 253 alias.iface.parent = ifaces[parent_name] 254 255 def _find_bridged_ifaces(self): 256 bridged_ifaces = {} 257 for stanza in self._stanzas: 258 if not stanza.is_logical_interface: 259 continue 260 if stanza.iface.is_bridged: 261 bridged_ifaces[stanza.iface.name] = stanza.iface 262 return bridged_ifaces 263 264 def _physical_interfaces(self): 265 return {x.phy.name: x.phy for x in [y for y in self._stanzas if y.is_physical_interface]} 266 267 def __iter__(self): # class iter 268 for s in self._stanzas: 269 yield s 270 271 def _is_already_bridged(self, name, bridge_port): 272 iface = self._bridged_interfaces.get(name, None) 273 if iface: 274 return bridge_port in iface.bridge_ports 275 return False 276 277 def bridge(self, interface_names_to_bridge, bridge_prefix, bridge_name): 278 bridged_stanzas = [] 279 for s in self.stanzas(): 280 if s.is_logical_interface: 281 if s.iface.name not in interface_names_to_bridge: 282 if s.iface.has_auto_stanza: 283 bridged_stanzas.append(AutoStanza(s.iface.name)) 284 bridged_stanzas.append(s) 285 else: 286 existing_bridge_name = bridge_prefix + s.iface.name 287 if self._is_already_bridged(existing_bridge_name, s.iface.name): 288 if s.iface.has_auto_stanza: 289 bridged_stanzas.append(AutoStanza(s.iface.name)) 290 bridged_stanzas.append(s) 291 else: 292 bridged_stanzas.extend(s.iface._bridge(bridge_prefix, bridge_name)) 293 elif not s.is_physical_interface: 294 bridged_stanzas.append(s) 295 return bridged_stanzas 296 297 298 def uniq_append(dst, src): 299 for x in src: 300 if x not in dst: 301 dst.append(x) 302 return dst 303 304 305 def IfaceStanza(name, family, method, options): 306 """Convenience function to create a new "iface" stanza. 307 308 Maintains original options order but removes duplicates with the 309 exception of 'dns-*' options which are normalised as required by 310 resolvconf(8) and all the dns-* options are moved to the end. 311 312 """ 313 314 dns_search = [] 315 dns_nameserver = [] 316 dns_sortlist = [] 317 unique_options = [] 318 319 for o in options: 320 words = o.split() 321 ident = words[0] 322 if ident == "dns-nameservers": 323 dns_nameserver = uniq_append(dns_nameserver, words[1:]) 324 elif ident == "dns-search": 325 dns_search = uniq_append(dns_search, words[1:]) 326 elif ident == "dns-sortlist": 327 dns_sortlist = uniq_append(dns_sortlist, words[1:]) 328 elif o not in unique_options: 329 unique_options.append(o) 330 331 if dns_nameserver: 332 option = "dns-nameservers " + " ".join(dns_nameserver) 333 unique_options.append(option) 334 335 if dns_search: 336 option = "dns-search " + " ".join(dns_search) 337 unique_options.append(option) 338 339 if dns_sortlist: 340 option = "dns-sortlist " + " ".join(dns_sortlist) 341 unique_options.append(option) 342 343 return Stanza("iface {} {} {}".format(name, family, method), unique_options) 344 345 346 def AutoStanza(name): 347 # Convenience function to create a new "auto" stanza. 348 return Stanza("auto {}".format(name)) 349 350 351 def print_stanza(s, stream=sys.stdout): 352 print(s.definition, file=stream) 353 for o in s.options: 354 print(" ", o, file=stream) 355 356 357 def print_stanzas(stanzas, stream=sys.stdout): 358 n = len(stanzas) 359 for i, stanza in enumerate(stanzas): 360 print_stanza(stanza, stream) 361 if stanza.is_logical_interface and i + 1 < n: 362 print(file=stream) 363 364 365 def shell_cmd(s): 366 p = subprocess.Popen(s, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 367 out, err = p.communicate() 368 return [out, err, p.returncode] 369 370 371 def print_shell_cmd(s, verbose=True, exit_on_error=False): 372 if verbose: 373 print(s) 374 out, err, retcode = shell_cmd(s) 375 if out and len(out) > 0: 376 print(out.decode().rstrip('\n')) 377 if err and len(err) > 0: 378 print(err.decode().rstrip('\n')) 379 if exit_on_error and retcode != 0: 380 exit(1) 381 382 383 def check_shell_cmd(s, verbose=False): 384 if verbose: 385 print(s) 386 output = subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).strip().decode("utf-8") 387 if verbose: 388 print(output.rstrip('\n')) 389 return output 390 391 392 def arg_parser(): 393 parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 394 parser.add_argument('--bridge-prefix', help="bridge prefix", type=str, required=False, default='br-') 395 parser.add_argument('--one-time-backup', help='A one time backup of filename', action='store_true', default=True, required=False) 396 parser.add_argument('--activate', help='activate new configuration', action='store_true', default=False, required=False) 397 parser.add_argument('--interfaces-to-bridge', help="interfaces to bridge; space delimited", type=str, required=True) 398 parser.add_argument('--bridge-name', help="bridge name", type=str, required=False) 399 parser.add_argument('filename', help="interfaces(5) based filename") 400 return parser 401 402 403 def main(args): 404 interfaces = args.interfaces_to_bridge.split() 405 406 if len(interfaces) == 0: 407 sys.stderr.write("error: no interfaces specified\n") 408 exit(1) 409 410 if args.bridge_name and len(interfaces) > 1: 411 sys.stderr.write("error: cannot use single bridge name '{}' against multiple interface names\n".format(args.bridge_name)) 412 exit(1) 413 414 parser = NetworkInterfaceParser(args.filename) 415 stanzas = parser.bridge(interfaces, args.bridge_prefix, args.bridge_name) 416 417 if not args.activate: 418 print_stanzas(stanzas) 419 exit(0) 420 421 if args.one_time_backup: 422 backup_file = "{}-before-add-juju-bridge".format(args.filename) 423 if not os.path.isfile(backup_file): 424 shutil.copy2(args.filename, backup_file) 425 426 ifquery = "$(ifquery --interfaces={} --exclude=lo --list)".format(args.filename) 427 428 print("**** Original configuration") 429 print_shell_cmd("cat {}".format(args.filename)) 430 print_shell_cmd("ifconfig -a") 431 print_shell_cmd("ifdown --exclude=lo --interfaces={} {}".format(args.filename, ifquery)) 432 433 print("**** Activating new configuration") 434 435 with open(args.filename, 'w') as f: 436 print_stanzas(stanzas, f) 437 f.close() 438 439 # On configurations that have bonds in 802.3ad mode there is a 440 # race condition betweeen an immediate ifdown then ifup. 441 # 442 # On the h/w I have a 'sleep 0.1' is sufficient but to accommodate 443 # other setups we arbitrarily choose something larger. We don't 444 # want to massively slow bootstrap down but, equally, 0.1 may be 445 # too small for other configurations. 446 447 for s in stanzas: 448 if s.is_logical_interface and s.iface.is_bonded: 449 print("working around https://bugs.launchpad.net/ubuntu/+source/ifenslave/+bug/1269921") 450 print("working around https://bugs.launchpad.net/juju-core/+bug/1594855") 451 print_shell_cmd("sleep 3") 452 break 453 454 print_shell_cmd("cat {}".format(args.filename)) 455 print_shell_cmd("ifup --exclude=lo --interfaces={} {}".format(args.filename, ifquery)) 456 print_shell_cmd("ip link show up") 457 print_shell_cmd("ifconfig -a") 458 print_shell_cmd("ip route show") 459 print_shell_cmd("brctl show") 460 461 # This script re-renders an interfaces(5) file to add a bridge to 462 # either all active interfaces, or a specific interface. 463 464 if __name__ == '__main__': 465 main(arg_parser().parse_args())