github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/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 19 class SeekableIterator(object): 20 """An iterator that supports relative seeking.""" 21 22 def __init__(self, iterable): 23 self.iterable = iterable 24 self.index = 0 25 26 def __iter__(self): 27 return self 28 29 def next(self): # Python 2 30 try: 31 value = self.iterable[self.index] 32 self.index += 1 33 return value 34 except IndexError: 35 raise StopIteration 36 37 def __next__(self): # Python 3 38 return self.next() 39 40 def seek(self, n, relative=False): 41 if relative: 42 self.index += n 43 else: 44 self.index = n 45 if self.index < 0 or self.index >= len(self.iterable): 46 raise IndexError 47 48 49 class PhysicalInterface(object): 50 """Represents a physical ('auto') interface.""" 51 52 def __init__(self, definition): 53 self.name = definition.split()[1] 54 55 def __str__(self): 56 return self.name 57 58 59 class LogicalInterface(object): 60 """Represents a logical ('iface') interface.""" 61 62 def __init__(self, definition, options=None): 63 if not options: 64 options = [] 65 _, self.name, self.family, self.method = definition.split() 66 self.options = options 67 self.is_bonded = [x for x in self.options if "bond-" in x] 68 self.is_alias = ":" in self.name 69 self.is_vlan = [x for x in self.options if x.startswith("vlan-raw-device")] 70 self.is_active = self.method == "dhcp" or self.method == "static" 71 self.is_bridged = [x for x in self.options if x.startswith("bridge_ports ")] 72 73 def __str__(self): 74 return self.name 75 76 def bridge_now(self, prefix, bridge_name): 77 # https://wiki.archlinux.org/index.php/Network_bridge 78 # ip addr delete dev <interface name> <cidr> 79 if bridge_name is None: 80 bridge_name = prefix + self.name 81 82 args = { 83 'bridge': bridge_name, 84 'parent': self.name, 85 } 86 87 for o in self.options: 88 if o.startswith('vlan') or o.startswith('bond'): 89 continue 90 option = o.split() 91 if len(option) < 2: 92 args[option[0]] = "" 93 else: 94 args[option[0]] = option[1] 95 96 addr = check_shell_cmd('ip -d addr show {parent}'.format(**args)) 97 flags = re.search('<(.*?)>', addr).group(1).split(',') 98 for exclude_flag in ['LOOPBACK', 'SLAVE']: 99 if exclude_flag in flags: 100 # Don't bridge the loopback interface or slaves of bonds. 101 return 102 103 # Save routes 104 routes = check_shell_cmd('ip route show dev {parent}'.format(**args)) 105 106 print_shell_cmd('ip link add name {bridge} type bridge'.format(**args)) 107 print_shell_cmd('ip link set {bridge} up'.format(**args)) 108 print_shell_cmd('ip link set {parent} master {bridge}'.format(**args)) 109 110 if 'address' in args: 111 print_shell_cmd('ip addr delete dev {parent} {address}'.format(**args)) 112 113 cmd = 'ip addr add dev {bridge} {address}' 114 if 'netmask' in args: 115 cmd += '/{netmask}' 116 117 print_shell_cmd(cmd.format(**args)) 118 119 for route in routes.splitlines(): 120 # ip route replace will add missing routes or update existing ones. 121 print_shell_cmd('ip route replace {} dev {bridge}'.format(route, **args)) 122 123 # Returns an ordered set of stanzas to bridge this interface 124 def bridge(self, prefix, bridge_name, add_auto_stanza): 125 if bridge_name is None: 126 bridge_name = prefix + self.name 127 128 # Note: the testing order here is significant. 129 if not self.is_active or self.is_bridged: 130 return self._bridge_unchanged(add_auto_stanza) 131 elif self.is_alias: 132 return self._bridge_alias(add_auto_stanza) 133 elif self.is_vlan: 134 return self._bridge_vlan(bridge_name, add_auto_stanza) 135 elif self.is_bonded: 136 return self._bridge_bond(bridge_name, add_auto_stanza) 137 else: 138 return self._bridge_device(bridge_name) 139 140 def _bridge_device(self, bridge_name): 141 s1 = IfaceStanza(self.name, self.family, "manual", []) 142 s2 = AutoStanza(bridge_name) 143 options = list(self.options) 144 options.append("bridge_ports {}".format(self.name)) 145 s3 = IfaceStanza(bridge_name, self.family, self.method, options) 146 return [s1, s2, s3] 147 148 def _bridge_vlan(self, bridge_name, add_auto_stanza): 149 stanzas = [] 150 s1 = IfaceStanza(self.name, self.family, "manual", self.options) 151 stanzas.append(s1) 152 if add_auto_stanza: 153 stanzas.append(AutoStanza(bridge_name)) 154 options = [x for x in self.options if not x.startswith("vlan")] 155 options.append("bridge_ports {}".format(self.name)) 156 s3 = IfaceStanza(bridge_name, self.family, self.method, options) 157 stanzas.append(s3) 158 return stanzas 159 160 def _bridge_alias(self, add_auto_stanza): 161 stanzas = [] 162 if add_auto_stanza: 163 stanzas.append(AutoStanza(self.name)) 164 s1 = IfaceStanza(self.name, self.family, self.method, list(self.options)) 165 stanzas.append(s1) 166 return stanzas 167 168 def _bridge_bond(self, bridge_name, add_auto_stanza): 169 stanzas = [] 170 if add_auto_stanza: 171 stanzas.append(AutoStanza(self.name)) 172 s1 = IfaceStanza(self.name, self.family, "manual", list(self.options)) 173 s2 = AutoStanza(bridge_name) 174 options = [x for x in self.options if not x.startswith("bond")] 175 options.append("bridge_ports {}".format(self.name)) 176 s3 = IfaceStanza(bridge_name, self.family, self.method, options) 177 stanzas.extend([s1, s2, s3]) 178 return stanzas 179 180 def _bridge_unchanged(self, add_auto_stanza): 181 stanzas = [] 182 if add_auto_stanza: 183 stanzas.append(AutoStanza(self.name)) 184 s1 = IfaceStanza(self.name, self.family, self.method, list(self.options)) 185 stanzas.append(s1) 186 return stanzas 187 188 189 class Stanza(object): 190 """Represents one stanza together with all of its options.""" 191 192 def __init__(self, definition, options=None): 193 if not options: 194 options = [] 195 self.definition = definition 196 self.options = options 197 self.is_logical_interface = definition.startswith('iface ') 198 self.is_physical_interface = definition.startswith('auto ') 199 self.iface = None 200 self.phy = None 201 if self.is_logical_interface: 202 self.iface = LogicalInterface(definition, self.options) 203 if self.is_physical_interface: 204 self.phy = PhysicalInterface(definition) 205 206 def __str__(self): 207 return self.definition 208 209 210 class NetworkInterfaceParser(object): 211 """Parse a network interface file into a set of stanzas.""" 212 213 @classmethod 214 def is_stanza(cls, s): 215 return re.match(r'^(iface|mapping|auto|allow-|source)', s) 216 217 def __init__(self, filename): 218 self._stanzas = [] 219 with open(filename, 'r') as f: 220 lines = f.readlines() 221 line_iterator = SeekableIterator(lines) 222 for line in line_iterator: 223 if self.is_stanza(line): 224 stanza = self._parse_stanza(line, line_iterator) 225 self._stanzas.append(stanza) 226 227 def _parse_stanza(self, stanza_line, iterable): 228 stanza_options = [] 229 for line in iterable: 230 line = line.strip() 231 if line.startswith('#') or line == "": 232 continue 233 if self.is_stanza(line): 234 iterable.seek(-1, True) 235 break 236 stanza_options.append(line) 237 return Stanza(stanza_line.strip(), stanza_options) 238 239 def stanzas(self): 240 return [x for x in self._stanzas] 241 242 def physical_interfaces(self): 243 return {x.phy.name: x.phy for x in [y for y in self._stanzas if y.is_physical_interface]} 244 245 def __iter__(self): # class iter 246 for s in self._stanzas: 247 yield s 248 249 250 def uniq_append(dst, src): 251 for x in src: 252 if x not in dst: 253 dst.append(x) 254 return dst 255 256 257 def IfaceStanza(name, family, method, options): 258 """Convenience function to create a new "iface" stanza. 259 260 Maintains original options order but removes duplicates with the 261 exception of 'dns-*' options which are normlised as required by 262 resolvconf(8) and all the dns-* options are moved to the end. 263 264 """ 265 266 dns_search = [] 267 dns_nameserver = [] 268 dns_sortlist = [] 269 unique_options = [] 270 271 for o in options: 272 words = o.split() 273 ident = words[0] 274 if ident == "dns-nameservers": 275 dns_nameserver = uniq_append(dns_nameserver, words[1:]) 276 elif ident == "dns-search": 277 dns_search = uniq_append(dns_search, words[1:]) 278 elif ident == "dns-sortlist": 279 dns_sortlist = uniq_append(dns_sortlist, words[1:]) 280 elif o not in unique_options: 281 unique_options.append(o) 282 283 if dns_nameserver: 284 option = "dns-nameservers " + " ".join(dns_nameserver) 285 unique_options.append(option) 286 287 if dns_search: 288 option = "dns-search " + " ".join(dns_search) 289 unique_options.append(option) 290 291 if dns_sortlist: 292 option = "dns-sortlist " + " ".join(dns_sortlist) 293 unique_options.append(option) 294 295 return Stanza("iface {} {} {}".format(name, family, method), unique_options) 296 297 298 def AutoStanza(name): 299 # Convenience function to create a new "auto" stanza. 300 return Stanza("auto {}".format(name)) 301 302 303 def print_stanza(s, stream=sys.stdout): 304 print(s.definition, file=stream) 305 for o in s.options: 306 print(" ", o, file=stream) 307 308 309 def print_stanzas(stanzas, stream=sys.stdout): 310 n = len(stanzas) 311 for i, stanza in enumerate(stanzas): 312 print_stanza(stanza, stream) 313 if stanza.is_logical_interface and i + 1 < n: 314 print(file=stream) 315 316 317 def shell_cmd(s): 318 p = subprocess.Popen(s, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 319 out, err = p.communicate() 320 return [out, err, p.returncode] 321 322 323 def print_shell_cmd(s, verbose=True, exit_on_error=False): 324 if verbose: 325 print(s) 326 out, err, retcode = shell_cmd(s) 327 if out and len(out) > 0: 328 print(out.decode().rstrip('\n')) 329 if err and len(err) > 0: 330 print(err.decode().rstrip('\n')) 331 if exit_on_error and retcode != 0: 332 exit(1) 333 334 335 def check_shell_cmd(s, verbose=False): 336 if verbose: 337 print(s) 338 output = subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).strip().decode("utf-8") 339 if verbose: 340 print(output.rstrip('\n')) 341 return output 342 343 344 def arg_parser(): 345 parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 346 parser.add_argument('--bridge-prefix', help="bridge prefix", type=str, required=False, default='br-') 347 parser.add_argument('--one-time-backup', help='A one time backup of filename', action='store_true', default=True, required=False) 348 parser.add_argument('--activate', help='activate new configuration', action='store_true', default=False, required=False) 349 parser.add_argument('--interface-to-bridge', help="interface to bridge", type=str, required=False) 350 parser.add_argument('--bridge-name', help="bridge name", type=str, required=False) 351 parser.add_argument('filename', help="interfaces(5) based filename") 352 return parser 353 354 355 def main(args): 356 if args.bridge_name and args.interface_to_bridge is None: 357 sys.stderr.write("error: --interface-to-bridge required when using --bridge-name\n") 358 exit(1) 359 360 if args.interface_to_bridge and args.bridge_name is None: 361 sys.stderr.write("error: --bridge-name required when using --interface-to-bridge\n") 362 exit(1) 363 364 stanzas = [] 365 config_parser = NetworkInterfaceParser(args.filename) 366 physical_interfaces = config_parser.physical_interfaces() 367 368 # Bridging requires modifying 'auto' and 'iface' stanzas only. 369 # Calling <iface>.bridge() will return a set of stanzas that cover 370 # both of those stanzas. The 'elif' clause catches all the other 371 # stanza types. The args.interface_to_bridge test is to bridge a 372 # single interface only, which is only used for juju < 2.0. And if 373 # that argument is specified then args.bridge_name takes 374 # precedence over any args.bridge_prefix. 375 376 for s in config_parser.stanzas(): 377 if s.is_logical_interface: 378 add_auto_stanza = s.iface.name in physical_interfaces 379 380 if args.interface_to_bridge and args.interface_to_bridge != s.iface.name: 381 if add_auto_stanza: 382 stanzas.append(AutoStanza(s.iface.name)) 383 stanzas.append(s) 384 else: 385 stanza = s.iface.bridge(args.bridge_prefix, args.bridge_name, add_auto_stanza) 386 stanzas.extend(stanza) 387 388 elif not s.is_physical_interface: 389 stanzas.append(s) 390 391 if not args.activate: 392 print_stanzas(stanzas) 393 exit(0) 394 395 print("**** Original configuration") 396 print_shell_cmd("cat {}".format(args.filename)) 397 print_shell_cmd("ip -d addr show") 398 print_shell_cmd("ip route show") 399 400 for s in config_parser.stanzas(): 401 if s.is_logical_interface: 402 if not(args.interface_to_bridge and args.interface_to_bridge != s.iface.name): 403 s.iface.bridge_now(args.bridge_prefix, args.bridge_name) 404 405 if args.one_time_backup: 406 backup_file = "{}-before-add-juju-bridge".format(args.filename) 407 if not os.path.isfile(backup_file): 408 shutil.copy2(args.filename, backup_file) 409 410 with open(args.filename, 'w') as f: 411 print_stanzas(stanzas, f) 412 f.close() 413 414 print("**** New configuration") 415 print_shell_cmd("cat {}".format(args.filename)) 416 print_shell_cmd("ip -d addr show") 417 print_shell_cmd("ip route show") 418 419 # This script re-renders an interfaces(5) file to add a bridge to 420 # either all active interfaces, or a specific interface. 421 422 if __name__ == '__main__': 423 main(arg_parser().parse_args())