github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 # Returns an ordered set of stanzas to bridge this interface. 77 def bridge(self, prefix, bridge_name, add_auto_stanza): 78 if bridge_name is None: 79 bridge_name = prefix + self.name 80 # Note: the testing order here is significant. 81 if not self.is_active or self.is_bridged: 82 return self._bridge_unchanged(add_auto_stanza) 83 elif self.is_alias: 84 return self._bridge_alias(add_auto_stanza) 85 elif self.is_vlan: 86 return self._bridge_vlan(prefix, bridge_name, add_auto_stanza) 87 elif self.is_bonded: 88 return self._bridge_bond(prefix, bridge_name, add_auto_stanza) 89 else: 90 return self._bridge_device(prefix, bridge_name) 91 92 def _bridge_device(self, prefix, bridge_name): 93 s1 = IfaceStanza(self.name, self.family, "manual", []) 94 s2 = AutoStanza(bridge_name) 95 options = list(self.options) 96 options.append("bridge_ports {}".format(self.name)) 97 options.append("bridge_stp off") 98 options.append("bridge_maxwait 0") 99 s3 = IfaceStanza(bridge_name, self.family, self.method, options) 100 return [s1, s2, s3] 101 102 def _bridge_vlan(self, prefix, bridge_name, add_auto_stanza): 103 stanzas = [] 104 s1 = IfaceStanza(self.name, self.family, "manual", self.options) 105 stanzas.append(s1) 106 if add_auto_stanza: 107 stanzas.append(AutoStanza(bridge_name)) 108 options = [x for x in self.options if not x.startswith("vlan")] 109 options.append("bridge_ports {}".format(self.name)) 110 options.append("bridge_stp off") 111 options.append("bridge_maxwait 0") 112 s3 = IfaceStanza(bridge_name, self.family, self.method, options) 113 stanzas.append(s3) 114 return stanzas 115 116 def _bridge_alias(self, add_auto_stanza): 117 stanzas = [] 118 if add_auto_stanza: 119 stanzas.append(AutoStanza(self.name)) 120 s1 = IfaceStanza(self.name, self.family, self.method, list(self.options)) 121 stanzas.append(s1) 122 return stanzas 123 124 def _bridge_bond(self, prefix, bridge_name, add_auto_stanza): 125 stanzas = [] 126 if add_auto_stanza: 127 stanzas.append(AutoStanza(self.name)) 128 s1 = IfaceStanza(self.name, self.family, "manual", list(self.options)) 129 s2 = AutoStanza(bridge_name) 130 options = [x for x in self.options if not x.startswith("bond")] 131 options.append("bridge_ports {}".format(self.name)) 132 options.append("bridge_stp off") 133 options.append("bridge_maxwait 0") 134 s3 = IfaceStanza(bridge_name, self.family, self.method, options) 135 stanzas.extend([s1, s2, s3]) 136 return stanzas 137 138 def _bridge_unchanged(self, add_auto_stanza): 139 stanzas = [] 140 if add_auto_stanza: 141 stanzas.append(AutoStanza(self.name)) 142 s1 = IfaceStanza(self.name, self.family, self.method, list(self.options)) 143 stanzas.append(s1) 144 return stanzas 145 146 147 class Stanza(object): 148 """Represents one stanza together with all of its options.""" 149 150 def __init__(self, definition, options=None): 151 if not options: 152 options = [] 153 self.definition = definition 154 self.options = options 155 self.is_logical_interface = definition.startswith('iface ') 156 self.is_physical_interface = definition.startswith('auto ') 157 self.iface = None 158 self.phy = None 159 if self.is_logical_interface: 160 self.iface = LogicalInterface(definition, self.options) 161 if self.is_physical_interface: 162 self.phy = PhysicalInterface(definition) 163 164 def __str__(self): 165 return self.definition 166 167 168 class NetworkInterfaceParser(object): 169 """Parse a network interface file into a set of stanzas.""" 170 171 @classmethod 172 def is_stanza(cls, s): 173 return re.match(r'^(iface|mapping|auto|allow-|source)', s) 174 175 def __init__(self, filename): 176 self._stanzas = [] 177 with open(filename, 'r') as f: 178 lines = f.readlines() 179 line_iterator = SeekableIterator(lines) 180 for line in line_iterator: 181 if self.is_stanza(line): 182 stanza = self._parse_stanza(line, line_iterator) 183 self._stanzas.append(stanza) 184 185 def _parse_stanza(self, stanza_line, iterable): 186 stanza_options = [] 187 for line in iterable: 188 line = line.strip() 189 if line.startswith('#') or line == "": 190 continue 191 if self.is_stanza(line): 192 iterable.seek(-1, True) 193 break 194 stanza_options.append(line) 195 return Stanza(stanza_line.strip(), stanza_options) 196 197 def stanzas(self): 198 return [x for x in self._stanzas] 199 200 def physical_interfaces(self): 201 return {x.phy.name: x.phy for x in [y for y in self._stanzas if y.is_physical_interface]} 202 203 def __iter__(self): # class iter 204 for s in self._stanzas: 205 yield s 206 207 208 def uniq_append(dst, src): 209 for x in src: 210 if x not in dst: 211 dst.append(x) 212 return dst 213 214 215 def IfaceStanza(name, family, method, options): 216 """Convenience function to create a new "iface" stanza. 217 218 Maintains original options order but removes duplicates with the 219 exception of 'dns-*' options which are normlised as required by 220 resolvconf(8) and all the dns-* options are moved to the end. 221 222 """ 223 224 dns_search = [] 225 dns_nameserver = [] 226 dns_sortlist = [] 227 unique_options = [] 228 229 for o in options: 230 words = o.split() 231 ident = words[0] 232 if ident == "dns-nameservers": 233 dns_nameserver = uniq_append(dns_nameserver, words[1:]) 234 elif ident == "dns-search": 235 dns_search = uniq_append(dns_search, words[1:]) 236 elif ident == "dns-sortlist": 237 dns_sortlist = uniq_append(dns_sortlist, words[1:]) 238 elif o not in unique_options: 239 unique_options.append(o) 240 241 if dns_nameserver: 242 option = "dns-nameservers " + " ".join(dns_nameserver) 243 unique_options.append(option) 244 245 if dns_search: 246 option = "dns-search " + " ".join(dns_search) 247 unique_options.append(option) 248 249 if dns_sortlist: 250 option = "dns-sortlist " + " ".join(dns_sortlist) 251 unique_options.append(option) 252 253 return Stanza("iface {} {} {}".format(name, family, method), unique_options) 254 255 256 def AutoStanza(name): 257 # Convenience function to create a new "auto" stanza. 258 return Stanza("auto {}".format(name)) 259 260 261 def print_stanza(s, stream=sys.stdout): 262 print(s.definition, file=stream) 263 for o in s.options: 264 print(" ", o, file=stream) 265 266 267 def print_stanzas(stanzas, stream=sys.stdout): 268 n = len(stanzas) 269 for i, stanza in enumerate(stanzas): 270 print_stanza(stanza, stream) 271 if stanza.is_logical_interface and i + 1 < n: 272 print(file=stream) 273 274 275 def shell_cmd(s): 276 p = subprocess.Popen(s, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 277 out, err = p.communicate() 278 return [out, err, p.returncode] 279 280 281 def print_shell_cmd(s, verbose=True, exit_on_error=False): 282 if verbose: 283 print(s) 284 out, err, retcode = shell_cmd(s) 285 if out and len(out) > 0: 286 print(out.decode().rstrip('\n')) 287 if err and len(err) > 0: 288 print(err.decode().rstrip('\n')) 289 if exit_on_error and retcode != 0: 290 exit(1) 291 292 293 def check_shell_cmd(s, verbose=False): 294 if verbose: 295 print(s) 296 output = subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).strip().decode("utf-8") 297 if verbose: 298 print(output.rstrip('\n')) 299 return output 300 301 302 def arg_parser(): 303 parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 304 parser.add_argument('--bridge-prefix', help="bridge prefix", type=str, required=False, default='br-') 305 parser.add_argument('--one-time-backup', help='A one time backup of filename', action='store_true', default=True, required=False) 306 parser.add_argument('--activate', help='activate new configuration', action='store_true', default=False, required=False) 307 parser.add_argument('--interface-to-bridge', help="interface to bridge", type=str, required=False) 308 parser.add_argument('--bridge-name', help="bridge name", type=str, required=False) 309 parser.add_argument('filename', help="interfaces(5) based filename") 310 return parser 311 312 313 def main(args): 314 if args.bridge_name and args.interface_to_bridge is None: 315 sys.stderr.write("error: --interface-to-bridge required when using --bridge-name\n") 316 exit(1) 317 318 if args.interface_to_bridge and args.bridge_name is None: 319 sys.stderr.write("error: --bridge-name required when using --interface-to-bridge\n") 320 exit(1) 321 322 stanzas = [] 323 config_parser = NetworkInterfaceParser(args.filename) 324 physical_interfaces = config_parser.physical_interfaces() 325 326 # Bridging requires modifying 'auto' and 'iface' stanzas only. 327 # Calling <iface>.bridge() will return a set of stanzas that cover 328 # both of those stanzas. The 'elif' clause catches all the other 329 # stanza types. The args.interface_to_bridge test is to bridge a 330 # single interface only, which is only used for juju < 2.0. And if 331 # that argument is specified then args.bridge_name takes 332 # precendence over any args.bridge_prefix. 333 334 for s in config_parser.stanzas(): 335 if s.is_logical_interface: 336 add_auto_stanza = s.iface.name in physical_interfaces 337 if args.interface_to_bridge and args.interface_to_bridge != s.iface.name: 338 if add_auto_stanza: 339 stanzas.append(AutoStanza(s.iface.name)) 340 stanzas.append(s) 341 else: 342 stanzas.extend(s.iface.bridge(args.bridge_prefix, args.bridge_name, add_auto_stanza)) 343 elif not s.is_physical_interface: 344 stanzas.append(s) 345 346 if not args.activate: 347 print_stanzas(stanzas) 348 exit(0) 349 350 if args.one_time_backup: 351 backup_file = "{}-before-add-juju-bridge".format(args.filename) 352 if not os.path.isfile(backup_file): 353 shutil.copy2(args.filename, backup_file) 354 355 ifquery = "$(ifquery --interfaces={} --exclude=lo --list)".format(args.filename) 356 357 print("**** Original configuration") 358 print_shell_cmd("cat {}".format(args.filename)) 359 print_shell_cmd("ifconfig -a") 360 print_shell_cmd("ifdown --exclude=lo --interfaces={} {}".format(args.filename, ifquery)) 361 362 print("**** Activating new configuration") 363 364 with open(args.filename, 'w') as f: 365 print_stanzas(stanzas, f) 366 f.close() 367 368 print_shell_cmd("cat {}".format(args.filename)) 369 print_shell_cmd("ifup --exclude=lo --interfaces={} {}".format(args.filename, ifquery)) 370 print_shell_cmd("ip link show up") 371 print_shell_cmd("ifconfig -a") 372 print_shell_cmd("ip route show") 373 print_shell_cmd("brctl show") 374 375 # This script re-renders an interfaces(5) file to add a bridge to 376 # either all active interfaces, or a specific interface. 377 378 if __name__ == '__main__': 379 main(arg_parser().parse_args())