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